跳到主要内容

Map(映射)

映射(map)保存键到值的关联,类型写作 map[K]V。它适合按唯一键快速查找数据,但不保证遍历顺序,也不提供并发读写同步。

创建映射

可以使用映射字面量创建并初始化数据:

package main

import "fmt"

func main() {
scores := map[string]int{
"Ada": 95,
"Linus": 90,
}

fmt.Println(scores["Ada"])
}

需要逐步写入时,使用 make 创建非 nil 映射:

scores := make(map[string]int)
scores["Rob"] = 88

make(map[K]V, size) 可以提供初始容量提示,但 size 不是固定长度或上限。映射的实际存储和扩容方式属于实现细节。

键必须可比较

键类型必须支持 ==!=。布尔值、数值、字符串、指针、通道、接口,以及成员均可比较的数组和结构体,都可以作为键。切片、映射和函数不能作为映射键。

type Coordinate struct {
X int
Y int
}

visited := map[Coordinate]bool{
{X: 2, Y: 3}: true,
}

接口类型可以声明为键类型,但写入接口的动态值仍必须可比较;否则在运行时执行映射操作会触发 panic。业务代码通常应优先选择约束更明确的具体键类型。

查询键是否存在

读取不存在的键会得到值类型的零值:

scores := map[string]int{"Ada": 0}
fmt.Println(scores["Ada"]) // 0
fmt.Println(scores["Unknown"]) // 0

仅看返回值无法区分“键存在且值为零”和“键不存在”。使用双赋值形式获取存在标志:

score, exists := scores["Ada"]
if !exists {
fmt.Println("score not found")
} else {
fmt.Println(score)
}

只判断存在性时可以忽略值:

if _, exists := scores["Ada"]; exists {
fmt.Println("Ada exists")
}

添加、更新与删除

对键赋值即可添加或更新元素:

scores["Ada"] = 98
scores["Grace"] = 96

内置 delete 删除指定键。键不存在时,delete 不执行任何操作:

delete(scores, "Grace")

Go 1.21 起,内置 clear 删除映射中的所有条目,但保留映射值本身以供继续使用:

clear(scores)

映射没有 cap 操作。len(scores) 只返回当前条目数量。

nil 映射

映射的零值是 nil

var scores map[string]int

可以安全地读取、查询、遍历和删除 nil 映射;其长度为 0。但向 nil 映射写入会触发 panic:

scores["Ada"] = 95 // panic: assignment to entry in nil map

写入前应使用字面量或 make 初始化。是否保留 nil 与空映射的区别取决于接口契约,因为序列化等边界可能以不同形式表示它们。

遍历映射

使用 range 遍历键和值:

for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}

Go 不规定映射的遍历顺序,同一个映射在不同轮遍历中也可能出现不同顺序。测试输出、生成文件或签名计算需要稳定顺序时,应先收集键并排序:

package main

import (
"fmt"
"sort"
)

func main() {
scores := map[string]int{"Linus": 90, "Ada": 95}
names := make([]string, 0, len(scores))
for name := range scores {
names = append(names, name)
}
sort.Strings(names)

for _, name := range names {
fmt.Printf("%s: %d\n", name, scores[name])
}
}

赋值与复制

把映射赋给另一个变量不会复制条目。两个变量会引用同一份映射数据:

original := map[string]int{"Ada": 95}
shared := original
shared["Ada"] = 100

fmt.Println(original["Ada"]) // 100

需要独立映射时,应显式复制每个条目:

cloned := make(map[string]int, len(original))
for key, value := range original {
cloned[key] = value
}

这仍是浅复制。如果值包含切片、映射或指针,两个映射中的对应值仍可能共享更深层的数据。

映射值不可寻址

映射可能在增长时移动内部数据,因此通过索引表达式取得的映射元素不可寻址。不能直接修改存储在映射中的结构体字段:

type Profile struct {
Visits int
}

profiles := map[string]Profile{"Ada": {Visits: 1}}
// profiles["Ada"].Visits++ // 编译错误

可以取出值、修改后再写回:

profile := profiles["Ada"]
profile.Visits++
profiles["Ada"] = profile

也可以存储结构体指针,但这会引入共享可变状态和 nil 指针边界,需要根据数据所有权选择,而不是只为缩短代码。

对于数值映射,counts[key]++ 是语言直接支持的更新形式:

counts := make(map[string]int)
counts["go"]++

并发访问

普通映射不支持读写并发安全。若至少一个 goroutine 写入,同时其他 goroutine 读或写同一映射,程序存在数据竞态,还可能因并发映射操作而终止。

常见解决方式包括:

  • 使用 sync.Mutexsync.RWMutex 保护映射及相关状态。
  • 让单个 goroutine 拥有映射,通过通道传递操作请求。
  • 在符合读多写少、键稳定等特定场景时评估 sync.Map,而不是把它当作普通映射的默认替代。

并发测试应运行 go test -race。只有在映射完成初始化且之后没有任何 goroutine 修改时,多个 goroutine 才可以直接并发读取。

与 TypeScript/JavaScript 的关键差异

Go 的 map 与 JavaScript 的 Map 或普通对象都不等价。Go 在编译期确定键和值类型,键必须可比较,读取缺失键返回值类型的零值,并通过第二个返回值判断是否存在;遍历顺序也没有 JavaScript Map 的插入顺序保证。

互斥锁、所有权划分和竞态验证的完整说明见 Sync(同步原语)

小结

  • 映射键必须可比较,值可以是任意允许的类型。
  • 读取缺失键返回零值,需要用 value, ok 区分键是否存在。
  • nil 映射可读但不可写;写入前必须初始化。
  • 映射赋值会共享数据,遍历顺序未作规定。
  • 映射元素不可寻址,普通映射也不支持无同步的并发读写。