映射(Map)
Map(映射)是 Go 语言中用于存储键值对(Key-Value)的内置数据结构。它类似于其他语言中的哈希表、字典(Dictionary)或 HashMap,提供了近乎 O(1) 时间复杂度的查找、插入和删除操作。
定义与初始化
使用 make 创建
make 是创建 map 最常用的方式,它分配并初始化一个哈希表数据结构:
// make(map[KeyType]ValueType)
m := make(map[string]int)
fmt.Println(m) // map[] —— 空的 map
// 预分配空间(提升性能,减少扩容次数)
m2 := make(map[string]int, 100) // 预分配约 100 个键值对的空间💡 提示:make(map[K]V, hint) 中的 hint 并非硬性上限,而是提示运行时预分配的桶(bucket)数量。实际的 map 仍然可以存储超过 hint 个元素。
使用字面量初始化
// 使用字面量直接初始化
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Charlie": 35,
}
fmt.Println(ages) // map[Alice:30 Bob:25 Charlie:35]
// 空的 map(非 nil)
emptyMap := map[string]int{}
fmt.Println(emptyMap == nil) // falsenil map 与空 map
// nil map —— 声明但未初始化
var nilMap map[string]int
fmt.Println(nilMap == nil) // true
fmt.Println(len(nilMap)) // 0 —— len 对 nil map 也安全
// 空 map —— 已初始化但无元素
emptyMap := make(map[string]int)
fmt.Println(emptyMap == nil) // false
fmt.Println(len(emptyMap)) // 0🔒 致命区别:nil map 可以读取(返回零值),但不能写入。向 nil map 写入会导致 panic: assignment to entry in nil map。在使用 map 前务必用 make 初始化。
var m map[string]int
// 读取安全
v := m["key"] // 返回零值 0,不 panic
fmt.Println(len(m)) // 0,不 panic
// 写入 panic
// m["key"] = 1 // panic: assignment to entry in nil map
// 正确做法:先初始化
m = make(map[string]int)
m["key"] = 1 // ✅ 正常基本操作
增加与修改
m := make(map[string]int)
// 添加键值对
m["Go"] = 2009
m["Python"] = 1991
m["Rust"] = 2010
// 修改已有的键(覆盖旧值)
m["Python"] = 2024
fmt.Println(m) // map[Go:2009 Python:2024 Rust:2010]查找(判断 key 是否存在)
在 Go 中,通过 map 查找不存在的 key 时,不会像某些语言那样抛出异常,而是返回 value 类型的零值。因此需要使用 comma ok 惯用法来判断 key 是否真正存在:
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// 普通查找:如果 key 不存在,返回零值
age := m["Alice"]
fmt.Println(age) // 30
age = m["Unknown"]
fmt.Println(age) // 0 —— 无法区分是 "Unknown: 0" 还是 "Unknown 不存在"Go 使用 value, ok := m[key] 的形式来判断 key 是否存在于 map 中。value 是 key 对应的值(不存在时为零值),ok 是布尔值,表示 key 是否存在。
m := map[string]int{
"Alice": 30,
"Bob": 0, // 值恰好为零值
}
// 使用 comma ok 判断 key 是否存在
if age, ok := m["Alice"]; ok {
fmt.Printf("Alice 的年龄是 %d\n", age) // Alice 的年龄是 30
}
if age, ok := m["Unknown"]; ok {
fmt.Printf("Unknown 的年龄是 %d\n", age)
} else {
fmt.Println("Unknown 不存在于 map 中") // Unknown 不存在于 map 中
}
// 区分 "key 存在且值为 0" 和 "key 不存在"
if age, ok := m["Bob"]; ok {
fmt.Printf("Bob 的年龄是 %d(key 存在)\n", age) // Bob 的年龄是 0(key 存在)
}删除
使用内置的 delete 函数删除 map 中的键值对:
m := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 28,
}
delete(m, "Bob")
fmt.Println(m) // map[Alice:30 Carol:28]
// 删除不存在的 key 不会 panic,是安全的操作
delete(m, "Unknown") // 无任何效果长度
使用 len 函数获取 map 中键值对的数量:
m := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(len(m)) // 3
delete(m, "a")
fmt.Println(len(m)) // 2遍历
使用 for range 遍历 map:
m := map[string]int{
"Go": 2009,
"Python": 1991,
"Rust": 2010,
"Java": 1995,
}
// 同时遍历 key 和 value
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// 只遍历 key
for key := range m {
fmt.Println(key)
}
// 只遍历 value
for _, value := range m {
fmt.Println(value)
}⚠️ 无序性:Map 的遍历顺序是不确定的。Go 刻意随机化了遍历顺序,以防止开发者依赖特定顺序。即使在同一程序中多次遍历同一个 map,每次的顺序也可能不同。如果需要有序输出,必须先收集所有 key,排序后再访问。
// 按固定顺序遍历 map
m := map[string]int{
"Go": 2009, "Python": 1991, "Rust": 2010, "Java": 1995,
}
// 收集所有 key 并排序
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
// 按排序后的 key 遍历
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}map 的内部结构
Go 的 map 基于哈希表实现,核心是一个 hmap 结构体,包含桶数组(buckets)、哈希种子、元素数量等信息。每个桶存储 8 个键值对,溢出时使用溢出桶链表。当元素数量达到负载因子阈值(6.5)时,触发扩容。
┌─────────────────────────────────┐
│ hmap │
│ ┌──────────┐ ┌──────────┐ │
│ │ count │ │ flags │ │
│ ├──────────┤ ├──────────┤ │
│ │ B (桶数) │ │ hash0 │ │
│ └──────────┘ └──────────┘ │
│ ┌──────────────────────┐ │
│ │ buckets (桶数组指针) │───┐ │
│ └──────────────────────┘ │ │
└─────────────────────────────│──┘
▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ bucket 0 │ │ bucket 1 │ │ bucket N │ ...
│ ┌─────┐ │ │ │ │ │
│ │k0,v0│ │ │ │ │ │
│ │k1,v1│ │ │ │ │ │
│ │ ... │ │ │ │ │ │
│ │k7,v7│ │ │ │ │ │
│ └─────┘ │ │ │ │ │
│ overflow│ │ │ │ │
└─────────┘ └─────────┘ └─────────┘注意事项
无序性
如前所述,map 不保证遍历顺序。Go 1.12+ 版本会在每次 range 时使用随机起始位置来遍历桶,确保顺序不可预测。
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 两次遍历的顺序可能不同
for k, v := range m {
fmt.Printf("%s=%d ", k, v)
}
fmt.Println()非线程安全
🔒 核心警告:Go 的 map 不是并发安全的。在多个 goroutine 中同时读写同一个 map 会导致 fatal error: concurrent map read and map write。
m := make(map[string]int)
// ❌ 并发写入 —— 会导致 fatal error
// go func() {
// m["a"] = 1
// }()
// go func() {
// m["b"] = 2
// }()解决并发安全问题
方案一:使用 sync.Mutex 加锁
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]int),
}
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.m, key)
}方案二:使用 sync.Map(推荐用于特定场景)
var m sync.Map
// 存储
m.Store("Go", 2009)
m.Store("Python", 1991)
// 读取
if v, ok := m.Load("Go"); ok {
fmt.Println(v) // 2009
}
// 读取或写入(如果 key 不存在,则调用 fn 计算)
v, loaded := m.LoadOrStore("Rust", 2010)
fmt.Println(v, loaded) // 2010 false(首次写入)
v, loaded = m.LoadOrStore("Rust", 9999)
fmt.Println(v, loaded) // 2010 true(key 已存在,返回旧值)
// 删除
m.Delete("Python")
// 遍历
m.Range(func(key, value any) bool {
fmt.Printf("%v: %v\n", key, value)
return true // 返回 false 停止遍历
})sync.Map 针对两种场景进行了优化:
- Key 稳定、读多写少:一旦写入后很少修改,频繁读取
- 多个 goroutine 读写不同的 key:即”分散”的并发访问
对于一般的并发 map 场景,sync.RWMutex + map 通常是更通用、性能更好的选择。
引用类型
map 是引用类型。将 map 赋值给新变量或作为函数参数传递时,不会拷贝底层数据,而是共享同一个哈希表:
m1 := map[string]int{"a": 1, "b": 2}
m2 := m1 // m2 和 m1 指向同一个底层数据
m2["c"] = 3
fmt.Println(m1) // map[a:1 b:2 c:3] —— m1 也被修改了
fmt.Println(m1 == m2) // 编译错误:map 只能与 nil 比较不能对 map 元素取地址
⚠️ 限制:不能对 map 的元素取地址(&m[key])。因为 map 在扩容时可能会重新分配内存,移动元素位置,导致之前的指针失效。
m := map[string]Person{
"Alice": {Name: "Alice", Age: 30},
}
// ❌ 编译错误:cannot take the address of m["Alice"]
// p := &m["Alice"]
// ✅ 正确做法:先取出值到局部变量
person := m["Alice"]
person.Age = 31
m["Alice"] = person
// ✅ 或者使用指针作为 map 的 value
m2 := map[string]*Person{
"Alice": &Person{Name: "Alice", Age: 30},
}
m2["Alice"].Age = 31 // 通过指针修改map 的 key 类型限制
map 的 key 必须是可比较的(comparable),即支持 == 运算符。以下类型不能作为 key:
- 切片(slice)
- 函数(func)
- map 本身
// ✅ 合法的 key 类型
m1 := map[string]int{} // string
m2 := map[int]bool{} // int
m3 := map[float64]string{} // float64
m4 := map[bool]int{} // bool
m5 := map[[3]int]string{} // 数组(元素可比较即可)
// ❌ 非法的 key 类型
// m6 := map[[]int]string{} // 编译错误:slice 作为 key
// m7 := map[map[string]int]int{} // 编译错误:map 作为 key
// m8 := map[func()]int{} // 编译错误:func 作为 key
// ✅ 结构体可以作为 key(只要所有字段都可比较)
type Point struct {
X, Y int
}
m := map[Point]bool{
{1, 2}: true,
{3, 4}: false,
}练习题
练习 1:单词频率统计
编写一个函数,接收一个字符串切片,返回一个 map[string]int,统计每个单词出现的次数。
package main
import (
"fmt"
"strings"
)
func wordCount(words []string) map[string]int {
result := make(map[string]int)
for _, word := range words {
word = strings.ToLower(word)
result[word]++
}
return result
}
func main() {
words := []string{"Go", "go", "Python", "Go", "Rust", "python", "Go"}
counts := wordCount(words)
for word, count := range counts {
fmt.Printf("%s: %d\n", word, count)
}
// 输出(顺序不确定):
// go: 3
// python: 2
// rust: 1
}练习 2:map 合并
编写一个函数,将两个 map[string]int 合并为一个新的 map。如果两个 map 中存在相同的 key,新 map 中该 key 的值应为两个 map 中对应值的和。
package main
import "fmt"
func mergeMaps(a, b map[string]int) map[string]int {
result := make(map[string]int, len(a)+len(b))
// 复制第一个 map
for k, v := range a {
result[k] = v
}
// 合并第二个 map(相同 key 的值相加)
for k, v := range b {
result[k] += v
}
return result
}
func main() {
m1 := map[string]int{"a": 1, "b": 2, "c": 3}
m2 := map[string]int{"b": 10, "c": 20, "d": 30}
merged := mergeMaps(m1, m2)
fmt.Println(merged) // map[a:1 b:12 c:23 d:30]
}练习 3:安全的 map 操作
编写一个并发安全的 map 计数器,使用 sync.Mutex 保护。提供 Increment(key)、Get(key) 和 Count() 三个方法。
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
m map[string]int
}
func NewCounter() *Counter {
return &Counter{m: make(map[string]int)}
}
func (c *Counter) Increment(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[key]++
}
func (c *Counter) Get(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.m[key]
}
func (c *Counter) Count() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.m)
}
func main() {
counter := NewCounter()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
key := fmt.Sprintf("worker-%d", id%5)
counter.Increment(key)
}(i)
}
wg.Wait()
for i := 0; i < 5; i++ {
key := fmt.Sprintf("worker-%d", i)
fmt.Printf("%s: %d\n", key, counter.Get(key))
}
fmt.Println("总 key 数:", counter.Count())
}