映射是一种数据结构,用于存储一系列无序的键值对。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。
//使用make创建
ages := make(map[string]int) // mapping from strings to ints
//使用字面量创建
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
delete(ages, "alice") // remove element ages["alice"]
map在初始化完的零值是nil,也就是没有引用任何哈希表。
var ages map[string]int
fmt.Println(ages == nil) // "true"
fmt.Println(len(ages) == 0) // "true"
map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常:
var ages map[string]int
fmt.Println(ages["he"]) // 0
delete(ages, "1") // no error
fmt.Println(len(ages)) // 0
for _, v := range ages { // no error
fmt.Println(v)
}
ages["me"] = 1 //panic: assignment to entry in nil map
当使用一个不存在的key去获取对应的value时,会返回一个value类型的零值。有时需要知道元素是否真的在map中,例如,如果元素类型是一个数字,你可能需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:
age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }
在这种场景下,map的下标语法将产生两个值;第二个是一个布尔值,用于报告元素是否真的存在。布尔变量一般命名为ok,特别适合马上用于if条件判断部分。
和slice一样,map是不能直接进行比较的,唯一的例外是和nil比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
从例子中可以看到如何用!ok来区分元素不存在,与元素存在但为0的。我们不能简单地用xv!=y[k]判断,那样会导致在判断下面两个map时产生错误的结果:
// 如果去掉!ok,那么下面的函数调用但会的时true
equal(map[string]int{"A": 0}, map[string]int{"B": 42})
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。下面是常见的处理方式:
import "sort"
var names []string
for name := range ages {
names = append(names, name)
}
sort.Strings(names)
// 这里使用_来忽略接受key的值
for _, name := range names {
fmt.Printf("%s\t%d\n", name, ages[name])
}
只能使用支持==比较运算符的数据类型作为map中的key,所以map可以通过比较key是否相等来判断是否存在。
虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,最坏的情况是可能出现的NaN和任何浮点数都不相等。map中的value是没有任何限制的。
有时候需要一个map或者set的key是slice类型,这并不满足map的key是支持等于运算符。我们可以绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x)==k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。
下面的例子演示了如何使用map来记录提交相同的字符串列表的次数。它使用了fmt.Sprintf函数将字符串列表转换为一个字符串以用于map的key,通过%q参数忠实地记录每个字符串元素的信息:
var m = make(map[string]int)
func k(list []string) string { return fmt.Sprintf("%q", list) }
func Add(list []string) { m[k(list)]++ }
func Count(list []string) int { return m[k(list)] }
使用同样的技术可以处理任何不可比较的key类型,而不仅仅是slice类型。这种技术对于想使用自定义key比较函数的时候也很有用,例如在比较字符串的时候忽略大小写。同时,辅助函数k(x)也不一定是字符串类型,它可以返回任何可比较的类型,例如整数、数组或结构体等。
能够直接作为key的类型有string、int、数组、结构体、指针、接口
对map中的元素取地址会报错,如下:
m := make(map[string]string)
m["key"] = "value"
fmt.Println(&m["key"])
// output:
// cannot take the address of m["key"]
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。例如:
func main() {
seen := make(map[string]bool) // a set of strings
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
line := input.Text()
if !seen[line] {
seen[line] = true
fmt.Println(line)
}
}
if err := input.Err(); err != nil {
fmt.Fprintf(os.Stderr, "dedup: %v\n", err)
os.Exit(1)
}
}
map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。
和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]valuetype, cap)。例如:
map2 := make(map[string]float32, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。
map并不是一个线程安全的数据结构,同时读写一个 map 是不安全的,如果被检测到,会直接 panic。
解决方法1:读写锁 sync.RWMutex。
type TestMap struct {
M map[int]string
Lock sync.RWMutex
}
func main() {
testMap := TestMap{
}
testMap.M = map[int]string{
1: "lili"}
go func() {
i := 0
for i < 10000 {
testMap.Lock.RLock()
fmt.Println(i, testMap.M[1])
testMap.Lock.RUnlock()
i++
}
}()
go func() {
i := 0
for i < 10000 {
testMap.Lock.Lock()
testMap.M[1] = "lily"
testMap.Lock.Unlock()
i++
}
}()
for {
runtime.GC()
}
}
解决方法2:使用golang提供的 sync.Map
func