上一篇我们学习了 Slice。Slice 非常适合保存一组动态数据,例如:
go
users := []string{"Tom", "Jack", "Lucy"}
但是 Slice 主要通过数字下标访问元素:
bash
users[0]
users[1]
实际开发中,我们经常希望通过用户名、商品编号、配置名称等直接查找数据。例如:
rust
"Tom" -> 90
"Jack" -> 85
"port" -> 8080
"host" -> localhost
这种"一个 Key 对应一个 Value"的数据结构,就是 Go 中的 Map。Map 是 Go 开发中使用频率非常高的数据结构,配置管理、缓存、数据统计、JSON 处理、数据库结果整理等场景都会大量使用。
一、什么是 Map
Map 是一种 Key-Value 键值对数据结构。
基本形式:
rust
Key -> Value
例如:
rust
Tom -> 90
Jack -> 85
Lucy -> 96
如果使用 Slice 保存成绩,可能需要:
go
names := []string{"Tom", "Jack", "Lucy"}
scores := []int{90, 85, 96}
而 Map 可以直接表示:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
查询 Tom 的成绩:
css
fmt.Println(scores["Tom"])
输出:
90
相比数字下标,通过具有业务含义的 Key 查找数据通常更加直观。
二、Map 的基本语法
Map 类型写法:
arduino
map[Key类型]Value类型
例如:
c
map[string]int
表示:
ini
Key = string
Value = int
也可以:
c
map[int]string
表示整数作为 Key,字符串作为 Value。
例如:
go
users := map[int]string{
1: "Tom",
2: "Jack",
3: "Lucy",
}
访问:
bash
fmt.Println(users[2])
输出:
Jack
三、定义 Map
可以先声明:
go
var scores map[string]int
但此时 scores 是 nil map:
ini
fmt.Println(scores == nil)
输出:
arduino
true
nil map 可以读取,但是不能直接写入:
css
scores["Tom"] = 90
这样运行时会发生错误。因此需要先初始化 Map。
四、使用 make 创建 Map
最常见的方式是:
go
scores := make(map[string]int)
然后添加数据:
css
scores["Tom"] = 90
scores["Jack"] = 85
scores["Lucy"] = 96
完整示例:
go
package main
import "fmt"
func main() {
scores := make(map[string]int)
scores["Tom"] = 90
scores["Jack"] = 85
scores["Lucy"] = 96
fmt.Println(scores)
}
还可以给 make 提供一个初始容量提示:
go
scores := make(map[string]int, 100)
如果预计需要保存较多数据,这种方式可以减少运行过程中重新分配内部存储的开销。不过这个数字不是固定长度,Map 仍然可以继续增加元素。
五、创建 Map 时直接初始化
如果数据已经确定,可以直接:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
字符串 Map:
go
config := map[string]string{
"host": "localhost",
"port": "8080",
"mode": "debug",
}
访问:
arduino
fmt.Println(config["host"])
输出:
localhost
六、添加和修改数据
Map 添加数据非常简单:
go
scores := make(map[string]int)
scores["Tom"] = 90
如果 Key 不存在,就是新增。
继续:
css
scores["Jack"] = 85
如果 Key 已经存在:
css
scores["Tom"] = 100
就是修改。
因此 Map 添加和修改使用相同语法:
css
m[key] = value
Map 中的 Key 不能重复,同一个 Key 只能对应一个当前 Value。
七、读取 Map 数据
读取数据:
go
scores := map[string]int{
"Tom": 90,
}
fmt.Println(scores["Tom"])
输出:
90
但有一个非常重要的问题。如果访问不存在的 Key:
css
fmt.Println(scores["Jack"])
不会直接报错,而是返回 Value 类型的零值。
由于 Value 是 int,因此得到:
0
如果 Value 是字符串:
go
config := map[string]string{}
fmt.Println(config["host"])
得到空字符串。
因此仅根据返回值,有时候无法判断 Key 到底存在还是不存在。
八、判断 Key 是否存在
Go 提供了非常经典的 Map 查询写法:
go
value, ok := scores["Tom"]
其中:
ini
value = 对应的数据
ok = Key 是否存在
例如:
go
score, ok := scores["Tom"]
if ok {
fmt.Println("成绩:", score)
} else {
fmt.Println("用户不存在")
}
也可以直接:
css
if score, ok := scores["Tom"]; ok {
fmt.Println(score)
}
这是 Go 项目中非常常见的写法。
例如:
go
scores := map[string]int{
"Tom": 0,
}
如果只执行:
go
score := scores["Tom"]
得到 0,但无法判断是 Tom 的成绩真的为 0,还是 Tom 不存在。
使用:
go
score, ok := scores["Tom"]
就可以准确区分。
九、删除 Map 数据
Go 提供内置函数:
scss
delete()
例如:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
}
delete(scores, "Tom")
fmt.Println(scores)
Tom 对应的数据就被删除了。
基本语法:
go
delete(map变量, key)
如果删除一个不存在的 Key:
go
delete(scores, "Lucy")
也不会报错。
十、获取 Map 元素数量
可以使用:
scss
len()
例如:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
fmt.Println(len(scores))
输出:
3
添加:
css
scores["Bob"] = 88
此时:
scss
len(scores)
就是 4。
删除:
go
delete(scores, "Tom")
长度又会减少。
十一、遍历 Map
Map 通常使用 range 遍历:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
for key, value := range scores {
fmt.Println(key, value)
}
如果只需要 Key:
go
for key := range scores {
fmt.Println(key)
}
如果只需要 Value:
go
for _, value := range scores {
fmt.Println(value)
}
需要特别注意:不要依赖 Map 的遍历顺序。
不能认为:
go
for key, value := range scores {
// 每次都会按照插入顺序执行
}
Go 不保证 Map 的遍历顺序。如果业务要求固定顺序,通常需要单独保存 Key,然后排序。
十二、按照 Key 排序输出
例如:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
先提取所有 Key:
go
keys := make([]string, 0, len(scores))
for key := range scores {
keys = append(keys, key)
}
然后排序:
lua
sort.Strings(keys)
最后按照排序后的 Key 访问:
vbnet
for _, key := range keys {
fmt.Println(key, scores[key])
}
完整代码:
go
package main
import (
"fmt"
"sort"
)
func main() {
scores := map[string]int{
"Tom": 90,
"Jack": 85,
"Lucy": 96,
}
keys := make([]string, 0, len(scores))
for key := range scores {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Println(key, scores[key])
}
}
这也是 Slice 和 Map 配合使用的典型场景。
十三、Map 的 Key 有什么要求
并不是所有类型都能作为 Map 的 Key。
Map 的 Key 必须是可以使用 == 和 != 比较的类型。
常见可以作为 Key 的类型包括:
c
string
int
bool
数组
指针
部分 struct
例如:
c
map[string]int
map[int]string
都是非常常见的。
Slice 不能直接作为 Map Key:
c
map[[]int]string
这是错误的,因为 Slice 不能直接使用 == 比较两个切片的内容。
Map 本身和函数类型也不能作为 Map Key。
十四、Map 的 Value 可以很复杂
Map 的 Value 不仅可以是基本类型,还可以是 Slice、Map、Struct 等。
例如 Value 是 Slice:
go
users := map[string][]string{
"admin": {"Tom", "Jack"},
"user": {"Lucy", "Bob"},
}
访问:
bash
fmt.Println(users["admin"])
输出:
csharp
[Tom Jack]
也可以追加:
bash
users["admin"] = append(users["admin"], "Mike")
这种 map[string][]string 在实际开发中非常常见。
十五、嵌套 Map
Map 的 Value 还可以继续是 Map:
go
users := map[string]map[string]string{
"1001": {
"name": "Tom",
"age": "20",
},
"1002": {
"name": "Jack",
"age": "25",
},
}
访问:
css
fmt.Println(users["1001"]["name"])
输出:
Tom
不过当数据结构越来越复杂时,通常更推荐使用 Struct,而不是无限嵌套 Map,因为 Struct 类型更加明确,也更容易维护。
十六、Map 作为函数参数
Map 可以直接作为函数参数:
go
func change(scores map[string]int) {
scores["Tom"] = 100
}
调用:
go
scores := map[string]int{
"Tom": 90,
}
change(scores)
fmt.Println(scores["Tom"])
输出:
100
这说明函数中修改 Map 内容可以影响调用方看到的数据。
因此通常没有必要写:
go
func change(scores *map[string]int)
直接传:
go
func change(scores map[string]int)
一般就可以完成对 Map 内容的修改。
十七、Map 不能直接比较
两个 Map 不能直接:
css
a == b
例如:
go
a := map[string]int{"Tom": 90}
b := map[string]int{"Tom": 90}
下面这样是不允许的:
css
fmt.Println(a == b)
Map 只能和 nil 比较:
go
if a == nil {
fmt.Println("nil map")
}
如果需要比较两个 Map 的内容,可以自己遍历比较,或者在合适场景下使用标准库提供的相关工具。
十八、nil Map 和空 Map
下面是 nil Map:
go
var a map[string]int
此时:
ini
a == nil
为 true。
下面是已经初始化但没有数据的 Map:
go
b := make(map[string]int)
此时:
ini
b == nil
为 false。
两者:
scss
len(a)
len(b)
都是 0。
最大的区别之一是 nil Map 不能写入:
css
a["Tom"] = 90
会发生运行时错误。
而:
css
b["Tom"] = 90
可以正常执行。
因此如果准备向 Map 写入数据,应先使用 make() 或字面量初始化。
十九、Map 实战:统计单词出现次数
Map 非常适合进行数据统计。
例如:
go
words := []string{
"go", "java", "go", "rust",
"go", "java",
}
统计每个单词出现次数:
go
package main
import "fmt"
func main() {
words := []string{
"go", "java", "go",
"rust", "go", "java",
}
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
for word, count := range counts {
fmt.Println(word, count)
}
}
核心代码只有:
arduino
counts[word]++
如果 Key 不存在:
arduino
counts[word]
默认得到 0,然后执行 ++,第一次就变成 1。
最终数据类似:
rust
go -> 3
java -> 2
rust -> 1
这就是 Map 非常典型的使用方式。
二十、Map 实战:用户信息查询
例如保存用户 ID 和用户名:
go
users := map[int]string{
1001: "Tom",
1002: "Jack",
1003: "Lucy",
}
查询:
bash
id := 1002
if name, ok := users[id]; ok {
fmt.Println("用户:", name)
} else {
fmt.Println("用户不存在")
}
这种结构可以用于缓存、配置映射、状态映射等场景。
二十一、并发使用 Map 要注意
普通 Map 不适合在没有同步保护的情况下进行并发读写。
例如多个 goroutine 同时修改:
css
m["count"]++
可能产生并发安全问题。
后面学习并发编程时,可以使用:
dart
sync.Mutex
对共享 Map 进行保护,或者根据具体场景使用:
dart
sync.Map
因此现阶段先记住:
javascript
普通 Map 不要随意进行无同步的并发读写。
二十二、Slice 和 Map 怎么选择
如果数据主要按照位置保存:
第0个
第1个
第2个
通常使用 Slice:
c
[]string
如果需要根据 Key 查找:
rust
userID -> user
name -> score
config -> value
通常使用 Map:
c
map[string]int
例如用户列表:
go
users := []string{"Tom", "Jack", "Lucy"}
适合 Slice。
用户 ID 对应用户名:
go
users := map[int]string{
1001: "Tom",
1002: "Jack",
}
则更加适合 Map。
二十三、Map 常见错误
第一个错误是没有初始化就写入:
go
var m map[string]int
m["Tom"] = 90
应该:
go
m := make(map[string]int)
第二个错误是无法区分零值和 Key 不存在:
go
value := m["Tom"]
更可靠的方式:
go
value, ok := m["Tom"]
第三个错误是依赖 Map 遍历顺序:
go
for key := range m {
}
Map 不保证遍历顺序。
第四个错误是使用不支持比较的类型作为 Key:
c
map[[]int]string
Slice 不能作为 Map Key。
第五个错误是多个 goroutine 无保护地同时读写普通 Map,在并发程序中必须特别注意。
二十四、总结
Map 是 Go 中非常重要的键值数据结构。
定义:
go
var scores map[string]int
创建:
go
scores := make(map[string]int)
初始化:
go
scores := map[string]int{
"Tom": 90,
"Jack": 85,
}
添加:
css
scores["Lucy"] = 96
修改:
css
scores["Tom"] = 100
读取:
go
score := scores["Tom"]
判断 Key:
go
score, ok := scores["Tom"]
删除:
go
delete(scores, "Tom")
长度:
scss
len(scores)
遍历:
go
for key, value := range scores {
fmt.Println(key, value)
}
学习 Map 重点掌握:
javascript
1.Map 使用 Key-Value 保存数据
2.Key 必须是可比较类型
3.读取不存在的 Key 会返回 Value 的零值
4.使用 value, ok 判断 Key 是否存在
5.delete 可以删除指定 Key
6.Map 遍历顺序不固定
7.nil Map 可以读取但不能写入
8.普通 Map 并发读写需要同步保护
到这里,我们已经学习了数组 Array、切片 Slice 和 Map,这三种数据结构能够解决大量集合数据存储问题。但前面的内容中还有一个非常重要的问题:当变量传递给函数以后,到底是在操作原来的数据,还是操作一份副本?为什么有时候修改函数参数不会影响外部变量,而 Map、Slice 又表现得有所不同?要真正理解这些问题,就需要掌握 Go 中非常重要的基础概念------指针 Pointer 。
下一篇:指针 Pointer------理解地址、取址与解引用