跳转至

Go语言Map

Map是Go语言中的一个重要的数据结构,它提供了键值对的存储方式,类似其他语言中的哈希表或字典。

Map的本质:"键值对储物间"

什么是Map?

可以把Map想象成一个特殊的储物间,每个物品(值)都有自己的标签(键)。通过这个标签,我们可以快速找到对应的物品。比如:一个存储学生成绩的系统,学号就是键,分数就是值。

Map的定义方式

在Go语言中,定义Map有几种常用方式:

Go
// 1. 使用make函数创建
scoreMap := make(map[string]int)

// 2. 创建时直接初始化
studentScores := map[string]int{
    "张三": 95,
    "李四": 88,
    "王五": 92,
}

// 3. 声明一个空map
var prices map[string]float64
// 注意:声明后需要通过make初始化才能使用
prices = make(map[string]float64)

Map的基本操作

Go
package main

import "fmt"

func main() {
    // 创建一个存储水果价格的map
    fruitPrices := make(map[string]float64)

    // 添加键值对
    fruitPrices["苹果"] = 5.5
    fruitPrices["香蕉"] = 3.8
    fruitPrices["橙子"] = 4.2

    // 获取值
    applePrice := fruitPrices["苹果"]
    fmt.Printf("苹果的价格是:%.2f元\n", applePrice)

    // 修改值
    fruitPrices["苹果"] = 5.8

    // 删除键值对
    delete(fruitPrices, "香蕉")

    // 遍历map
    for fruit, price := range fruitPrices {
        fmt.Printf("%s的价格是:%.2f元\n", fruit, price)
    }
}
运行结果:
Text Only
1
2
3
苹果的价格是:5.50元
苹果的价格是:5.80元
橙子的价格是:4.20元

判断键是否存在

在Go语言中,访问map中不存在的键会返回该类型的零值。因此,我们需要一种方法来判断键是否真实存在:

Go
package main

import "fmt"

func main() {
    userAge := map[string]int{
        "Alice": 25,
        "Bob":   30,
    }

    // 使用两个变量接收返回值
    age, exists := userAge["Tom"]
    if exists {
        fmt.Printf("Tom的年龄是:%d\n", age)
    } else {
        fmt.Println("Tom不在map中")
    }
}
运行结果:
Text Only
Tom不在map中

Map的进阶用法

1. 嵌套Map

Map的值可以是另一个Map,这样就形成了嵌套结构:

Go
package main

import "fmt"

func main() {
    // 创建一个存储学生成绩的嵌套map
    studentScores := map[string]map[string]int{
        "张三": {
            "数学": 95,
            "英语": 88,
            "语文": 92,
        },
        "李四": {
            "数学": 90,
            "英语": 85,
            "语文": 88,
        },
    }

    // 获取张三的英语成绩
    englishScore := studentScores["张三"]["英语"]
    fmt.Printf("张三的英语成绩是:%d\n", englishScore)
}
运行结果:
Text Only
张三的英语成绩是:88

2. Map作为函数参数

Map是引用类型,作为函数参数时传递的是底层指针的拷贝:

Go
func updatePrice(prices map[string]float64, fruit string, price float64) {
    prices[fruit] = price
}

func main() {
    fruitPrices := map[string]float64{
        "苹果": 5.5,
        "香蕉": 3.8,
    }

    updatePrice(fruitPrices, "苹果", 6.0)
    fmt.Printf("更新后苹果的价格:%.2f\n", fruitPrices["苹果"])
}

使用技巧和注意事项

  1. 初始化容量

    Go
    // 如果知道map大约需要存储多少键值对,可以在创建时指定容量
    userAges := make(map[string]int, 100)
    

  2. 并发安全

    Go
    1
    2
    3
    4
    5
    6
    // map不是并发安全的,需要使用sync.Map或互斥锁来保证并发安全
    import "sync"
    var mutex sync.Mutex
    mutex.Lock()
    map操作
    mutex.Unlock()
    

  3. 常见陷阱

  4. map的零值是nil,需要先初始化才能使用
  5. 不能对map的元素取地址
  6. map的遍历顺序是随机的,不要依赖遍历顺序

