变量和常量虽能存储数据,但是在编写一些逻辑稍复杂的程序中,往往需要存储更多、更复杂且不同类型的数据,这些数据一般存储在Go语言的内置容器中。

Go语言的内置容器主要有数组、切片和映射。
下面详细介绍以上三种内置容器的特点和使用方法,学习目标:在编程中能使用恰当的容器存储数据并对其进行增加、删除和修改等操作。

rq

  • 数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中。
  • 数组中存放的元素类型可以是整型、字符串或其他自定义类型。数组在使用前需先声明,声明时必须指定数组的大小且数组大小之后不可再变
  • 数组元素可以通过数组下标来读取或修改,数组下标从0开始,第一个元素的数组下标为0,第二个元素的数组下标为1,以此类推。

array

数组声明格式如下:

1
var 数组变量名 [数组长度]元素类型

例如,声明数组student,长度为3,元素类型为string:

1
var student [3]string
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var array [3]string
fmt.Println(array)
}

// [ ]

由于以上代码仅声明了数组,没有对数组进行赋值,因此打印出来的数组为空数组。

数组可在声明后进行赋值,样例如下:

1
2
var student [3]string
student = [3]string{"Tom","Ben","Peter"}

数组可在声明时进行赋值,样例如下:

1
var student = [3]string{"Tom","Ben","Peter"}

使用这种方式初始化数组,需要保证大括号里面的元素数量和数组大小一致。

如果忽略中括号内的数字,不设置数组大小,Go语言编译器在编译时也可根据元素的个数来设置数组的大小,通过用“…”代替数组大小来实现。样例如下:

1
var student = [...]string{"Tom","Ben","Peter"}
1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var array = [...]string{"felix","xiaodongwang","mafeifei"}
fmt.Println(array)
}

// [felix xiaodongwang mafeifei]

append切片添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
a := []int{1, 2, 3}
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a,[]int{11,11,11}...) // 追加一个切片, 切片需要解包
a = append([]int{22,22,22},a...) // 在开头添加1个切片
a = append(a[:0], append([]int{1,2,3}, a[0:]...)...) // 在第0个位置插入切片
fmt.Println(a)
for i:=0;i<len(a);i++{
fmt.Println(a[i])
}
}

删除元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 从开头位置删除
package main

import "fmt"

func main() {
//删除开头的元素可以直接移动数据指针:
a := []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
fmt.Println(a)

//也可以不移动数据指针,但是将后面的数据向开头移动,可以用 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 = 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个元素
}

// 从中间位置删除
func main() {
//对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 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个元素
}

// 从尾部删除
func main() {
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
}

range是Go语言中非常常用的一个关键字,其主要作用就是配合for关键字对数组以及之后会介绍到的切片和映射等数据结构进行迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
var array = [...]int{1,2,3,4,5,6}
for k, v := range array {
fmt.Println("变量k",k,"变量v",v)
}
}

//变量k 0 变量v 1
//变量k 1 变量v 2
//变量k 2 变量v 3
//变量k 3 变量v 4
//变量k 4 变量v 5
//变量k 5 变量v 6

range后接的表达式称为range表达式,本例的range表达式为数组。在迭代时,关键字range会返回两个值,分别由变量k和v接收。其中k是当前循环迭代到的索引位置,v是该位置对应元素值的一份副本。

其他range表达式及其对应的返回值如下表:

range 表达式 第一返回值 第二返回值
数组 元素下标 元素值
切片 元素下标 元素值
映射
通道 元素 N/A

数组元素可以通过数组下标来读取或修改,数组下标从0开始,第一个元素的数组下标为0,第二个元素的数组下标为1,以此类推。
通过遍历数组的方式(for循环)来对其进行打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var array = [...]string{"felix","xiaodongwang","mafeifei"}
for k,v := range array {
fmt.Println("变量k",k,"变量v",v)
}
}

//变量k 0 变量v felix
//变量k 1 变量v xiaodongwang
//变量k 2 变量v mafeifei

相对于数组,切片(slice)是一种更方便和强大的数据结构,它同样表示多个同类型元素的连续集合,但是切片本身并不存储任何元素,而只是对现有数组的引用。
切片结构包括:地址、长度和容量。

  • 地址:切片的地址一般指切片中第一个元素所指向的内存地址,用十六进制表示。
  • 长度:切片中实际存在元素的个数。长度:切片中实际存在元素的个数。
  • 容量:从切片的起始元素开始到其底层数组中的最后一个元素的个数。

