这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
1. 数组 (array)
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成,数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推,数组一档定义后,长度不能更改。
因为数组的长度是固定的,所以在Go语言中很少直接使用数组,一般都是使用切片来代替数组, 切片(slice)是可以增长和收缩的动态序列,功能也更灵活,要理解切片工作原理的话需要先理解数组。
1.1. 数组的声明
语法格式:var name [size]type
- var:定义数组变量使用的关键字。
- name:数组声明及使用时的变量名。
- size:数组的元素数量(数组长度),可以是一个表达式,但是一定要在编译时就能获得确定值,不能含有到运行时才能确认大小的数值。
- type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
示例:
package main
import "fmt"
func main() {
var stringarr [2]string
stringarr[0] = "hello"
stringarr[1] = "Golang"
fmt.Println("stringarr = ", stringarr)
}
//stringarr = [hello Golang]
1.2. 初始化数组
var arr = [3]int{1, 2, 3}
//或
arr := [3]int{1, 2, 3}
如果数组长度不确定,可以使用...代替数组长度,编译器会自动根据元素个数确定数组的长度:
var arr = [...]int{1, 2, 3, 4, 5}
//或
arr := [...]int{1, 2, 3, 4, 5}
如果指定了数组长度,还可以对指定位置的元素进行初始化:
func main() {
arr := [5]float64{0: 1.0, 4: 5.0}
fmt.Println("arr = ", arr)
}
//arr = [1 0 0 0 5]
1.3. 访问数组元素
使用下标索引的方式,即可对数组进行访问或者修改指定的数组元素的值:
func main() {
arr := [2]string{"Hello", "Golang!"}
fmt.Println("arr[0] = ", arr[0])
fmt.Println("arr[1] = ", arr[1])
}
//arr[0] = Hello
//arr[1] = Golang!
1.4. 获取数组的长度
将数组作为参数传递给len()函数,可以获得数组的长度:
func main() {
arr := [2]string{"Hello", "Golang!"}
fmt.Println("arr[0] = ", arr[0])
fmt.Println("arr[1] = ", arr[1])
fmt.Println("数组长度为:", len(arr))
}
//arr[0] = Hello
//arr[1] = Golang!
//数组长度为: 2
1.5. 数组比较
Go语言的数组比较,是使用 == 的方式,如果数组的元素个数不相同或者元素类型不相同,那么不能比较数组。
在两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,可以直接通过较运算符(==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。
func main() {
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [3]int{1, 2, 3}
fmt.Println(a == b) // true
fmt.Println(a == c) // invalid operation: a == c (mismatched types [2]int and [3]int)
}
1.6. 多维数组
Go同样支持多维数组,声明多维数组的语法格式为:
var name [size1][size2]...[sizen]type
// 声明一个4行2列的数组
var array [4][2]int
// 声明并初始化一个4行2列的数组
array1 := [4][2]int{{1, 1}, {2, 2}, {3, 3}, {4, 4}}
// 三维数组
array2 := [3][3]int{
{0, 1, 2},
{3, 4, 5},
{6, 7, 8}
}
1.7. 数组是值类型
Go中的数组是值类型,而不是引用类型。当对数组进行传递时,传递的是原始数组的副本。比如:将数组a传递给变量b,通过变量b对数组进行更改,原数组并不受影响。
func main() {
a := [3]int{1, 2, 3}
b := a
b[0] = 4
b[1] = 5
b[2] = 6
fmt.Println(a == b)
fmt.Println(a)
fmt.Println(b)
}
//false
//[1 2 3]
//[4 5 6]
同样地,在把数组作为参数传递给函数时,依然是值传递,而原始数组不受影响。
2. 切片 (slice)
Go 数组的长度固定不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(动态数组),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用的传递机制。
切片数据结构包含 Go 语言需要操作底层数组的元数据,分别是指向底层数组的指针、切片访问的元素的个数len(即长度)和切片允许增长到的元素个数cap(即容量)。
2.1. 切片的声明
切片的声明有两种方式:
- 通过声明一个未指定大小的数组来定义切片:
var name []type,只需要声明切片的类型而不需要声明长度。 - 使用
make()函数创建切片:var name []type = make([]type, len),可以简写为name := make([]type, len),也可以为切片指定容量name := make([]type, len, capacity),len为切片的当前长度,capacity是可选参数,在不声明capacity的情况下,默认capacity = len。 注:使用make()函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
2.2. 初始化切片
切片在未初始化时,默认为nil,长度为0,一般有如下两种形式对切片进行初始化:
- 声明且初始化切片
s := []int{1, 2, 3},[]表示是切片类型,初始值为1, 2, 3,其中capacity = len = 3。
- 使用数组初始化切片
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。从连续内存区域生成切片是常见的操作,格式如下: slice := arr[startIndex : endIndex],不包含结束位置的元素。
func main() {
// 数组a
a := [5]int{1, 2, 3, 4, 5}
// 切片截取
b := a[1:2]
c := a[:5]
d := a[1:]
e := a[:]
f := a[0:0]
fmt.Println("a[1:2] = ", b)
fmt.Println("a[ :5] = ", c)
fmt.Println("a[1: ] = ", d)
fmt.Println("a[ : ] = ", e)
fmt.Println("a[0:0] = ", f)
}
//a[1:2] = [2]
//a[ :5] = [1 2 3 4 5]
//a[1: ] = [2 3 4 5]
//a[ : ] = [1 2 3 4 5]
//a[0:0] = []
从数组或切片生成新的切片具有如下特点:
- 取出的元素数量为:结束位置 - 开始位置,取出元素不包含结束位置对应的索引,切片最后一个元素使用
slice[len(slice)]获取; - 当缺省开始位置时,表示从开头到结束位置;
- 当缺省结束位置时,表示从开始位置到末尾;
- 两者同时缺省时,与数组本身等效;
- 两者同时为 0 时,等效于空切片,一般用于切片复位。
2.3. append()和copy()
切片的长度是切片中元素的数量,切片的容量是切片最大能容纳的元素数量,可以通过len()函数获取切片的长度,通过cap()函数获取切片的容量。除此之外,还可以使用append()向切片追加一个或者多个元素,然后返回一个新的切片。copy()函数将原切片的元素复制到目标切片,并且返回复制的元素的个数。
append()函数会改变切片所引用的数组,从而影响到引用同一数组的其它切片。 当切片中没有剩余空间,即cap-len == 0时,此时将动态分配新的数组空间进行扩容,切片在扩容时,容量的扩展是按原切片容量的 2 倍数进行扩充,返回的切片指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的切片则不受影响。
package main
import "fmt"
func printSlice(x []int) {
fmt.Printf("len = %d cap = %d slice = %v\n", len(x), cap(x), x)
}
func main() {
var s []int
printSlice(s)
// 允许追加空切片
s = append(s, 0)
printSlice(s)
// 向切片添加一个元素
s = append(s, 1)
printSlice(s)
// 同时添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
// 创建切片s1,是s容量的两倍
s1 := make([]int, len(s), (cap(s))*2)
// 拷贝s的内容到s1
copy(s1, s)
printSlice(s1)
}
注:copy()方法是进行值复制,切片s1与切片s两者不存在联系,切片s发生变化时,s1不会随着变化。
在上述的示例代码中,向切片添加元素都是添加到尾部,其实可以使用append()函数加上切片索引的形式实现在切片的任意位置插入元素。
package main
import "fmt"
func main() {
var s = []string{"a", "c"}
// 在index处添加一个元素
s = append(s[:1], append([]string{"b"}, s[1:]...)...)
fmt.Println(s)
// 在头部插入元素
s = append([]string{"0"}, s...)
fmt.Println(s)
// 在index处插入切片
var newSlice = []string{" ", " ", " "}
s = append(s[:1], append(newSlice, s[1:]...)...)
fmt.Println(s)
}
//[a b c]
//[0 a b c]
//[0 a b c]
在上述代码中,s[:1]返回的是切片的第一个元素,s[1:]返回的是从二个元开始的整个切片,切片截取搭配上append()函数可以实现非常灵活的操作。
注:在向可变参数的函数中传递切片时,需要在切片后面加上...
2.4. 删除切片元素
切片是一个引用类型(类似于一个指针),本身不保存数据,对切片做的任何修改都将反映到它所指向的底层数组。切片是引用类型,只能与 nil判定相等,不能互相判定相等。
切片和C语言指针类似,指针可以做运算偏移,但可能造成内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。
func main() {
a := [5]int{1, 2, 3, 4, 5}// 数组a
var b []int // 声明一个切片b
if (b == nil) {
fmt.Println("切片为空...")
}
b = a[:]
for i := range b {
b[i]++
}
fmt.Println("a[1:2] = ", a)
}
//切片为空...
//a = [2 3 4 5 6]
Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是在开头位置删除、在中间位置删除和在尾部删除,其中删除切片尾部的元素速度最快。
在开头位置删除
// 删除开头的元素可以直接移动数据指针
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
// 也可以不移动数据指针,但是将后面的数据向开头移动,可以用append原地完成
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
// 还可以用copy()函数来删除开头的元素
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
在中间位置删除
// 删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用append或copy原地完成
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
在尾部位置删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
示例: 删除切片指定位置的元素。
package main
import "fmt"
func main() {
seq := []string{"a", "b", "c", "d", "e"}
// 指定删除位置
index := 2
// 查看删除位置之前的元素和之后的元素
fmt.Println(seq[:index], seq[index+1:])
// 将删除点前后的元素连接起来
seq = append(seq[:index], seq[index+1:]...)
fmt.Println(seq)
}
//[a b] [d e]
//[a b d e]
切片删除元素的操作过程:
Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。
2.5. 切片扩容
在使用append()函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变,会在内存中创建一个新的底层数组,切片指向这个新的的底层数组,而不再指向原数组,所以如果发生扩容,对切片的更改并不会影响原数组。切片在扩容时,容量的扩展规律是按原容量的 2 倍数进行扩充。
package main
import "fmt"
func main() {
var arr = [5]int{1, 2, 3, 4, 5}
s1 := arr[:] // 指向arr数组
s2 := arr[:] // 指向arr数组
// 改变s1切片的元素,底层数组改变,s2切片也改变
s1[1] = 0
fmt.Println("arr = ", arr)
fmt.Println("s2 = ", s2)
// 默认len == cap
fmt.Printf("长度:%d, 容量: %d\n", len(s1), cap(s1))
// 向s1切片添加新元素,发生扩容
s1 = append(s1, 6, 7, 8)
fmt.Printf("长度:%d, 容量: %d\n", len(s1), cap(s1))
fmt.Println("arr = ", arr)
fmt.Println(s1)
}
注:当容量不足时,添加元素会进行扩容,扩容容量为原切片的两倍;扩容会创建一个新的数组,切片指向新数组,原数组并不会发生变化。
往一个切片中不断添加元素扩容的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。
- 员工和工位就是切片中的元素。
- 办公地就是分配好的内存。
- 搬家就是重新分配内存。
- 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
- 由于搬家后地址发生变化,因此内存“地址”也会有修改。
3. Map
Map 是一种无序的键值对的集合,可以通过 key 来快速对应的 value 数据,key 可以是所有任何可以使用 == 进行比较的数据类型(基本类型),比如数字型、布尔型、字符串类型等,value 可以是任意的类型。可以使用for...range进行迭代,不过,Map 是无序的,我们无法决定它的返回顺序。Map同样是引用类型,长度不固定,可以通过len()函数返回键值对的数量。
3.1. map声明
Map的声明有两种方式,可以使用内建函数make()或map关键字进行声明:
// 使用map关键字
var name map[keyType]valueType
// 使用make函数
name := make(map[keyType]valueType)
注:[keyType] 和 valueType 之间允许有空格,未初始化的map的值为nil,不能直接使用和赋值。
| var | 声明变量使用的关键字 |
|---|---|
| name | map 变量的变量名 |
| map | 声明 map 变量的关键字 |
| keyType | map 的键的类型 |
| valueType | map 的值的类型 |
3.2. 初始化map
- 声明同时初始化
package main
import "fmt"
func main() {
// 声明+初始化
cityMap := map[int]string{
1: "广州",
2: "上海",
3: "北京",
4: "杭州",
}
// 遍历map
for key, value := range cityMap {
fmt.Println(key, value)
}
}
注:每次遍历map的返回的结果顺序都是不固定的。
- 先声明后初始化
package main
import "fmt"
func main() {
// 声明一个map
var cityMap map[string]string
// 初始化map
cityMap["广州"] = "小蛮腰"
cityMap["上海"] = "东方明珠"
cityMap["北京"] = "故宫"
cityMap["杭州"] = "西湖"
// 遍历map
for key, value := range cityMap {
fmt.Println(key, value)
}
}
//运行时错误:panic: assignment to entry in nil map
错误原因:map不同于array和基础类型,在声明时会初始化一个默认值,map是引用类型,如果未在声明时进行初始化,默认值是nil,不指向任何内存地址,所以nil map不能赋值,对nil map赋值会导致运行时错误。解决办法:可以在map声明后,通过make()函数为其分配内存地址后再进行赋值。
package main
import "fmt"
func main() {
// 声明一个map,默认值为nil,不能直接赋值
var cityMap map[string]string
// 使用make函数为nil map分配内存
cityMap = make(map[string]string)
// 初始化map
cityMap["广州"] = "小蛮腰"
cityMap["上海"] = "东方明珠"
cityMap["北京"] = "故宫"
cityMap["杭州"] = "西湖"
// 遍历map
for key, value := range cityMap {
fmt.Println(key, value)
}
}
拓展:同为引用类型的slice,在使用
append()向nil slice添加元素并不会发生错误,原因在于append()函数底层的扩容机制(详情可参考2.5 切片扩容),append()函数将元素追加到切片的尾部时,如果数组太小无法进行追加,则会分配一个更大容量的数组,slice指向这个新数组。同理,将向nil slice追加元素时,会为nil slice重新分配新的数组,让nil slice指向这个数组的内存地址。nil map问题官方文档解释如下:This variable m is a map of string keys to int values:var m map[string]int,Map types are reference types, like pointers or slices, and so the value of m above is nil; it doesn't point to an initialized map. A nil map behaves like an empty map when reading, but attempts to write to a nil map will cause a runtime panic; don't do that. To initialize a map, use the built in make function:m = make(map[string]int),nil slice问题官方文档解释如下:nil map doesn't point to an initialized map. Assigning value won't reallocate point address.The append function appends the elements x to the end of the slice s, If the backing array of s is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.
- 使用
make()初始化
package main
import "fmt"
func main() {
// 使用make声明并分配内存
cityMap := make(map[int]string)
// 初始化map
cityMap[1] = "北京"
cityMap[2] = "上海"
cityMap[3] = "广州"
cityMap[4] = "深圳"
// 遍历map
for key, value := range cityMap {
fmt.Println(key, value)
}
}
注:不能使用make()初始化map并同时进行赋值,错误示例如下:
// 使用make同时声明和为map赋值(错误)
cityMap := make(map[string]string){
"广州": "小蛮腰",
"上海": "东方明珠",
"北京": "故宫",
"杭州": "西湖",
}
总结:
map的赋值一共有三种方式:
- 在使用map关键字声明时同时进行赋值初始化
- 使用map关键字声明后,使用
make()函数为其分配内存地址进行初始化,再进行赋值 - 使用
make()函数同时声明和初始化,再进行赋值
3.3. 特殊类型作为value值
- 切片作为value值
正常情况下,key和value一一对应,但有些情况下,可能需要一个key对应多个value值,此时可以通过将value定义为切片类型来实现。
package main
import "fmt"
func main() {
hobbyMap := make(map[string][]string)
hobbyMap["张三"] = []string{"唱歌", "跳舞"}
hobbyMap["李四"] = []string{"跑步", "游泳"}
for k, v := range hobbyMap {
fmt.Printf("%s的爱好是:%v\n", k, v)
}
}
//张三的爱好是:[唱歌 跳舞]
//李四的爱好是:[跑步 游泳]
- map作为value值
示例:
- 使用
map[string]map[string]sting的map类 型 - key:表示用户名,是唯一的,不可重复
- 如果某个用户名存在,就将其密码修改"888888",如果不存在就增加这个用户信息(包括昵称nickname和密码 pwd)
- 编写一个函数
modifyUser(users map[string]map[string]sting, name string)完成上述功能
package main
import "fmt"
func modifyUser(users map[string]map[string]string, name string) {
if users[name] != nil {
// 用户存在密码改为888888
users[name]["pwd"] = "888888"
} else {
// 用户不存在,添加用户信息
users[name] = make(map[string]string, 2)
users[name]["pwd"] = "888888"
users[name]["nickname"] = "小小" + name //示意
}
}
func main() {
users := make(map[string]map[string]string, 10)
users["张三"] = make(map[string]string, 2)
users["张三"]["nickname"] = "zs"
users["张三"]["pwd"] = "1111111"
modifyUser(users, "李四")
modifyUser(users, "王五")
for k, v := range users {
fmt.Println(k, v)
}
}
//李四 map[nickname:小小李四 pwd:888888]
//张三 map[nickname:zs pwd:1111111]
//王五 map[nickname:小小王五 pwd:888888]
3.4. 增删改查
- 增加和更新
map[key] = value,如果key不存在于map中,则会对map增加一个键值对;如果key已经存在,则会对map中key对应的value值进行更新。
- 删除
Go语言提供了一个内置函数delete(),用于删除容器内的元素,delete()函数的语法格式:
delete(mapName, key),函数无返回值,当删除不存在的key时不会报错。
示例:
package main
import "fmt"
func main() {
cityMap := make(map[int]string)
cityMap[1] = "北京"
cityMap[2] = "上海"
cityMap[3] = "广州"
cityMap[4] = "深圳"
// 删除元素
delete(cityMap, 1)
for key, value := range cityMap {
fmt.Println(key, value)
}
}
注:使用delete()每次只能删除一对键值对,Golang并没有提供清空所有元素的方法,如果需要对map进行清空,可以对key进行遍历逐个删除或者map = make(...),则原来的map指向的底层数据结构会被Golang的垃圾回收机制进行回收。
- 查找
可以通过key获取map中对应的value值,语法格式为:map[key]。如果key存在,则返回对应的value值;如果key不存在,则返回value类型的默认值,如value的类型为int,当key不存在时,返回的为0,在Golang中操作map,无论key是否存在都不会出现panic或者返回error。
但是在很多情况下,需要判断key:value是否存在,为此,Golang通过在使用map[key]时返回第二参数来标识key是否存在,语法格式为:value, ok := map[key],当key存在时,返回的第二参数为true;当key不存在时,返回的第二参数为false。
func main() {
dict := map[string]int{"key1": 1, "key2": 2}
value, ok := dict["key1"]
if ok {
fmt.Printf(value)
} else {
fmt.Println("key1不存在...")
}
}
3.5. map遍历
可以通过for...range对map进行遍历,遍历时,同时返回key和value。
在某些情况下,如果只需要获得value,可以使用下划线_来替代key,示例如下:
package main
import "fmt"
func main() {
dict := map[int]string{
1: "张三",
2: "李四",
3: "王五",
}
// 只遍历value值
for _, value := range dict {
fmt.Println(value)
}
}
//李四
//王五
//张三
在只需要遍历key时,可以使用如下形式:for key := range dict,无需使用匿名变量:
package main
import "fmt"
func main() {
dict := map[int]string{
1: "张三",
2: "李四",
3: "王五",
}
// 只遍历key值
for key := range dict {
fmt.Println(key)
}
}
//1
//2
//3
注:Golang中的map默认是无序的,遍历的结果顺序与填充的顺序无关,可能每次遍历返回的结果都不同,如果需要返回特定顺序的结果,需要进行排序。步骤如下:
- 先将map中的key存入切片中
- 对切片进行排序
- 遍历切片,按照key输出map的value
package main
import (
"fmt"
"sort"
)
func main() {
dict := make(map[int]int)
dict[2] = 2
dict[5] = 8
dict[1] = 10
dict[6] = 1
dict[4] = 9
// 声明一个切片保存map的key
var slice []int
// 将map数据遍历保存到slice中
for key := range dict {
slice = append(slice, key)
}
// 对切片进行排序
sort.Ints(slice)
// 打印切片
fmt.Println(slice)
// 根据排序的key输出value
for _, key := range slice {
fmt.Printf("map[%v] = %v\n", key, dict[key])
}
}
3.6. map切片
当切片的数据类型为map时,称之为 slice of map,此时map的个数就能动态变化。
示例:使用map切片,map中保存学生的个人信息,包含name和age。
package main
import "fmt"
func main() {
// map切片,放入2个map
slice := make([]map[string]string, 2)
// 增加第一个学生的信息
if slice[0] == nil {
slice[0] = make(map[string]string, 2)
slice[0]["name"] = "张三"
slice[0]["age"] = "18"
}
// 增加第二个学生的信息
if slice[1] == nil {
slice[1] = make(map[string]string, 2)
slice[1]["name"] = "李四"
slice[1]["age"] = "20"
}
fmt.Println(slice)
}
//[map[age:18 name:张三] map[age:20 name:李四]]
3.7. 其他
- map是引用类型,遵守引用类型传递机制,一个函数在接收map后,对其进行修改,会直接修改原来的map,或者将一个map变量赋值给另一个变量时,它们都指向同一个底层map,相互之间会产生影响。
- map的容量是不固定的,可以动态变化,当向map中增加一个元素时,会自动进行扩容。
- map经常使用struct结构体作为value值,可以实现更为复杂的数据存储和管理。
3.8. sync.Map
Go语言中map如果在并发读的情况下是线程安全的,如果是在并发写的情况下,则是线程不安全的。Golang 为我们提供了一个 sync.Map 是并发写安全的。
Golang 中的 map 的 key 和 value 的类型必须是一致的,但 sync.Map 的 key 和 value 不一定是要相同的类型,不同的类型也是支持的。