Go 语言中的 Map

本文介绍一种特殊的数据结构。它是一种元素对的无序集合,每一个索引(key)对应一个值(value),这种数据结构在 Go 语言中被称之为 mapmap 是一种能够通过索引(key)迅速找到值(value)的数据结构,所以也被称为字典。在 Go 语言中因为线程安全问题,一共实现了两种类型的 map,接下来我们每种都了解一下。

Tips:线程的知识会在Go语言的多线程中讲解。

1. 无锁的map

这种类型的 map 是线程不安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,会有读写冲突,会导致系统奔溃。所以一般在单线程程序中使用的较多。

1.1 map 的创建

map 的底层结构也是一个指针,所以和变量不同,并不是声明后立刻能够使用。和切片相同,需要使用make()函数进行初始化。在初始化之前为空,没有零值。

代码示例:

package main

import (
    "fmt"
)

func main() {
    var m map[string]string
    fmt.Println(m == nil)
    m = make(map[string]string)
    fmt.Println(m == nil)
}
  • 第 8 行:声明一个 key 为 string 类型,value 为 string 类型的 map 变量;
  • 第 9 行:此时 m 未初始化,值为 nil;
  • 第 10 行:初始化 m。
  • 第 11 行:此时 m 是一个没有存放数据的 map,值不为 nil。

执行结果:

图片描述

1.2 map 的赋值

map 的赋值有两种方式:

  • 使用:=使map在定义的时候直接赋值;
  • 使用map[key]=value的形式对map进行赋值。

在明确知道 map 的值的时候就可以使用第一种方式进行赋值,比如说在建立中英文对应关系的时候。在未知 map 的取值时,一般建议使用后者进行赋值。

代码示例:

package main

import "fmt"

func main() {
    m1 := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
    fmt.Println(m1["Apple"])
    m2 := make(map[string]string)
    m2["Apple"] = "苹果"
    m2["Orange"] = "橘子"
    m2["Banana"] = "香蕉"
    fmt.Println(m2["Apple"])
}
  • 第 6 行:在 m1 被定义的时候直接赋值;
  • 第 7 行:输出 m 1中 key 为 “Apple” 时对应的值;
  • 第 8 行:使用:=进行免声明 make;
  • 第 9~11 行:对 m2 进行赋值;
  • 第 12 行:输出 m2 中 key 为 “Apple” 时对应的值。

执行结果:

图片描述

1.3 map 的遍历

map 是字典结构,如果不清楚所有 key 的值,是无法对 map 进行遍历的,所以 Go 语言中使用了一个叫做range的关键字,配合for循环结构来对map结构进行遍历。

Tips:range同时也可以用来遍历数组和切片,数组和切片在range中可以看为map[int]数据类型结构,遍历和用法和map一致。

代码示例:

package main

import "fmt"

func main() {
    m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
    for k, v := range m {
        fmt.Println("key:", k, ", value:", v)
    }
}
  • 第 7 行:使用 range 关键字,每次 for 循环都会取出一个不重复的 key 和 value,赋值给 k 和 v,直至循环结束。

Tips:map 是无序的,所以每次输出的顺序可能会不一样。

执行结果:

图片描述

1.4 map 的删除

map 在普通的用法中是无法移除只可以增加 key 和 value 的,所以 Go 语言中使用了一个内置函数delete(map,key)来移除 map 中的 key 和 value。

代码示例:

package main

import "fmt"

func main() {
    m := map[string]string{"Apple": "苹果", "Orange": "橘子", "Banana": "香蕉"}
    fmt.Println(m)
    delete(m, "Apple")
    fmt.Println(m)
}
  • 第8行:删除 m 中的 “Apple” 和其对应的 value。

执行结果:

图片描述

2. 自带锁的 sync.Map

这种类型的 map 是线程安全的 map,多个线程同时访问这个类型的 map 的同一个变量时,不会有读写冲突,因为它自带原子锁,保障了多线程的数据安全。

2.1 sync.Map 的创建

这种类型的 map 创建不需要make,直接声明就可以使用,而且不需要声明 map 的 key 和 value 的类型。因为它底层的实现并不是指针,是一种多个变量的聚合类型,叫做结构体

Tips:结构体的概念会在Go语言的结构体中讲解

代码示例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    fmt.Println(m)
}
  • 第 9 行:声明一个 sync.Map。
  • 第 10 行:输出 m 的零值。

执行结果:

图片描述

2.2 sync.Map 的操作

这个类型关于 map 的所有操作都是使用它自带的方法来实现的。包括range

代码示例:

package main

import (
    "fmt"
    "sync"
)

func main() {

    var m sync.Map
    m.Store("Apple", "苹果")
    m.Store("Orange", "橘子")
    m.Store("Banana", "香蕉")
    tmp, exist := m.Load("Orange")
    fmt.Println(tmp, exist)

    m.Delete("Banana")

    m.Range(func(k, v interface{}) bool {
        fmt.Println("key:", k, ", value:", v)
        return true
    })
}
  • 第 11~13 行:使用 Store 方法给 m 赋值;
  • 第 14 行:使用 Load 取出 “Orange” 对应的值,如果不存在 “Orange” 这个 key,exist 的值为 false;
  • 第 17 行:删除 m 中的 “Banana” 和其对应的 value;
  • 第 19 行:使用 Range 方法遍历 m。

执行结果:

图片描述

3. 小结

本文主要讲解了两个 map 数据类型,两种在功能上区别并不大,主要是在应用上。map[数据类型]数据类型一般使用在单线程场景,多线程场景使用sync.Map。在赋值上map[数据类型]数据类型可以赋初值,且需要指定数据类型。sync.Map无法赋初值,无需指定数据类型。