Go 语言 Map
Map(映射)是 Go 语言中重要的引用类型,用于存储无序的键值对集合。Map 通过 key 快速查找 value,查找时间复杂度为 O(1)。在日常开发中,map 被广泛用于数据索引、计数统计、对象缓存等场景。
Map 的创建
使用 make 创建
package main
import "fmt"
func main() {
// 创建一个空的 map,key 为 string,value 为 int
m1 := make(map[string]int)
fmt.Println(m1) // map[]
fmt.Printf("类型: %T\n", m1) // map[string]int
// 创建时指定初始容量
m2 := make(map[string]int, 100)
_ = m2
}make(map[K]V, hint) 的第二个参数是容量提示,预分配足够的桶(bucket)数量以减少扩容。
使用字面量创建
package main
import "fmt"
func main() {
// 字面量初始化
m := map[string]int{
"Go": 1,
"Java": 2,
"Python": 3,
}
fmt.Println(m) // map[Go:1 Java:2 Python:3]
fmt.Printf("长度: %d\n", len(m)) // 3
}注意:声明但未初始化的 map 值为 nil,对 nil map 读取不会 panic(返回零值),但写入会 panic。使用前必须用 make 或字面量初始化。
增删改查操作
package main
import "fmt"
func main() {
m := make(map[string]int)
// 增:添加键值对
m["Go"] = 1
m["Java"] = 2
m["Python"] = 3
fmt.Println(m) // map[Go:1 Java:2 Python:3]
// 查:通过 key 获取 value
fmt.Println(m["Go"]) // 1
// 改:修改已存在的 key 对应的值
m["Go"] = 10
fmt.Println(m["Go"]) // 10
// 删:使用 delete 函数删除键值对
delete(m, "Java")
fmt.Println(m) // map[Go:10 Python:3]
}判断 key 是否存在(comma ok 模式)
直接通过 key 访问 map 时,如果 key 不存在,会返回 value 类型的零值。但有时我们需要区分"key 不存在"和"value 恰好是零值"的情况。
package main
import "fmt"
func main() {
m := map[string]int{
"Go": 1,
"Python": 0, // value 为 0
}
// comma ok 模式
val, ok := m["Go"]
if ok {
fmt.Printf("Go 存在,值为 %d\n", val) // Go 存在,值为 1
}
val, ok = m["Python"]
if ok {
fmt.Printf("Python 存在,值为 %d\n", val) // Python 存在,值为 0
}
val, ok = m["Java"]
if !ok {
fmt.Println("Java 不存在") // Java 不存在
} else {
fmt.Printf("Java 的值为 %d\n", val)
}
// 只判断是否存在,不关心值
if _, ok := m["Rust"]; !ok {
fmt.Println("Rust 不在 map 中")
}
}Map 遍历
Map 的遍历使用 for-range,但遍历顺序是随机的,每次运行结果可能不同。
package main
import "fmt"
func main() {
m := map[string]int{
"Go": 1,
"Java": 2,
"Python": 3,
"Rust": 4,
}
// 遍历 map
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
}如果需要有序遍历,可以先提取 key 并排序:
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{
"Go": 1,
"Java": 2,
"Python": 3,
"Rust": 4,
}
// 提取所有 key
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
// 排序 key
sort.Strings(keys)
// 按排序后的顺序遍历
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
}Map 的 key 类型限制
Map 的 key 必须是可比较的类型(即支持 == 运算符)。
合法的 key 类型:
- 基本类型:
string、int、float64、bool等 - 指针类型
- 结构体(所有字段都可比较时)
- 数组(元素类型可比较时)
非法的 key 类型:
- 切片(slice)
- Map
- 函数(func)
package main
import "fmt"
func main() {
// 合法:使用结构体作为 key
type Point struct {
X, Y int
}
m := map[Point]string{
{1, 2}: "A",
{3, 4}: "B",
}
fmt.Println(m[Point{1, 2}]) // A
// 非法:使用切片作为 key(编译错误)
// m2 := map[[]string]int{} // invalid map key type
}Map 是引用类型
Map 是引用类型,赋值和传参时不会拷贝底层数据。
package main
import "fmt"
func addEntry(m map[string]int) {
m["new"] = 100 // 修改会影响原始 map
}
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := m1 // m1 和 m2 指向同一个底层数据结构
m2["c"] = 3
fmt.Println(m1) // map[a:1 b:2 c:3] —— m1 也被修改了
addEntry(m1)
fmt.Println(m1) // map[a:1 b:2 c:3 new:100]
}如果需要独立的副本,需要手动遍历复制:
m2 := make(map[string]int, len(m1))
for k, v := range m1 {
m2[k] = v
}删除元素
使用内置函数 delete(m, key) 删除 map 中的键值对。
package main
import "fmt"
func main() {
m := map[string]int{
"Go": 1,
"Java": 2,
"Python": 3,
}
// 删除指定的 key
delete(m, "Java")
fmt.Println(m) // map[Go:1 Python:3]
// 删除不存在的 key,不会 panic,什么也不做
delete(m, "Rust")
fmt.Println(m) // map[Go:1 Python:3]
// 删除 nil map 会 panic
// var m2 map[string]int
// delete(m2, "key") // 不会 panic,对 nil map 的 delete 是安全的
}注意:delete 对 nil map 操作是安全的,不会 panic。
并发访问 Map 的问题
Go 的 map 不是并发程安全的。多个 goroutine 同时读写 map 会导致 fatal error: concurrent map writes。
package main
import (
"fmt"
"sync"
)
func main() {
m := make(map[int]int)
var wg sync.WaitGroup
// 这段代码运行时可能会 panic
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m[n] = n * n // 并发写入,不安全!
}(i)
}
wg.Wait()
fmt.Println(len(m))
}运行上述代码,大概率会看到 fatal error: concurrent map writes。
解决方案:使用 sync.Mutex
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
m map[int]int
}
func (s *SafeMap) Set(key, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}
func (s *SafeMap) Get(key int) (int, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.m[key]
return val, ok
}
func main() {
sm := &SafeMap{m: make(map[int]int)}
sm.Set(1, 100)
sm.Set(2, 200)
val, ok := sm.Get(1)
fmt.Printf("key=1: val=%d, exists=%v\n", val, ok) // key=1: val=100, exists=true
}sync.Map 简介
Go 1.9 引入了 sync.Map,它是并发安全的 map 实现,适用于读多写少、多个 goroutine 读写不相交 key 的场景。
package main
import (
"fmt"
"sync"
)
func main() {
var sm sync.Map
// 存储
sm.Store("Go", 1)
sm.Store("Java", 2)
// 读取
val, ok := sm.Load("Go")
if ok {
fmt.Printf("Go: %v\n", val) // Go: 1
}
// LoadOrStore:如果 key 存在则返回已有值,否则存储新值
actual, loaded := sm.LoadOrStore("Go", 100)
fmt.Printf("值: %v, 已存在: %v\n", actual, loaded) // 值: 1, 已存在: true
// 删除
sm.Delete("Java")
// 遍历
sm.Range(func(key, value interface{}) bool {
fmt.Printf("%v: %v\n", key, value)
return true
})
}sync.Map 与内置 map 的选择:
- 一般场景优先使用内置 map +
sync.RWMutex,更灵活 - 读多写少的并发场景使用
sync.Map,性能更好
总结
本篇介绍了 Go 语言 Map 的核心知识:map 的创建方式(make、字面量)、增删改查操作、comma ok 模式判断 key 是否存在、map 的无序遍历特性及有序遍历方法、key 类型限制、引用类型特性、delete 函数、并发访问的安全问题以及 sync.Map 的使用场景。Map 是 Go 中最常用的数据结构之一,理解其特性和限制对于编写正确、高效的 Go 程序至关重要。