切片的长度和容量都是不固定的,可以通过追加元素使切片的长度和容量增大。
切片主要有三种生成方式:

  1. 从数组生成一个新的切片;
  2. 从切片生成一个新的切片;
  3. 直接生成一个新的切片。

从数组或切片生成新的切片语法格式如下:

1
slice [开始位置:结束位置]

动手写一个长度为3的student数组,我们可以生成一个新的切片student1,使用len()函数可获得当前切片长度,cap()函数可获得当前切片容量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
var student = [...]string{"Tom","Ben","Peter"}
var student1 = student[1:2]
fmt.Println("student数组:",student)
fmt.Println("student1切片:",student1)
fmt.Println("student数组地址为:",&student[1]) //取student[1]元素的地址
fmt.Println("student1切片地址为:",&student1[0])//取student1[0]元素的地址
fmt.Println("student1切片长度为:",len(student1))
fmt.Println("student1切片容量为:",cap(student1))
}

//student数组: [Tom Ben Peter]
//student1切片: [Ben]
//student数组地址为: 0xc0000b6340
//student1切片地址为: 0xc0000b6340
//student1切片长度为: 1
//student1切片容量为: 2

根据运行结果,我们可以归纳出从数组或切片生成新的切片有如下特性:

  • 新生成的切片长度:结束位置-开始位置。
  • 新生成的切片取出的元素不包括结束位置对应的元素。
  • 新生成的切片是对现有数组或切片的引用,其地址与截取的数组或切片开始位置对应的元素地址相同。
  • 新生成的切片容量指从切片的起始元素开始到其底层数组中的最后一个元素的个数。(所以为什么切片的长度显示为1,而容量却是2)

我们重新从student数组生成student1切片,再从student1切片生成student2切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

func main() {
var sudent = [...]string{"wang","yao","fei"}
var sudent1 = sudent[1:3]
var sudent2 = sudent1[0:1]
fmt.Println("student数组",sudent[:])
fmt.Println("sudent1切片",sudent1[:])
fmt.Println("sudent1切片",sudent2[:])
fmt.Println("student数组地址为",&sudent[1])
fmt.Println("student1切片地址为",&sudent1[0])
fmt.Println("student2切片地址为",&sudent2[0])
fmt.Println("student1切片长度为",len(sudent1))
fmt.Println("student1切片容量为",cap(sudent1))
fmt.Println("student2切片长度为",len(sudent2))
fmt.Println("student2切片容量为",cap(sudent2))

}

//student数组 [wang yao fei]
//sudent1切片 [yao fei]
//sudent1切片 [yao]
//student数组地址为 0xc000072190
//student1切片地址为 0xc000072190
//student2切片地址为 0xc000072190
//student1切片长度为 2
//student1切片容量为 2
//student2切片长度为 1
//student2切片容量为 2
  • 为了将student的最后一个元素也取到,student1切片的结束位置设为了3。(包前不包后)
  • 根据结果,可以发现student2切片仍然是对底层数组student的引用。
  • 另外,也可以通过slice[:]来表示切片本身。

声明切片

切片的声明格式如下:

1
var 切片变量名 []元素类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var student []int
fmt.Println("student切片:",student)
fmt.Println("student切片长度:",len(student))
fmt.Println("student切片容量:",cap(student))
fmt.Println("student切片是否为空:",student==nil)
}

//student切片: []
//student切片长度: 0
//student切片容量: 0
//student切片是否为空: true

从运行结果可以知道,切片声明后其内容为空,长度和容量均为0。

初始化切片

我们可以在声明切片的同时进行初始化赋值,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var student = []string{"yao","fei","chi"}
fmt.Println("student切片",student)
fmt.Println("student切片长度",len(student))
fmt.Println("student切片容量",cap(student))
fmt.Println("student切片是否为空",student==nil)
}

//student切片 [yao fei chi]
//student切片长度 3
//student切片容量 3
//student切片是否为空 false

使用make()函数初始化
声明完切片后,可以通过内建函数make()来初始化切片,格式如下:

1
make([]元素类型,切片长度,切片容量)

注意:切片的容量值必须大于等于切片长度值,否则程序会报错。对于切片的容量应该有个大概的估值,若容量值过小,对切片的多次扩充会造成性能损耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main(){
var student []int
student = make([]int,2,10)
fmt.Println("student切片",student)
fmt.Println("student切片长度",len(student))
fmt.Println("student切片容量",cap(student))
fmt.Println("判断student切片是否为空",student==nil)
}

