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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!