变量和常量虽能存储数据,但是在编写一些逻辑稍复杂的程序中,往往需要存储更多、更复杂且不同类型的数据,这些数据一般存储在Go语言的内置容器中。
Go语言的内置容器主要有数组、切片和映射。
下面详细介绍以上三种内置容器的特点和使用方法,学习目标:在编程中能使用恰当的容器存储数据并对其进行增加、删除和修改等操作。
- 数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中。
- 数组中存放的元素类型可以是整型、字符串或其他自定义类型。数组在使用前需先声明,声明时必须指定数组的大小且数组大小之后不可再变。
- 数组元素可以通过数组下标来读取或修改,数组下标从0开始,第一个元素的数组下标为0,第二个元素的数组下标为1,以此类推。
数组声明格式如下:
例如,声明数组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) }
|
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) a = append(a, 1, 2, 3) a = append(a,[]int{11,11,11}...) a = append([]int{22,22,22},a...) a = append(a[:0], append([]int{1,2,3}, a[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:] a = a[N:] fmt.Println(a)
a = []int{1, 2, 3} a = append(a[:0], a[1:]...) a = append(a[:0], a[N:]...)
a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1:]...) a = append(a[:i], a[i+N:]...) a = a[:i+copy(a[i:], a[i+1:])] a = a[:i+copy(a[i:], a[i+N:])] }
func main() { a = []int{1, 2, 3, ...} a = append(a[:i], a[i+1:]...) a = append(a[:i], a[i+N:]...) a = a[:i+copy(a[i:], a[i+1:])] a = a[:i+copy(a[i:], a[i+N:])] }
func main() { a = []int{1, 2, 3} a = a[:len(a)-1] a = a[:len(a)-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) } }
|
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) } }
|
相对于数组,切片(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]) fmt.Println("student1切片地址为:",&student1[0]) fmt.Println("student1切片长度为:",len(student1)) fmt.Println("student1切片容量为:",cap(student1)) }
|
根据运行结果,我们可以归纳出从数组或切片生成新的切片有如下特性:
- 新生成的切片长度:结束位置-开始位置。
- 新生成的切片取出的元素不包括结束位置对应的元素。
- 新生成的切片是对现有数组或切片的引用,其地址与截取的数组或切片开始位置对应的元素地址相同。
- 新生成的切片容量指从切片的起始元素开始到其底层数组中的最后一个元素的个数。(所以为什么切片的长度显示为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的最后一个元素也取到,student1切片的结束位置设为了3。(包前不包后)
- 根据结果,可以发现student2切片仍然是对底层数组student的引用。
- 另外,也可以通过slice[:]来表示切片本身。
声明切片
切片的声明格式如下:
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) }
|
从运行结果可以知道,切片声明后其内容为空,长度和容量均为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) }
|
使用make()函数初始化
声明完切片后,可以通过内建函数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值且不再为空。
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 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"} var student1 = student[0:1] fmt.Println("student数组:",student) fmt.Println("student1切片:",student1) fmt.Println("student1切片:",student1,",切片长度为:",len(student1),",切片容量为:",cap(student1)) student1 = append(student1,"Danny") fmt.Println("扩充Danny后的student1切片:",student1,",切片长度为:",len(student1),",切片容量为:",cap(student1)) fmt.Println("扩充Danny后的student数组:",student) }
|
由于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)) }
|
其中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)) }
|
切片的遍历和数组类似,可以通过切片下标来进行遍历。切片下标同样从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) } }
|
映射(map)是一种无序的键值对的集合,map的键类似于索引,指向数据的值。当程序中需要存放有关联关系的数据时,往往就会用到map。
例如,将国家中文名和其英文名关联起来,生成如下映射:
1 2 3 4 5
| country := map[string]string{ "中国":"China", "美国":"America", "日本":"Japan", }
|
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) }
|
声明后初始化:
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) }
|
在声明的同时初始化:
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) }
|
使用make()函数初始化:
与切片的初始化类似,map也可以使用make()函数来进行初始化,格式如下
注意:使用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) }
|
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) } }
|
只遍历键:
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) } }
|
只遍历值:
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) } }
|
通过delete()函数来对map中的指定键值对进行删除操作,delete()函数格式如下:
其中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) }
|
注意: 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) } }
|
从运行结果可以发现,程序异常终止,原因是出现了严重错误:多个协程在尝试对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() m := Gomap[key] lock.Unlock() return m }
func writeMap(Gomap map[int]int,key int,value int){ lock.Lock() Gomap[key] = value lock.Unlock() }
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") }
|
由于加锁对程序性能会有一定影响,因此,如果需要在多协程情况下对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") }
|
sync.Map无须使用make创建。
- Load()方法的第一个返回值是接口类型,需要将其转换为map值的类型。
- 目前sync.Map没有提供获取map数量的方法,解决方案是通过循环遍历map。
- 与较普通的map相比,sync.Map为了保证并发安全,会有性能上的损失,因此在非并发情况下,推荐使用map。
总结:
- 数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中,数组大小之后不可再变。
- 切片表示多个同类型元素的连续集合,但是切片本身并不存储任何元素,而只是对现有数组的引用。
- 如果切片是从其他数组或切片生成,新切片的元素添加需要考虑对原有数组或切片中数据的影响。
- Go语言没有为删除切片元素提供方法,所以需要我们手动将删除点前后的元素连接起来,从而实现对切片中元素的删除。
- 映射是一种无序的键值对的集合,当程序中需要存放有关联关系的数据时,往往就会用到map。
- Go语言没有为map提供清空所有元素的方法,想要清空map的唯一方法就是重新定义一个新的map。