//student切片 [0 0]
//student切片长度 2
//student切片容量 10
//判断student切片是否为空 false

我们可以发现student切片在初始化后,自动填充了0值且不再为空。

Go语言中,可以使用append()函数来对切片进行元素的添加。当切片不能再容纳其他元素时(即当前切片长度值等于容量值),下一次使用append()函数对切片进行元素添加,容量会按2倍数进行扩充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
student := make([]int, 1, 1)
fmt.Println("当前切片长度:", len(student), "当前切片容量:", cap(student))
for i := 0; i < 8; i++ {
student = append(student, i)
fmt.Println("当前切片长度:", len(student), "当前切片容量:", cap(student))
}
}

//当前切片长度: 1 当前切片容量: 1
//当前切片长度: 2 当前切片容量: 2
//当前切片长度: 3 当前切片容量: 4
//当前切片长度: 4 当前切片容量: 4
//当前切片长度: 5 当前切片容量: 8
//当前切片长度: 6 当前切片容量: 8
//当前切片长度: 7 当前切片容量: 8
//当前切片长度: 8 当前切片容量: 8
//当前切片长度: 9 当前切片容量: 16

由于切片是对底层数组的引用,所以在改变切片数据时,要考虑对其他切片或数组的数据影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import "fmt"

func main() {
//声明一个数组
var student = [...]string{"fei","chi","yao"}
//从数组生成切片student1
var student1 = student[0:1]
//打印数组和切片
fmt.Println("student数组:",student)
fmt.Println("student1切片:",student1)
fmt.Println("student1切片:",student1,",切片长度为:",len(student1),",切片容量为:",cap(student1))
//对student1切片添加元素
student1 = append(student1,"Danny")
//查看切片长度变化
fmt.Println("扩充Danny后的student1切片:",student1,",切片长度为:",len(student1),",切片容量为:",cap(student1))
//查看底层数组内容变化
fmt.Println("扩充Danny后的student数组:",student)
}

//student数组: [fei chi yao]
//student1切片: [fei]
//student1切片: [fei] ,切片长度为: 1 ,切片容量为: 3
//扩充Danny后的student1切片: [fei Danny] ,切片长度为: 2 ,切片容量为: 3
//扩充Danny后的student数组: [fei Danny yao]

由于student1切片是从student数组生成(即对student数组的引用),为student1添加元素会覆盖student数组中对应的元素。所以,如果切片是从其他数组或切片生成,新切片的元素添加需要考虑对原有数组或切片中数据的影响。

由于Go语言没有为删除切片元素提供方法,所以需要手动将删除点前后的元素连接起来,从而实现对切片中元素的删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var student = []string{"Tom", "Ben", "Peter", "Danny"}
student = append(student[0:2],student[3:]...) //...依次取出元素
fmt.Println("student切片:",student)
fmt.Println("student切片长度:",len(student))
fmt.Println("student切片容量:",cap(student))
}

//student切片: [Tom Ben Danny]
//student切片长度: 3
//student切片容量: 4

其中append()函数中传入的省略号代表按student切片展开,该行代码等价于:

1
student = append(student[0:2],student[3])

如果需要清空切片中的所有元素,可以把切片的开始下标和结束下标都设为0来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var student = []string{"Tom", "Ben", "Peter", "Danny"}
student = student[0:0]
fmt.Println("student切片:",student)
fmt.Println("student切片长度:",len(student))
fmt.Println("student切片容量:",cap(student))
}

//student切片: []
//student切片长度: 0
//student切片容量: 4

切片的遍历和数组类似,可以通过切片下标来进行遍历。切片下标同样从0开始,第一个元素的数组下标为0,第二个元素的数组下标为1,以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var student = []string{"Tom", "Ben", "Peter", "Danny"}
for k,v := range student {
fmt.Println("切片下标:",k,",对应元素",v)
}
}

//切片下标: 0 ,对应元素 Tom
//切片下标: 1 ,对应元素 Ben
//切片下标: 2 ,对应元素 Peter
//切片下标: 3 ,对应元素 Danny

映射(map)是一种无序的键值对的集合,map的键类似于索引,指向数据的值。当程序中需要存放有关联关系的数据时,往往就会用到map。
例如,将国家中文名和其英文名关联起来,生成如下映射:

1
2
3
4
5
country := map[string]string{
"中国":"China",
"美国":"America",
"日本":"Japan",
}

