Go语言映射(map)

映射(map)是Go语言内置的哈希表数据结构,用于存储键值对(key-value)。键必须是可比较的类型(如整数、字符串、浮点数、结构体等),值可以是任意类型。map提供O(1)平均时间复杂度的存取,在实际开发中使用极为广泛。

核心特性:map是引用类型,零值为nil;对nil map进行读取不会报错,但写入会引发panic;使用make或字面量创建后才能写入。

1. 声明与初始化

1.1 声明nil map

var m map[string]int
fmt.Println(m == nil)  // true
// m["key"] = 1        // panic: assignment to entry in nil map

1.2 使用make函数创建

// 不指定容量(自动扩容)
m1 := make(map[string]int)

// 指定初始容量(提高性能)
m2 := make(map[string]int, 100)

m1["age"] = 30
fmt.Println(m1["age"])

1.3 字面量创建

// 空map
m3 := map[string]int{}

// 带初始值
m4 := map[string]int{
    "apple":  5,
    "pear":   3,
    "banana": 2,
}
fmt.Println(m4["apple"]) // 5

2. 基本操作(增删改查)

2.1 插入 / 更新键值对

scores := map[string]int{}
scores["Alice"] = 95   // 插入
scores["Bob"] = 87
scores["Alice"] = 98   // 更新

2.2 读取键的值

fmt.Println(scores["Alice"])     // 98
// 不存在的键返回零值(不会panic)
fmt.Println(scores["Charlie"])   // 0

2.3 判断键是否存在(推荐写法)

value, ok := scores["Charlie"]
if ok {
    fmt.Println("存在,值为", value)
} else {
    fmt.Println("键不存在")
}

2.4 删除键值对(内置delete)

delete(scores, "Bob")   // 删除Bob
delete(scores, "Unknown") // 删除不存在的键不会报错

2.5 获取map长度

length := len(scores)
fmt.Println("当前map大小:", length)

3. 遍历map(for range)

使用for range遍历map,返回键和值。注意:遍历顺序是随机的(Go有意设计),不保证与插入顺序一致。

遍历示例
ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 28,
}
for name, age := range ages {
    fmt.Printf("%s 年龄 %d\n", name, age)
}

// 只遍历键
for name := range ages {
    fmt.Println(name)
}

// 只遍历值
for _, age := range ages {
    fmt.Println(age)
}

4. map是引用类型

map作为函数参数传递时,传递的是指向底层数据结构的指针,函数内部对map的修改会影响外部。

func modify(m map[string]int) {
    m["new"] = 100
}
func main() {
    data := map[string]int{"old": 1}
    modify(data)
    fmt.Println(data) // map[new:100 old:1]
}

5. 键的约束与复杂值类型

键类型必须支持==和!=操作。不能使用切片、map、函数作为键(这些类型不可比较)。但可以使用结构体、数组等作为键。

// 结构体作为键
type Point struct {
    X, Y int
}
points := map[Point]string{
    {0, 0}: "origin",
    {1, 0}: "right",
}
fmt.Println(points[Point{1,0}]) // "right"

// 值可以是任意类型,包括切片、map等
var nested = map[string][]int{
    "primes": {2,3,5,7},
    "evens":  {2,4,6,8},
}

6. 并发读写与sync.Map

Go原生的map不支持并发读写(会导致fatal error)。需要并发安全时,可以使用sync.Mutex保护map,或使用sync.Map(适用于读多写少或键值生命周期特殊的场景)。

使用sync.Map
import "sync"

func main() {
    var sm sync.Map
    // 存储
    sm.Store("key1", 100)
    // 读取
    if val, ok := sm.Load("key1"); ok {
        fmt.Println(val.(int)) // 类型断言
    }
    // 删除
    sm.Delete("key1")
    // 遍历
    sm.Range(func(k, v interface{}) bool {
        fmt.Println(k, v)
        return true // 继续遍历
    })
}
注意:普通map需要加锁(如sync.RWMutex)才能在多个goroutine中安全使用。推荐使用sync.Map或自己加Mutex。

7. 容量与性能建议

使用make(map[K]V, hint)预先分配容量可减少扩容次数,提高性能。当预先知道map可能存储的元素数量时,建议指定初始容量。

// 预计存储100个元素
m := make(map[string]int, 100)

综合示例:统计单词频率

使用map实现词频统计
package main

import (
    "fmt"
    "strings"
)

func main() {
    text := "go is a great language. go is simple and efficient."
    words := strings.Fields(strings.ToLower(text))
    freq := make(map[string]int)

    for _, w := range words {
        // 去除标点(简单示例)
        w = strings.Trim(w, ".,!?;")
        freq[w]++
    }

    // 输出统计结果
    for word, count := range freq {
        fmt.Printf("%s: %d\n", word, count)
    }
}
go: 2
is: 2
a: 1
great: 1
language: 1
simple: 1
and: 1
efficient: 1
小结:map是Go语言最常用的容器之一。掌握声明、初始化、增删改查和遍历操作后,可以解决大部分关联数据存储问题。注意map的引用特性和并发限制,在实际生产环境中避免竞态条件。下一章我们将学习函数的高级特性,包括多返回值、闭包和defer。