1. Map的随机遍历

Go
package main

import "fmt"

func main() {
    // 创建一个简单的map
    m := map[string]int{
        "a": 1,
        "b": 2,
        "c": 3,
        "d": 4,
    }

    // 连续遍历三次,观察输出顺序
    for i := 0; i < 3; i++ {
        fmt.Printf("第%d次遍历:\n", i+1)
        for k, v := range m {
            fmt.Printf("key: %s, value: %d\n", k, v)
        }
        fmt.Println()
    }
}
运行结果:
Text Only
第1次遍历:
key: a, value: 1
key: d, value: 4
key: b, value: 2
key: c, value: 3

第2次遍历:
key: c, value: 3
key: a, value: 1
key: d, value: 4
key: b, value: 2

第3次遍历:
key: b, value: 2
key: c, value: 3
key: d, value: 4
key: a, value: 1
从输出可以看到,每次遍历的顺序都是不同的,这就是map遍历的随机性。

2. Map的并发安全问题

这个部分涉及到并发编程,将在后续章节中详细介绍,这里只做简单介绍,如果不是很理解的话,可以等后面学习了并发编程之后再回来看。

Go
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    // 不安全的map操作
    unsafeMap := make(map[int]int)

    // 使用WaitGroup等待所有goroutine完成
    var wg sync.WaitGroup

    // 启动10个goroutine同时写map
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            unsafeMap[n] = n // 可能导致panic
            time.Sleep(time.Millisecond)
        }(i)
    }

    // 安全的map操作
    var mutex sync.Mutex
    safeMap := make(map[int]int)

    // 再次启动10个goroutine,这次使用互斥锁保护
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            mutex.Lock()
            safeMap[n] = n // 安全的写入
            mutex.Unlock()
            time.Sleep(time.Millisecond)
        }(i)
    }

    wg.Wait()
    fmt.Printf("安全的map最终内容: %v\n", safeMap)
}
运行结果:
Text Only
fatal error: concurrent map writes  // 第一个map可能会panic
安全的map最终内容: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]  // 第二个map安全执行

或者使用sync.Map:

Go
package main

import (
    "fmt"
    "sync"
)

func main() {
    var sm sync.Map
    var wg sync.WaitGroup

    // 启动10个goroutine同时操作sync.Map
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Store(n, n)  // 存储键值对
        }(i)
    }

    wg.Wait()

    // 遍历sync.Map
    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("key: %v, value: %v\n", key, value)
        return true
    })
}
运行结果:
Text Only
key: 5, value: 5
key: 0, value: 0
key: 1, value: 1
key: 2, value: 2
key: 3, value: 3
key: 4, value: 4
key: 6, value: 6
key: 7, value: 7
key: 8, value: 8
key: 9, value: 9

实际应用场景

  1. 缓存系统

    Go
    cache := make(map[string]interface{})
    

  2. 计数器

    Go
    1
    2
    3
    4
    wordCount := make(map[string]int)
    for _, word := range words {
        wordCount[word]++
    }
    

  3. 去重

    Go
    1
    2
    3
    4
    seen := make(map[string]bool)
    for _, item := range items {
        seen[item] = true
    }
    

小结

Map是Go语言中一个强大而灵活的数据结构,它提供了高效的键值对存储方案。在实际开发中,Map的应用非常广泛,从简单的数据存储到复杂的缓存系统都能见到它的身影。掌握Map的使用,对于Go程序开发来说是必不可少的。

记住:合理使用Map可以让我们的代码更加简洁高效,但也要注意并发安全等问题。选择合适的数据结构和使用方式,才能写出更好的程序。