三分钟搞定Go里面的Map|Go主题月

341 阅读5分钟

最近在整理自己的知识体系,发现很多知识当时学的时候很费时间精力,回头看抓住要点很容易理解。输出是最好的学习方法,所以就想写这么一个系列,整理下各种知识要点和大家分享。

go语言的基础概念大概会是有以下几个部分:

  • 基础用法,方便大家快速掌握;
  • 底层实现结构,以便大家迅速get到实现原理;
  • 常见问题,以便更好得落地应用;
  • 参考文章,因为是抓要点,所以内容不会太详细,但会在这一部分给出我看过的比较好的参考文章,以便继续深入研究。

Map用法

map是每个语言中都有的结构,有的叫map,有的叫dictionary,标准的翻译是映射,也称为关联数组或字典或哈希表,核心的作用就是存储一个key-value的键值对,其中key是不允许重复的。

声明和初始化

在Go语言中,map可以用make关键字初始化。

m = make(map[string]string)
m["a"] = "b"

make关键字初始化的map是一个空值nil,这里多说一句,make关键字只能用于初始化 slice,map,channel,且初始化后的都是空值nil,但这个空值是带有类型的。

var m map[string]string
if m == nil {
    fmt.Printf("m is nil --> %#v \n ", m) //map[string]string(nil)
}

map也可以同时进行声明和初始化

var m = map[string]string{
	"a": "go",
	"b": "gogo",
}

增删改查

增删改查比较直观,就直接贴代码了。需要注意一点,Go里面的map每次遍历都是随机的,即同样一个map遍历两次得到的结果可能是不一样的。

 	// 增、改数据
	m1 := map[int]string{1: "go", 2: "zen"}
	m1[1] = "zen"
	m1[3] = "gogo"
	fmt.Println(m1)

	m2 := make(map[int]string, 3)
	m2[0] = "aaa"
	m2[1] = "bbb"
	fmt.Println(m2)
	fmt.Println(m2[0], m2[1])

	// key,value遍历 
	m3 := map[int]string{1: "go", 2: "zen"}
	for k, v := range m3 {
		fmt.Printf("%d:%s\n", k, v)
	}
        // key 遍历
	for k := range m3 {
		fmt.Printf("%d:%s\n", k, m3[k])
	}
	// 获取
	value, ok := m3[1]
	fmt.Println("value:", value, "ok:", ok)

	//删除
	m4 := map[int]string{1: "go", 2: "zen"}
	delete(m4, 1)
	for k, v := range m4 {
		fmt.Printf("%d:%s\n", k, v)
	}

底层实现

不同语言中map的实现方式各有不同,总的来说有两种底层实现结构,一种是哈希表,一种是搜索树。如果采用哈希表实现,还需要解决哈希碰撞的问题,解决方法有开放地址法链地址法。 Go语言中的map基于哈希表来实现,并使用链地址法解决哈希碰撞问题。结构体定义在源代码的go/src/runtime/map.go文件中,具体如下:

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}

源码中的注释比较清晰,主要属性包括最底层的buckets,标志当前个数的count,标记容量的B,处理读写冲突的flags,哈希函数要用到的hash0,扩容相关的noverflowoldbucketsnevacuate等。 buckets指向一个 2^B大小的数组,最终真正存储数据,当没有元素的时候可能是空值nil,其底层的结构是bucket,最终指向bmap

type bmap struct {
    tophash [bucketCnt]uint8
}

tophash是高8位的哈希值。而tophash 除了放正常的高 8 位的 hash 值,还会在空闲、迁移时存储一些特征的状态值,所以合法的 tophash(指计算出来的那种),最小也应该是 4,小于 4 的表示的都是定义的状态值。 由于哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导,因此这个结构在编译期间会动态地创建一个新的结构:

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

这里我们就终于看到了key、value的存放地方了。可以看到每个bucket最多存放8个元素,而且还有指向下一个的指针,这就是前面提到的采用链地址法解决哈希冲突。

整体的结构如下图所示,图片来源:《深度解密Go语言之map》,关于更多的对map的访问、扩容等内容也可以查看这篇文章,里面有详细介绍。

image.png

Go里面map的其他问题

map 是值类型还是引用类型

引用类型, interface、map、channel、slice、func都是引用类型,这就意味着如果将map作为参数传到函数里面,函数里的修改会同步生效。

map是线程安全的吗

不是,要想线程安全的话需要用sync.map结构,或者自己定义map+mutex结构体。

哪些类型可以做map里面的key

所有可比较的类型都可以作为 key,简单来说,除了func、map、slice,其他类型都可以当作map的key。具体可参考Go语言中的"=="比较规则

参考文章

A Tour Of Go

深度解密Go语言之map

曹春晖大佬关于map的文章,强烈建议看一下

Go 语言设计与实现