最近在整理自己的知识体系,发现很多知识当时学的时候很费时间精力,回头看抓住要点很容易理解。输出是最好的学习方法,所以就想写这么一个系列,整理下各种知识要点和大家分享。
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
,扩容相关的noverflow
、oldbuckets
、nevacuate
等。
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的访问、扩容等内容也可以查看这篇文章,里面有详细介绍。
Go里面map的其他问题
map 是值类型还是引用类型
引用类型, interface、map、channel、slice、func都是引用类型,这就意味着如果将map作为参数传到函数里面,函数里的修改会同步生效。
map是线程安全的吗
不是,要想线程安全的话需要用sync.map
结构,或者自己定义map+mutex
结构体。
哪些类型可以做map里面的key
所有可比较的类型都可以作为 key,简单来说,除了func、map、slice,其他类型都可以当作map的key。具体可参考Go语言中的"=="比较规则