Appearance
06|映射
映射(map)是无序的键值对集合,查找接近常数时间。运维和后端开发里,映射常用于存储主机标签、HTTP 请求头、配置项、路由表这类键值对数据。映射是引用类型,使用前要初始化,读写不存在的键时返回零值而不是报错。并发场景下,普通映射不是线程安全的,需要额外保护。
一、声明与初始化
映射用 make 初始化:
go
package main
import "fmt"
func main() {
labels := make(map[string]string)
labels["env"] = "prod"
labels["team"] = "ops"
fmt.Println(labels["env"]) // prod
}声明时直接初始化:
go
labels := map[string]string{
"env": "prod",
"team": "ops",
}键类型必须是可比较的类型(== 和 != 能工作的类型),比如 string、int 和各种指针。切片、映射和函数不能作为键,因为它们的值不能直接比较。
二、读取与判断存在性
读取不存在的键时,返回该值类型的零值,不会报错:
go
value := labels["region"] // "region" 不存在,返回 ""这个行为有隐患:空字符串可能是键不存在,也可能是键存在但值就是空字符串。用"逗号 ok"语法区分:
go
value, ok := labels["region"]
if !ok {
fmt.Println("key not found")
} else {
fmt.Println("value:", value)
}ok 为 true 表示键存在,false 表示键不存在。遍历映射、删除键、判断存在性时都应该用这个模式。
如果只需要判断键是否存在,不关心值,用空标识符:
go
if _, ok := labels["region"]; !ok {
fmt.Println("region not set")
}三、删除
删除键用 delete:
go
delete(labels, "team")delete 对不存在的键执行也不会报错,是安全操作。删除后键值对立即从映射中移除,但底层内存不一定立即释放——Go 的运行时会延迟回收,映射重新变小时才会缩容。
四、遍历
用 range 遍历映射:
go
labels := map[string]string{
"env": "prod",
"team": "ops",
}
for key, value := range labels {
fmt.Println(key, value)
}range 遍历映射的顺序不固定,每次运行可能不同。如果需要固定顺序,先取出所有键排序后再遍历:
go keys
for k := range labels {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, labels[k])
}五、nil 映射
映射是引用类型,未初始化的映射是 nil。nil 映射可以读取(返回零值),但不能写入:
go
var labels map[string]string // nil 映射
value := labels["env"] // 不会 panic,返回 ""
labels["env"] = "prod" // panic: assignment to entry in nil map使用映射前必须确保它已经被初始化。函数返回映射时,返回 nil 表示"没有数据",调用方读取不会 panic,但写入会。
六、并发安全
普通映射不是并发安全的。多个 goroutine 同时读写同一个映射会触发 fatal error: concurrent map writes:
go
var counts = make(map[string]int)
for i := 0; i < 10; i++ {
go func() {
counts["a"]++ // 危险:并发写
}()
}并发场景下保护映射的方式:
- 用
sync.Mutex或sync.RWMutex加锁保护读写 - 用
sync.Map(特定并发读写场景下性能更好,但 API 和普通映射不同) - 每个 goroutine 操作自己的映射,最后合并结果
sync.Map 适合读多写少、键的类型不一致的场景。普通业务逻辑里,Mutex + 普通映射更直观。
七、映射作为函数参数
映射是引用类型,函数内部修改会影响原映射:
go
func addLabel(m map[string]string) {
m["region"] = "bj"
}
func main() {
labels := map[string]string{"env": "prod"}
addLabel(labels)
fmt.Println(labels["region"]) // bj
}如果需要函数内部的修改不影响原映射,先 copy 一份:
go
func processLabels(original map[string]string) map[string]string {
copy := make(map[string]string, len(original))
for k, v := range original {
copy[k] = v
}
// 修改 copy,不影响 original
return copy
}八、常见错误
nil 映射写入
go
var labels map[string]string
labels["env"] = "prod" // panic必须先用 make 初始化。
遍历时修改映射
遍历映射时删除当前键是安全的,但不能新增键——新增键可能导致遍历顺序变化,某些键被跳过或重复访问:
go
for k := range labels {
delete(labels, k) // 安全
labels["new"] = "v" // 不安全,遍历行为不确定
}需要遍历时修改,先收集所有键再处理。
用映射的值类型判断存在性
go
if labels["env"] == "" {
fmt.Println("env not set")
}如果 "env" 的值恰好是空字符串,上面的判断会误判为键不存在。始终用"逗号 ok"语法判断存在性。
并发读写的竞争
即使只是并发读和写(不是并发写),也可能触发 concurrent map iteration and map write。映射在遍历过程中如果被其他 goroutine 写入,会 panic。遍历前加读锁,或遍历前先 copy 一份数据。