map的声明格式如下:

1
var map [键类型]值类型

建立一个学生和其成绩的对应关系,样例如下:

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
fmt.Println(studentScoreMap)
}

// map[]

声明后初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = map[string]int{
"Felix": 99,
"Tom": 98,
"jackey": 9,
}
fmt.Println(studentScoreMap)
}

// map[Felix:99 Tom:98 jackey:9]

在声明的同时初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main(){
var studentScoreMap = map[string]int{
"Tom":80,
"Felix":85,
"Peter":90,
}
fmt.Println(studentScoreMap)
}

// map[Felix:85 Peter:90 Tom:80]

使用make()函数初始化:
与切片的初始化类似,map也可以使用make()函数来进行初始化,格式如下

1
make(map[键类型]值类型,map容量)

注意:使用make()函数初始化map时可以不指定map容量,但是对于map的多次扩充会造成性能损耗。
cap()函数只能用于获取切片的容量,无法获得map的容量,因此可以通过len()函数获取map的当前长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = make(map[string]int)
studentScoreMap["Tom"]=80
studentScoreMap["Felix"]=85
studentScoreMap["Peter"]=80
fmt.Println("map长度为:",len(studentScoreMap))
fmt.Println(studentScoreMap)
}

// map长度为: 3
// map[Felix:85 Peter:80 Tom:80]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = make(map[string]int)
studentScoreMap["Tom"]=80
studentScoreMap["Felix"]=85
studentScoreMap["Peter"]=80

//遍历
for k,v := range studentScoreMap{
fmt.Println(k,v)
}
}

// Tom 80
// Felix 85
// Peter 80

只遍历键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = make(map[string]int)
studentScoreMap["Tom"]=80
studentScoreMap["Felix"]=85
studentScoreMap["Peter"]=80

//遍历
for k := range studentScoreMap{
fmt.Println(k)
}
}

// Tom
// Felix
// Peter

只遍历值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = make(map[string]int)
studentScoreMap["Tom"]=80
studentScoreMap["Felix"]=85
studentScoreMap["Peter"]=80

//遍历
for _,v := range studentScoreMap{
fmt.Println(v)
}
}

// 80
// 85
// 80

通过delete()函数来对map中的指定键值对进行删除操作,delete()函数格式如下:

1
delete(map,键)

其中map为要删除的map实例,键为map键值对中的键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
var studentScoreMap map[string]int
studentScoreMap = make(map[string]int)
studentScoreMap["Tom"]=80
studentScoreMap["Felix"]=85
studentScoreMap["Peter"]=80
delete(studentScoreMap,"Tom")
fmt.Println(studentScoreMap)
}

// map[Felix:85 Peter:80]

注意: delete()函数会直接删除指定的键值对,而不是仅仅删除键或值。
另外,Go语言没有为map提供清空所有元素的方法,想要清空map的唯一方法就是重新定义一个新的map。

对于map,我们可以定义一个键和值,然后从map中获取、变更和删除这个值。前面对map的操作都是在单协程的情况下完成的,这种情况下一般不会出现错误。如果是多个协程并发访问一个map,就有可能会导致程序异常退出,具体示例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

func readMap(Gomap map[int]int,key int) int {
return Gomap[key]
}

func writeMap(Gomap map[int]int,key int,value int){
Gomap[key] = value
}

func main() {
GoMap := make(map[int]int)
for i := 0; i < 10000; i++ {
go writeMap(GoMap,i,i)
go readMap(GoMap,i)
}
}

