Files
cs-note/hzh/GolangStar/Go语言基础/Go语言Map.md
T

356 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- Go
- golang
- go基础语法
- map
- 哈希表
---
# 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)
}
}
```
运行结果:
```
苹果的价格是: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中")
}
}
```
运行结果:
```
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)
}
```
运行结果:
```
张三的英语成绩是: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
// map不是并发安全的,需要使用sync.Map或互斥锁来保证并发安全
import "sync"
var mutex sync.Mutex
mutex.Lock()
map操作
mutex.Unlock()
```
3. 常见陷阱
- map的零值是nil,需要先初始化才能使用
- 不能对map的元素取地址
- 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()
}
}
```
运行结果:
```
第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)
}
```
运行结果:
```
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
})
}
```
运行结果:
```
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
wordCount := make(map[string]int)
for _, word := range words {
wordCount[word]++
}
```
3. 去重
```go
seen := make(map[string]bool)
for _, item := range items {
seen[item] = true
}
```
## 小结
Map是Go语言中一个强大而灵活的数据结构,它提供了高效的键值对存储方案。在实际开发中,Map的应用非常广泛,从简单的数据存储到复杂的缓存系统都能见到它的身影。掌握Map的使用,对于Go程序开发来说是必不可少的。
记住:合理使用Map可以让我们的代码更加简洁高效,但也要注意并发安全等问题。选择合适的数据结构和使用方式,才能写出更好的程序。