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.Mutex或sync.RWMutex保护映射及相关状态。 - 让单个 goroutine 拥有映射,通过通道传递操作请求。
- 在符合读多写少、键稳定等特定场景时评估
sync.Map,而不是把它当作普通映射的默认替代。
并发测试应运行 go test -race。只有在映射完成初始化且之后没有任何 goroutine 修改时,多个 goroutine 才可以直接并发读取。
与 TypeScript/JavaScript 的关键差异
Go 的 map 与 JavaScript 的 Map 或普通对象都不等价。Go 在编译期确定键和值类型,键必须可比较,读取缺失键返回值类型的零值,并通过第二个返回值判断是否存在;遍历顺序也没有 JavaScript Map 的插入顺序保证。
互斥锁、所有权划分和竞态验证的完整说明见 Sync(同步原语)。
小结
- 映射键必须可比较,值可以是任意允许的类型。
- 读取缺失键返回零值,需要用
value, ok区分键是否存在。 nil映射可读但不可写;写入前必须初始化。- 映射赋值会共享数据,遍历顺序未作规定。
- 映射元素不可寻址,普通映射也不支持无同步的并发读写。