Go 语言类型 映射
基本概念
映射(Map)是用于储存一系列无序键值对的数据结构。映射类型适合用于存储和检索关联数据,类似其他编程语言中的哈希表或字典。
在 Go 语言映射中,键(key)和值(value)是组成映射的基本元素,每个键都关联到一个特定值:
- 键:键用来在映射中唯一标识值。键类型必须是可比较类型,包括所有基本类型以及数组和结构体,不包括引用类型。
- 值:值是映射中与键相关联的数据。在同一个映射中,所有值类型都必须相同,值类型可以是任意类型,包括映射或结构体。
映射提供一种直观方式来表达数据之间的联系,底层实现是一个散列表,属于引用类型。
创建映射
映射在使用前必须初始化,为其分配内存空间,才能存储键值对。
声明和初始化
以下是声明和初始化映射方法:
package main
import "fmt"
func main() {
// 声明一个 nil 映射,不能直接使用
// 切片不能作为键,但数组可以
var a map[[3]int]int
// 声明并使用字面量初始化映射
var b = map[string]int{"a": 1, "b": 2}
// 短变量声明,采用多行赋值形式
// 如果 } 另起一行,最后一个元素后必须加逗号
c := map[string]int{
"c": 3,
"d": 4,
}
// 使用 make 函数创建并初始化空映射
// 容量参数可选,预设有助于提高性能
d := make(map[string]int, 100)
// 输出:map[] map[a:1 b:2] map[c:3 d:4] map[]
fmt.Println(a, b, c, d)
}
nil 映射
和 nil
切片不同,nil
映射由于没分配内存,不能直接用于储存键值对。尝试添加键值对会引发运行时错误,必须先通过 make
函数初始化:
package main
import "fmt"
func main() {
// 声明一个 nil 映射
var m map[string]int
// 可以读取 nil 映射
value := m["k"]
fmt.Println("读取不存在的键值:", value) // 输出:0
// 尝试添加键值对会导致运行时错误
m["k"] = 1 // 报错:assignment to entry in nil map
// 使用 make 函数初始化
m = make(map[string]int)
m["k"] = 1
fmt.Println("添加键值后:", m) // 输出:map[k:1]
}
nil
映射不占用内存,可以用于延迟初始化或作为函数返回值。
映射操作
只要映射已初始化,映射操作基本是安全的。
读取键值
映射中可以通过键来获取元素值,此外还会获取到键存在标志。如果键值对不存在,存在标志为 false
并返回值类型零值:
package main
import "fmt"
func main() {
m := map[string]int{"Alice": 68000, "Bob": 72000}
// 对返回分别赋值
salary, exists := m["Charlie"]
if exists {
fmt.Println(salary)
} else {
fmt.Println(salary, exists) // exists 值为 false,salary 为 0
}
}
键存在标志是通过比较运算得到,所以键才必须是可比较类型。但和切片一样,映射之间不可比较,只能同 nil
做比较。
修改键值
修改映射键值对通过赋值来实现。如果键不存在,会新增键值对记录;如果键存在,赋值操作会更新该键值:
package main
import "fmt"
func main() {
m := map[string]int{"Alice": 68000, "Bob": 72000}
fmt.Println("初始工资:", m)
// 添加新记录
m["Charlie"] = 90000
fmt.Println("添加操作后:", m)
// 更新记录
m["Alice"] = 71000
fmt.Println("更新操作后:", m)
}
遍历映射
可以使用 for
和 range
来遍历映射中所有键值对:
package main
import "fmt"
func main() {
m := map[string]int{"Alice": 101, "Bob": 102, "Charlie": 104, "David": 103}
// 使用 len 获取映射中键值对数量
fmt.Println("员工数量:", len(m))
// 使用 for 和 range 遍历映射,每次迭代输出顺序可能不同
for k, v := range m {
fmt.Printf("%s 在部门编号:%d\n", k, v)
}
}
由于映射内元素排序不固定,因此不能对映射元素取址。
删除键值
Go 语言提供内置 delete
函数,用于删除映射中的键值对。 delete
函数没有返回值,如果键值对存在则删除;如果不存在则忽略,不会报错:
package main
import "fmt"
func main() {
m := map[string]int{"Alice": 7000, "Bob": 8000, "Charlie": 9000}
fmt.Println("初始映射:", m)
// 删除存在的键
delete(m, "Bob")
fmt.Println("第一次删除:", m)
// 尝试删除不存在的键
delete(m, "David")
fmt.Println("第二次删除:", m) // 输出没有变化
}
复制映射
映射是引用类型,所以在函数中对映射修改,会影响所有被传递映射的引用。可以在函数内部创建映射副本,修改并返回副本。复制映射需要创建一个空映射,遍历原始映射键值对来填充新映射:
package main
import "fmt"
func modify(m map[string]int) map[string]int {
// 创建映射副本
n := make(map[string]int)
for k, v := range m {
n[k] = v
}
n["c"] = 3
return n
}
func main() {
m := map[string]int{"a": 1, "b": 2}
n := modify(m)
fmt.Println("原始映射:", m)
fmt.Println("新映射:", n)
}
注意,当映射值是复杂数据结构(如切片、其他映射或包含指针的结构体)时,仅复制键值对复制的可能是指针,不是真正的深度复制。除了手动实现更深层次复制逻辑,也可以借助第三方库 copier 和 deepcopy,或者通过 encoding/gob
包序列化和反序列化对象到内存中来实现深度复制。
模拟集合
Go 语言没有集合类型,可利用映射键特性来模拟集合去重功能:
package main
import "fmt"
func main() {
// 原始数组
a := [...]int{1, 2, 3, 2, 1}
// 建立一个映射模拟集合
m := map[int]bool{}
// 创建一个切片来保存结果
s := []int{}
// 利用键的唯一性填充映射
for _, v := range a {
m[v] = true
}
// 通过查询键值来判断状态
fmt.Println(m[1]) // 值已存入,输出:true
fmt.Println(m[10]) // 没在映射,输出:false
// 将键存入切片
for k := range m {
s = append(s, k)
}
fmt.Println(s) // 输出: [1 2 3]
}
并发映射
映射类型在并发情况下允许同时读,但同时写会导致运行时错误:
package main
func main() {
m := make(map[int]int)
// 不停写入
go func() {
for {
m[0] = 0
}
}()
// 不停读取
go func() {
for {
_ = m[1]
}
}()
// 无限循环,让并发程序在后台执行
for {
}
}
上面代码会报错:concurrent map read and map write
,指出两个并发函数读写映射时发生竟态问题。
除了加锁外,可以使用 sync.Map
替代映射。sync.Map
是一个支持并发安全的映射类型,使用上与原生映射不太相同,需要调用专属方法:
package main
import (
"fmt"
"sync"
)
func main() {
// 不需要初始化,直接声明后使用
var m sync.Map
// 储存键值对,以 interface{} 类型保存值
m.Store("a", 1)
m.Store("b", 2)
// 也用于更新键值对
m.Store("b", 20)
// 获取键值对
v, ok := m.Load("b")
if ok {
fmt.Println(v) // 输出:20
}
fmt.Println(m.Load("c")) // 输出:<nil> false
// 如果键存在则读取值,否则储存键值对
v, ok = m.LoadOrStore("c", 3)
fmt.Println(v, ok)
// 删除键值对
m.Delete("a")
// 遍历键值对需要提供一个函数,函数可以操作键值。这里只打印
m.Range(func(key, value interface{}) bool {
fmt.Println(key, ":", value)
return true
})
}
使用 sync.Map
能保证并发安全,且对读操作进行过优化,适合读多写少的场景。