//fatal error: concurrent map read and map write
//fatal error: concurrent map read and map write
//fatal error: concurrent map read and map write
//fatal error: concurrent map writes
//
//goroutine 285 [running]:
//runtime.throw(0x104a70d, 0x21)
//C:/Go/src/runtime/panic.go:1116 +0x79 fp=0xc0001e7f70 sp=0xc0001e7f40 pc=0x1000f79
//runtime.mapaccess1_fast64(0x103b420, 0xc000018030, 0x8b, 0xc000092568)
//C:/Go/src/runtime/map_fast64.go:21 +0x1a7 fp=0xc0001e7f98 sp=0xc0001e7f70 pc=0xfdf027
//main.readMap(0xc000018030, 0x8b, 0x8b)
//D:/golang/src/dm2/map/binmap.go:4 +0x48 fp=0xc0001e7fc8 sp=0xc0001e7f98 pc=0x10311c8
//runtime.goexit()
//C:/Go/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc0001e7fd0 sp=0xc0001e7fc8 pc=0x102b621
//created by main.main
//D:/golang/src/dm2/map/binmap.go:15 +0x92fatal error: concurrent map read and map write
//fatal error: concurrent map read and map write
//fatal error: concurrent map read and map write
//fatal error: concurrent map writes
//
//goroutine 285 [running]:
//runtime.throw(0x104a70d, 0x21)
//C:/Go/src/runtime/panic.go:1116 +0x79 fp=0xc0001e7f70 sp=0xc0001e7f40 pc=0x1000f79
//runtime.mapaccess1_fast64(0x103b420, 0xc000018030, 0x8b, 0xc000092568)
//C:/Go/src/runtime/map_fast64.go:21 +0x1a7 fp=0xc0001e7f98 sp=0xc0001e7f70 pc=0xfdf027
//main.readMap(0xc000018030, 0x8b, 0x8b)
//D:/golang/src/dm2/map/binmap.go:4 +0x48 fp=0xc0001e7fc8 sp=0xc0001e7f98 pc=0x10311c8
//runtime.goexit()
//C:/Go/src/runtime/asm_amd64.s:1374 +0x1 fp=0xc0001e7fd0 sp=0xc0001e7fc8 pc=0x102b621
//created by main.main
//D:/golang/src/dm2/map/binmap.go:15 +0x92

从运行结果可以发现,程序异常终止,原因是出现了严重错误:多个协程在尝试对map进行同时写入。
由于map不是协程安全的,同一时刻只能有一个协程对map进行操作。最常见的解决方案就是使用sync包对map加锁或直接使用Go在1.9版本中提供的线程安全map。

加锁的本质其实就是当前协程在对map操作前需先加上锁,加锁后其他任何协程无法对map进行任何操作,直至当前协程解锁。样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"sync"
)

var lock sync.RWMutex

func readMap(Gomap map[int]int,key int) int {
lock.Lock() //读map操作前先加锁
m := Gomap[key]
lock.Unlock() //读完map后解锁
return m
}

func writeMap(Gomap map[int]int,key int,value int){
lock.Lock() //写map操作前先加锁
Gomap[key] = value
lock.Unlock() //写完map后解锁
}

func main() {
GoMap := make(map[int]int)
for i := 0; i < 10000; i++ {
go writeMap(GoMap,i,i)
go readMap(GoMap,i)
}
fmt.Println("Done")
}

// Done

由于加锁对程序性能会有一定影响,因此,如果需要在多协程情况下对map进行操作,我们推荐使用Go在1.9版本中提供的一种效率较高的并发安全的map–sync.Map。

sync.Map有以下特点:

  • 内部通过冗余的数据结构降低加锁对性能的影响。
  • 使用前无须初始化,直接声明即可。
  • sync.Map不使用map中的方式来进行读取和赋值等操作。

使用sync.Map进行替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
"fmt"
"sync"
)

var lock sync.RWMutex

func readMap(Gomap sync.Map,key int) int {
res ,ok := Gomap.Load(key) //线程安全读取
if ok == true {
return res.(int)
} else {
return 0
}

}

func writeMap(Gomap sync.Map,key int,value int){
Gomap.Store(key,value) //线程安全设置
}

func main() {
var GoMap sync.Map
for i := 0; i < 10000; i++ {
go writeMap(GoMap,i,i)
go readMap(GoMap,i)
}
fmt.Println("Done")
}

// Done

sync.Map无须使用make创建。

  • Load()方法的第一个返回值是接口类型,需要将其转换为map值的类型。
  • 目前sync.Map没有提供获取map数量的方法,解决方案是通过循环遍历map。
  • 与较普通的map相比,sync.Map为了保证并发安全,会有性能上的损失,因此在非并发情况下,推荐使用map。

总结:

  • 数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中,数组大小之后不可再变。
  • 切片表示多个同类型元素的连续集合,但是切片本身并不存储任何元素,而只是对现有数组的引用。
  • 如果切片是从其他数组或切片生成,新切片的元素添加需要考虑对原有数组或切片中数据的影响。
  • Go语言没有为删除切片元素提供方法,所以需要我们手动将删除点前后的元素连接起来,从而实现对切片中元素的删除。
  • 映射是一种无序的键值对的集合,当程序中需要存放有关联关系的数据时,往往就会用到map。
  • Go语言没有为map提供清空所有元素的方法,想要清空map的唯一方法就是重新定义一个新的map。