Go 语言的字典类型其实是一个哈希表(hash table)的特定实现。
在这个实现中,键和元素的最大不同在于,前者的类型是受限的,不可以是函数类型、字典类型和切片类型(因为不支持 == 和 != 判等操作 ),而后者却可以是任意类型的。
键和元素的这种对应关系在数学里就被称为“映射”,这也是“map”这个词的本意,哈希表的映射过程就存在于对键 - 元素对的增、删、改、查的操作之中
aMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
k := "two"
v, ok := aMap[k]
if ok {
fmt.Printf("The element of key %q: %d\n", k, v)
} else {
fmt.Println("Not found!")
}
映射过程的第一步就是把键值转换为哈希值。在 Go 语言的字典中,每一个键值都是由它的哈希值代表的。
宽度越小的类型速度通常越快。对于字符串类型,由于它的宽度是不定的,所以要看它的值的具体长度,长度越短求哈希越快。
由于字典是引用类型,所以当我们仅声明而不初始化一个字典类型的变量的时候,它的值会是nil。
除了添加键 - 元素对,我们在一个值为nil的字典上做任何操都不会引起错误。当我们试图在一个值为nil的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个panic.
package main
import "fmt"
func main() {
var m map[string]int
key := "two"
elem, ok := m["two"]
fmt.Printf("The element paired with key %q in nil map: %d (%v)\n",
key, elem, ok)
fmt.Printf("The length of nil map: %d\n",
len(m))
fmt.Printf("Delete the key-element pair by key %q...\n",
key)
delete(m, key)
elem = 2
fmt.Println("Add a key-element pair to a nil map...")
m["two"] = elem // 这里会引发panic。
}
map基本数据结构
map的底层结构是hmap(即hashmap的缩写),核心元素是一个由若干个桶(bucket,结构为bmap)组成的数组,每个bucket可以存放若干元素(通常是8个),key通过哈希算法被归入不同的bucket中。当超过8个元素需要存入某个bucket时,hmap会使用extra中的overflow来拓展该bucket。下面是hmap的结构体。
type hmap struct {
count int // # 元素个数
flags uint8
B uint8 // 说明包含2^B个bucket
noverflow uint16 // 溢出的bucket的个数
hash0 uint32 // hash种子
buckets unsafe.Pointer // buckets的数组指针
oldbuckets unsafe.Pointer // 结构扩容的时候用于复制的buckets数组
nevacuate uintptr // 搬迁进度(已经搬迁的buckets数量)
extra *mapextra
}
extra中不仅有overflow,还有oldoverflow(用于扩容)和nextoverflow(prealloc的地址)。
bucket(bmap)的结构如下
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the
// code a bit more complicated than alternating key/value/key/value/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
tophash用于记录8个key哈希值的高8位,这样在寻找对应key的时候可以更快,不必每次都对key做全等判断。
注意后面几行注释,hmap并非只有一个tophash,而是后面紧跟8组kv对和一个overflow的指针,这样才能使overflow成为一个链表的结构。但是这两个结构体并不是显示定义的,而是直接通过指针运算进行访问的。
kv的存储形式为”key0key1key2key3…key7val1val2val3…val7″,这样做的好处是:在key和value的长度不同的时候,节省padding空间。如上面的例子,在map[int64]int8中,4个相邻的int8可以存储在同一个内存单元中。如果使用kv交错存储的话,每个int8都会被padding占用单独的内存单元(为了提高寻址速度)。
map练习
package main
import (
"fmt"
"sort"
)
func main() {
//定义一个map并初始化,根据key获取value
colors := map[string]string{
"bird": "blue",
"snake": "green",
"cat": "black",
}
// Get color of snake.
c := colors["snake"]
// Display string.
fmt.Println(c)
//通过==进行map赋值
colors["rabbit"] = "white"
fmt.Println(colors)
names := map[int]string{}
// Add three pairs to the map in separate statements.
names[990] = "file.txt"
names[1009] = "data.xls"
names[1209] = "image.jpg"
// There are three pairs in the map.
fmt.Println(len(names))
//通过delete删除
stus := map[int]string{}
stus[0] = "zhangsan"
stus[1] = "lisi"
stus[2] = "wang5"
stus[3] = "ma6"
fmt.Println(stus)
delete(stus, 2)
fmt.Println(stus)
//map的range遍历
animals := map[string]string{}
animals["cat"] = "Mitty"
animals["dog"] = "hashi"
//look over the map
for key, value := range animals {
fmt.Println(key, "=", value)
}
//获取map中所有的key
sizes := map[string]int{
"XL": 20,
"L": 10,
"M": 5,
}
// Loop over map and append keys to empty slice.
keys := []string{}
for key, _ := range sizes {
keys = append(keys, key)
}
// This is a slice of the keys.
fmt.Println(keys)
//通过make进行构建
lookup := make(map[string]int, 200)
// Use the new map.
lookup["cat"] = 10
result := lookup["cat"]
fmt.Println(result)
//map作为函数参数
// This map has two string keys.
colors2 := map[string]int{
"blue": 10,
"green": 20,
}
// Pass map to func.
PrintGreen(colors2)
//按key进行排序
// To create a map as input
m := make(map[int]string)
m[1] = "a"
m[2] = "c"
m[0] = "b"
// To store the keys in slice in sorted order
var keys2 []int
for k := range m {
fmt.Println(k) //每次都随机输出key的值
keys2 = append(keys2, k)
}
sort.Ints(keys2)
// To perform the opertion you want
for _, k := range keys2 {
fmt.Println("Key:", k, "Value:", m[k])
}
}
func PrintGreen(colors map[string]int) {
// Handle map argument.
fmt.Println(colors["green"])
}
使用中常见问题
Q:删除掉map中的元素是否会释放内存?
A:不会,删除操作仅仅将对应的tophash[i]设置为empty,并非释放内存。若要释放内存只能等待指针无引用后被系统gc
Q:如何并发地使用map?
A:map不是goroutine安全的,所以在有多个gorountine对map进行写操作是会panic。多gorountine读写map是应加锁(RWMutex),或使用sync.Map(1.9新增,在下篇文章中会介绍这个东西,总之是不太推荐使用)。
Q:map的iterator是否安全?
A:map的delete并非真的delete,所以对迭代器是没有影响的,是安全的。
非原子操作需要加锁, map并发读写需要加锁,map操作不是并发安全的,判断一个操作是否是原子的可以使用 go run race 命令做数据的竞争检测.