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
能保证并发安全,且对读操作进行过优化,适合读多写少的场景。