Go 常见数据类型-03印射
印射Map
印射是一种数据结构,用于存储一系列无序的键值对(类似PHP的对象),印射基于键来存储值。印射功能强大的地方是,能够基于键快速索引数据。键就像索引一样,指向该键关联的值。
因为印射也是一个数据集合,所以也可以使用类似处理数组和切片的方式来迭代印射中的元素。但是印射是无序集合,所以即使以同样的顺序来保存键值对,每次迭代印射时,元素顺序也可能不一样。无序的原因是因为印射使用了散列表。
印射使用散列表(hash)
实现,在$GOROOT/src/pkg/runtime/hashmap.go
可以查看具体实现细节(不同版本golang文件存储可能不同)。GO语言的map是一个hash数组列表,而不是像C++一样使用红黑树,与传统的hashmap一样,GO语言的map由一个个bucket组成。
hashmap列表中的每一个元素都被称为bucket的结构体,每个bucket可以保存8个键值对,所有元素将被hash算法填入到数组的bucket中,bucket填满后,将通过一个overflow指针来扩展一个bucket,从而形成链表,以解决hash冲突的问题。简单来说,这个map就是一个bucket指针型的一维数组,而每个bucket指针下面则不定长,可能挂着bucket指针list,也可能只有一个,视hash冲突而定。
印射的定义
Go语言中map
创建并初始化映射的方法有很多种,使用内置的make函数或者使用映射字面量都是常见的方法:
// 创建一个键的类型是string,值的类型为int的映射
dict := make(map[string]int)
// 创建一个键和值类型都为string的映射并初始化
dict := map[string]string{"a", "b", "c"}
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。
所以使用映射字面量是更常用的方法,映射的初始长度会根据初始化指定的键值对的数量来决定。
和数组、切片不同,映射可以根据新增的key-value动态伸缩,因此不存在固定长度和最大限制,但是也可以选择标明映射的初始容量capacity,如下:
make(map[keytype]valuetype, cap)
dict := make(map[string]string, 100)
当map增长到容量上限时,如果再增加新的key-value,容量会自动加1,所以出于性能考虑,对于大的映射或者可能快速扩张的映射,即使只知道大概容量,也最好先标明。
映射的键可以是任何值,值的类型并不限制,内置的类型或者结构类型都可以,不过需要确定这个值可以使用==
运算符作比较。需要注意的是,切片、函数以及包含切片的结构类型具有引用语义,都不能作为映射的键,使用这些类型会造成编译错误。
dict := map[int][]string{}
在使用一个映射键对应一组数据时,会非常有用。
map的使用
元素赋值
通过指定适当类型的键并给这个键赋值就完成了映射的键值对赋值(需要注意的是,未初始化的映射不能直接对键赋值):
var dict map[int]int // 只声明未初始化,此时dict为nil,不可使用,需要dict = map[int]int{}初始化才能使用
dict := map[int]int{} // 声明并自动初始化,可以使用
dict[0] = 2
fmt.Println(dict)
有时候会有几种特殊类型的map,处理的方式也不同。如:
// 元素为map类型的切片
var mapSlice = make([]map[string]int, 8, 8) // 只完成了切片的初始化,初始化了一个容量为8,有8个元素的切片,输出为 [map[] map[] map[] map[] map[] map[] map[] map[]]
//mapSlice[0]["test"] = 100 // 此时直接给mapSlice赋值会报错,因为只初始化了切片,map未初始化,map默认值为nil,会报不能给nil类型的map赋值
fmt.Print(mapSlice[0] == nil) // 输出:true
//结论:
fmt.Println(mapSlice)
//初始化切片内的map元素
mapSlice[0] = make(map[string]int, 8)
//赋值
mapSlice[0]["a"] = 10
fmt.Print(mapSlice) // 正常输出:[map[a:10] map[] map[] map[] map[] map[] map[] map[]]
//值为切片的map
var sliceMap = make(map[string][]int, 8) // 只完成了map的初始化,未初始化切片
_, ok := sliceMap["a"]
if ok {
fmt.Print(sliceMap["a"])
} else {
fmt.Print(sliceMap["a"])
//sliceMap["a"][0] = 10 // 未初始化,sliceMap["a"] 为一个未指定容量和长度的切片,无法赋值,此时会报错
sliceMap["a"] = make([]int, 8) // 初始化切片
sliceMap["a"][0] = 10
}
fmt.Print(sliceMap) // 输出:map[a:[10 0 0 0 0 0 0 0]]
与切片类似,通过声明一个未初始化的映射可以创建一个值为nil的映射(称为nil映射),nil映射不能用于存储键值对,否则会产生语言运行时的错误。
查找与遍历
map查找数据
从map中取数据有两种方式,一种是获得值以及一个表示这个键是否存在的标志:
value, exist := colors["red"]
// 如果存在 exist 为true,value为对应值,反之 exist 为false,value 为值类型零值
if exist {
fmt.PrintLn("exist")
}
另一种方式是,只返回对应的值,再判断这个值是否为0值,以此来确定键是否存在,这种只能用在映射存储的值都是非零值的情况:
value := color["red"]
if value != "" {
fmt.PrintLn("exist")
}
在GO语言中,通过键来索引映射时,即便这个键不存在也会返回该值对应的数据类型的零值。
map遍历
和迭代数组和切片一样,使用range关键字可以迭代映射里的所有值:
alphabet := map[string]string{
"a": "A",
"b": "B",
"c": "C",
}
for key, value := range alphabet {
fmt.Printf("key is : %s value is %s\n", key, value)
}
当我们只需要key
值的时候,也可以这样遍历:
for key := range alphabet {
fmt.Printf("key is : %s ", key)
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
map的删除
GO语言提供了一个内置函数delete()用于删除容器内的元素,下面是例子:
delete(alphabet, "a")
上面代码会从alphabet
中删除键位a
的键值对,如果这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil(即未初始化的map),该调用将导致程序抛出异常(panic),而delete一个空map并不会导致panic。(这个区别在最新的Go tips中已经没有了,即:delete一个nil map也不会panic)
map的并发
Go 语言中内置的 map 不是并发安全的。
var m = make(map[string]int)
func get(key string) int {
return m[key]
}
func set(key string, value int) {
m[key] = value
}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}
将上面的代码编译后执行,会报出fatal error: concurrent map writes
错误。我们不能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。
像这种场景下就需要为 map 加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map
。
开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
func (m *Map) Store(key, value interface{}):存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool):查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool):查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool):查询并删除key
func (m *Map) Delete(key interface{}):删除key
func (m *Map) Range(f func(key, value interface{}) bool):对map中的每个key-value依次调用f
下面的代码示例演示了并发读写sync.Map:
// 并发安全的map
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
// 对m执行20个并发的读写操作
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n) // 存储key-value
value, _ := m.Load(key) // 根据key取值
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
本作品采用《CC 协议》,转载必须注明作者和本文链接