Golang中的map是一种常用的键值对数据结构,在Go的类型体系中被归类为引用类型,这一特性决定了它的使用方式和值类型有本质区别。理解map的引用特性,能帮助开发者避免很多常见的使用错误。

为什么Golang map是引用类型
要理解map的引用类型特性,需要从它的底层实现说起。Go语言中map的底层是一个叫做hmap的结构体指针,当我们声明或者初始化一个map时,得到的实际上是一个指向底层hmap结构体的指针。
值类型的变量在赋值、传参时会拷贝整个变量的内容,而引用类型的变量在赋值、传参时只会拷贝这个指针,所有拷贝的指针都指向同一个底层的hmap结构体。这也是为什么map表现出引用类型的特性:多个变量操作的是同一个底层数据结构。
我们可以通过一段简单的代码验证这个特性:
package main
import "fmt"
func main() {
// 初始化一个map
m1 := make(map[string]int)
m1["a"] = 1
// 将m1赋值给m2
m2 := m1
// 修改m2的内容
m2["a"] = 2
// 打印m1的内容,会发现也被修改了
fmt.Println(m1["a"]) // 输出2
}
Golang map的引用特性表现
1. 赋值传递指向同一底层结构
如上面的示例代码所示,当把map赋值给另一个变量时,两个变量共享同一个底层hmap,修改任意一个变量的键值对,另一个变量对应的内容也会同步变化。
2. 函数传参不会拷贝整个map
当把map作为函数参数传递时,传递的也是底层hmap的指针,函数内部对map的修改会影响到函数外部的map。
package main
import "fmt"
func modifyMap(m map[string]int) {
m["b"] = 3
}
func main() {
m := make(map[string]int)
m["a"] = 1
modifyMap(m)
fmt.Println(m) // 输出map[a:1 b:3]
}
3. 零值为nil的特殊性
map的零值是nil,nil map没有指向任何底层的hmap结构体,不能往nil map中添加键值对,否则会触发panic。
package main
func main() {
var m map[string]int
// 下面这行代码会触发panic: assignment to entry in nil map
m["a"] = 1
}
Golang map使用注意事项
1. 并发读写问题
Go语言的原生map不是并发安全的,多个goroutine同时读写同一个map会触发panic。如果需要在并发场景下使用map,可以选择以下两种方案:
- 使用sync包中的
sync.Mutex或者sync.RWMutex对map的操作加锁 - 使用Go 1.9之后引入的
sync.Map,它是官方提供的并发安全map实现
下面是使用互斥锁保护map的示例:
package main
import (
"fmt"
"sync"
)
func main() {
var m = make(map[string]int)
var mu sync.Mutex
// 启动两个goroutine同时写map
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
mu.Lock()
m["a"]++
mu.Unlock()
}()
go func() {
defer wg.Done()
mu.Lock()
m["a"]++
mu.Unlock()
}()
wg.Wait()
fmt.Println(m["a"]) // 输出2,不会出现并发问题
}
2. 避免对map取地址操作
map中的元素是不可取地址的,因为map在扩容时可能会迁移键值对的位置,元素的地址会发生变化,所以Go语言禁止对map元素取地址。
package main
func main() {
m := make(map[string]int)
m["a"] = 1
// 下面这行代码会编译报错:cannot take the address of m["a"]
// p := &m["a"]
}
3. map的遍历顺序不固定
Go语言中map的遍历顺序是不确定的,每次遍历的结果可能都不一样,不要依赖map的遍历顺序来编写业务逻辑。如果需要固定顺序遍历,可以先将map的键排序,再按照排序后的键遍历。
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{
"c": 3,
"a": 1,
"b": 2,
}
// 先提取所有的键
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
// 对键排序
sort.Strings(keys)
// 按照排序后的键遍历
for _, k := range keys {
fmt.Println(k, m[k])
}
}
4. 判断map中键是否存在
获取map中的键值对时,可以通过第二个返回值判断键是否存在,避免获取到零值误以为是键存在。
package main
import "fmt"
func main() {
m := make(map[string]int)
m["a"] = 0
// 错误判断:键a存在,值是0,但是会误以为不存在
v := m["a"]
if v != 0 {
fmt.Println("键a存在")
}
// 正确判断
v, ok := m["a"]
if ok {
fmt.Println("键a存在,值是", v)
}
}
5. 不要用map作为函数返回值返回局部变量的引用
虽然map是引用类型,但是如果map是在函数内部通过make初始化的局部变量,函数返回后这个map依然可以正常使用,因为底层hmap结构体是在堆上分配的,不会因为函数栈帧销毁而被释放。不过如果返回的是nil map,需要注意外部使用前要先初始化。
package main
import "fmt"
func getMap() map[string]int {
m := make(map[string]int)
m["a"] = 1
return m
}
func main() {
m := getMap()
fmt.Println(m) // 输出map[a:1],可以正常使用
}