和许多编程语言一样,在 Go 中,字典是一组键-值对( Go 中称键-元素对)的集合。
存储/查找原理
当我们要存储或者查找某个键-元素对的时候,哈希表会先使用哈希函数将键值转换为哈希值,哈希值一般是一个无符号的整数。
一个哈希表内会存有一定数量的哈希桶,在字典的结构里面,有一个属性 B ,这个属性代表当前字典里面桶的个数 (2^B) 。
// 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
}
比如当 B 为 5 的时候,通过获取哈希值的低 5 位就能判断出当前键-元素对应该存放在哪一个桶里面。例如我们通过哈希函数,获取到了一个键-元素对中键值的哈希值为
1001011100001111011011001000111100101010001001011001010101011011
其中,低 5 位代表其所属的桶的位置,11011 换算为十进制为 26 ,即该键-元素对存在第 26 个桶内。哈希桶内存储的是“键的哈希值-内部结构”对的集合,即是按照 键1 键2 … 键8 元素1 元素2 … 元素8 溢出指针 的方式存储,是一块连续的内存,且键和元素时捆绑存储的。我们找到哈希桶之后,再对比键值,就可以定位我们所以需要的键的位置,又因为键 - 元素对是捆绑存储的,所以找到了键就等于是找到对应的元素值。
存储时也是同样的道理,但是要注意的是,每一个存储桶最多只能存储 8 个键-元素对,当超出 8 个的时候,就会生成一个溢出桶,并且当前哈希桶的溢出指针(上述连续内存的最后一块)会指向新生成的溢出桶。
限制
其实从上面就可以看出,字典类型其实是一个哈希表的一个特定实现,其中键和元素的最大区别在于键必须是可以哈希的,而元素却可以是任意类型的,因此字典中的键类型是受限的。
字典声明
// 声明字典 是个 nil 未初始化,直接存值会报错
var s0 map[string] int
// 声明字典并初始化
s1 := map[string]int{}
// 使用 make 声明
s2 := make(map[string] int)
fmt.Println(s0, s1, s2, s3)
-------结果-------------------------
map[] map[] map[]
要注意:声明字典的时候 key 的类型不能是函数、字典、切片。因为根据上面查找字典键-元素对的过程可以知道,最后是要通过比较桶内键和要查询的键是不是一样来确定键-元素对的位置的,但是这三种类型不支持判等操作,所以键的类型不支持这三种,编译器会直接报错。
但是有一个比较特殊的类型:接口 interface{},interface{} 是支持判等操作的,所以编译器不会报错。但是又因为 interface{} 这个空接口相当于是个万能类型,可以接受任何类型的值,所以会出现以下情况的代码:
var s4 = map[interface{}]int{
"1": 1,
[]int{2}: 2,
3: 3,
}
fmt.Println(s4)
------结果--------------
panic: runtime error: hash of unhashable type []int
当我们运行时,就会出现 panic 恐慌。程序运行出现这样的报错我们还能及时调整,但在程序运行时,我们添加了这样的键值对进去导致系统异常,再修改就为时已晚了,所以我们最好不要使用 interface{} 作为键的类型,而且我们要优先考虑计算哈希值比较快的类型作为字典的键类型 。
字典赋值
//初始化
s0 := map[string]int{}
fmt.Println(s0)
//添加key-value
s0["one"] = 1
s0["two"] = 2
fmt.Println(s0)
//修改指定key的值
s0["one"] = 11
s0["two"] = 22
fmt.Println(s0)
//删除指定key的元素
delete(s0, "one")
fmt.Println(s0)
//获取key-value对个数
fmt.Println(len(s0))
------结果-------------------
map[]
map[one:1 two:2]
map[one:11 two:22]
map[two:22]
1
特殊类型修改值
如果值的类型是数组或者结构体,那么不能直接修改 value 成员
s0 := map[string]struct {
x int
}{}
s0["one"] = struct{ x int }{1}
s0["two"] = struct{ x int }{2}
s0["one"].x = 1 //这里编译器会直接报错
方法一:先获取全部value,修改之后重新赋值
s0 := map[string]struct {
x int
}{}
s0["one"] = struct{ x int }{1}
s0["two"] = struct{ x int }{2}
s0["one"].x = 1 //这里编译器会直接报错
// 正确做法一
s1 := s0["one"]
s1.x = 111
s0["one"] = s1
fmt.Println(s0)
-----结果------------------
map[one:{111} two:{2}]
方法二:使用指针类型
* 开头表示是指针类型
& 是取址符号,即获取对应程序实体对象的地址
// 正确做法二
// value 的类型是指针类型,指针指向结构体
s0 := map[string]*struct {
x int
}{}
//创建一个结构体并把指针添加到字典中
s0["one"] = &struct{ x int }{1}
fmt.Println(*s0["one"])
s0["one"].x = 111
fmt.Println(*s0["one"])
-----结果------------------
{1}
{111}
字典遍历
s0 := map[string]int{}
s0["one"] = 1
s0["two"] = 2
//接收 key 和 value
for k, vla := range s0 {
fmt.Printf("%s:%d\n", k, vla)
}
fmt.Println("-----分割线---------------")
//只接收key
for k := range s0 {
fmt.Printf("%s:%d\n", k, s0[k])
}
-----结果----------------
one:1
two:2
-----分割线---------------
one:1
two:2
总结字典特性
- 字典的键类型是有限制的,必须支持哈希和判等
- 字典是无序的,每次遍历的顺序都可能不一样
- 如果值类型是结构体或者数组,那么不能直接对值的成员进行操作
- 不能对 nil 字典进行赋值操作,但是可以读,读出来是一个空字典 map[]
- 字典是线程不安全的,多个线程对同一个字典进行操作会导致报错
- 可以在迭代过程中删除或者添加键-元素对
到此这篇关于Go字典使用详解的文章就介绍到这了,更多相关Go字典内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!