映射表

寒江蓑笠翁大约 10 分钟

映射表

一般来说,映射表数据结构实现通常有两种,哈希表(hash table)和搜索树(search tree),区别在于前者无序,后者有序。在Go中,map的实现是基于哈希桶(也是一种哈希表),所以也是无序的,本篇不会对实现原理做过多的讲解,这超出了基础的范围,后续会进行深入分析。

提示

想要了解map的原理可以前往map实现


初始化

在Go中,map的键类型必须是可比较的,比如string int是可比较的,而[]int是不可比较的,也就无法作为map的键。初始化一个map有两种方法,第一种是字面量,格式如下:

map[keyType]valueType{}

举几个例子

mp := map[int]string{
   0: "a",
   1: "a",
   2: "a",
   3: "a",
   4: "a",
}

mp := map[string]int{
   "a": 0,
   "b": 22,
   "c": 33,
}

第二种方法是使用内置函数make,对于map而言,接收两个参数,分别是类型与初始容量,例子如下:

mp := make(map[string]int, 8)

mp := make(map[string][]int, 10)

map是引用类型,零值或未初始化的map可以访问,但是无法存放元素,所以必须要为其分配内存。

func main() {
   var mp map[string]int
   mp["a"] = 1
   fmt.Println(mp)
}
panic: assignment to entry in nil map   

提示

在初始化map时应当尽量分配一个合理的容量,以减少扩容次数。


访问

访问一个map的方式就像通过索引访问一个数组一样。

func main() {
	mp := map[string]int{
		"a": 0,
		"b": 1,
		"c": 2,
		"d": 3,
	}
	fmt.Println(mp["a"])
	fmt.Println(mp["b"])
	fmt.Println(mp["d"])
	fmt.Println(mp["f"])
}
0
1
3
0

通过代码可以观察到,即使map中不存在"f"这一键值对,但依旧有返回值。map对于不存的键其返回值是对应类型的零值,并且在访问map的时候其实有两个返回值,第一个返回值对应类型的值,第二个返回值一个布尔值,代表键是否存在,例如:

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   if val, exist := mp["f"]; exist {
      fmt.Println(val)
   } else {
      fmt.Println("key不存在")
   }
}

对map求长度

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(len(mp))
}

存值

map存值的方式也类似数组存值一样,例如:

func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   fmt.Println(mp)
}

存值时使用已存在的键会覆盖原有的值

func main() {
   mp := make(map[string]int, 10)
   mp["a"] = 1
   mp["b"] = 2
   if _, exist := mp["b"]; exist {
      mp["b"] = 3
   }
   fmt.Println(mp)
}

但是也存在一个特殊情况,那就是键为math.NaN()

func main() {
	mp := make(map[float64]string, 10)
	mp[math.NaN()] = "a"
	mp[math.NaN()] = "b"
	mp[math.NaN()] = "c"
	_, exist := mp[math.NaN()]
	fmt.Println(exist)
	fmt.Println(mp)
}
false
map[NaN:c NaN:a NaN:b]

通过结果可以观察到相同的键值并没有覆盖,反而还可以存在多个,也无法判断其是否存在,也就无法正常取值。因为NaN是IEE754标准所定义的,其实现是由底层的汇编指令UCOMISD完成,这是一个无序比较双精度浮点数的指令,该指令会考虑到NaN的情况,因此结果就是任何数字都不等于NaN,NaN也不等于自身,这也造成了每次哈希值都不相同。关于这一点社区也曾激烈讨论过,但是官方认为没有必要去修改,所以应当尽量避免使用NaN作为map的键。


删除

func delete(m map[Type]Type1, key Type)

删除一个键值对需要用到内置函数delete,例如

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   fmt.Println(mp)
   delete(mp, "a")
   fmt.Println(mp)
}
map[a:0 b:1 c:2 d:3]
map[b:1 c:2 d:3]

需要注意的是,如果值为NaN,甚至没法删除该键值对。

func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   fmt.Println(mp)
   delete(mp, math.NaN())
   fmt.Println(mp)
}
map[NaN:c NaN:a NaN:b]
map[NaN:c NaN:a NaN:b]

遍历

通过for range可以遍历map,例如

func main() {
   mp := map[string]int{
      "a": 0,
      "b": 1,
      "c": 2,
      "d": 3,
   }
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
c 2
d 3
a 0
b 1

可以看到结果并不是有序的,也印证了map是无序存储。值得一提的是,NaN虽然没法正常获取,但是可以通过遍历访问到,例如

func main() {
   mp := make(map[float64]string, 10)
   mp[math.NaN()] = "a"
   mp[math.NaN()] = "b"
   mp[math.NaN()] = "c"
   for key, val := range mp {
      fmt.Println(key, val)
   }
}
NaN a
NaN c
NaN b

清空

在go1.21之前,想要清空map,就只能对每一个map的key进行delete

func main() {
	m := map[string]int{
		"a": 1,
		"b": 2,
	}
	for k, _ := range m {
		delete(m, k)
	}
	fmt.Println(m)
}

但是go1.21更新了clear函数,就不用再进行之前的操作了,只需要一个clear就可以清空

func main() {
	m := map[string]int{
		"a": 1,
		"b": 2,
	}
	clear(m)
	fmt.Println(m)
}

输出

map[]

Set

Set是一种无序的,不包含重复元素的集合,Go中并没有提供类似的数据结构实现,但是map的键正是无序且不能重复的,所以也可以使用map来替代set。

func main() {
	set := make(map[int]struct{}, 10)
	for i := 0; i < 10; i++ {
		set[rand.Intn(100)] = struct{}{}
	}
	fmt.Println(set)
}
map[0:{} 18:{} 25:{} 40:{} 47:{} 56:{} 59:{} 81:{} 87:{}]

提示

一个空的结构体不会占用内存

注意

map并不是一个并发安全的数据结构,Go团队认为大多数情况下map的使用并不涉及高并发的场景,引入互斥锁会极大的降低性能,map内部有读写检测机制,如果冲突会触发fatal error。例如下列情况有非常大的可能性会触发fatal

func main() {

   group.Add(10)
   // map
   mp := make(map[string]int, 10)
   for i := 0; i < 10; i++ {
      go func() {
         // 写操作
         for i := 0; i < 100; i++ {
            mp["helloworld"] = 1
         }
         // 读操作
         for i := 0; i < 10; i++ {
            fmt.Println(mp["helloworld"])
         }
         group.Done()
      }()
   }
   group.Wait()
}
fatal error: concurrent map writes

在这种情况下,需要使用sync.Map来替代。