Go语言的内置容器主要有数组(array)、切片(slice)和映射(map)。
数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中,数组大小在声明时指定后不可再变。
格式如下:
var 数组变量名 [数组长度]元素类型
// 例:
var student [3]string
var grid [4][5]bool // 二维数组
数组可以在声明时进行赋值:
var student [3]string{"Tom", "Ben", "Peter"}
可以用...
代替中括号里面的数字,Go语言编译器在编译时可以根据元素的个数来设置数组的大小
var student = [...]string{"Tom", "Ben", "Peter"}
指定index赋值
var arr = [...]int{1: 800, 0: 900, 2: 999}
fmt.Println("arr = ", arr)
二维数组初始化,第1维可以用...
推测,第2维不能用...
var arr = [...][3]int{{1}, {2, 3}}
通过for range
遍历数组时,取得的是数组里每一个元素的拷贝
arr := [...]int{1, 2, 3}
for i, ele := range arr { //ele是arr中元素的拷贝
arr[i] += 8 //修改arr里的元素,不影响ele
fmt.Printf("%d %d %d\n", i, arr[i], ele)
ele += 1 //修改ele不影响arr
fmt.Printf("%d %d %d\n", i, arr[i], ele)
}
for i := 0; i < len(arr); i++ {
fmt.Printf("%d %d\n", i, arr[i])
}
数组的长度和元素类型都是数组类型的一部分,故[10]int
和 [20]int
是不同的类型。
在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据
func testArray(x [2]int) {
fmt.Printf("func Array : %p , %v\n", &x, x)
}
func main() {
arrayA := [2]int{100, 200}
var arrayB [2]int
arrayB = arrayA
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
testArray(arrayA)
}
如果两个数组类型相同的情况下,我们可以直接通过比较运算符( ==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // true false false
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误
由于数组类型变量一旦声明后长度就固定了,这意味着我们将不能动态添加元素到数组,如果要这么做的话,需要先创建一个容量更大的数组,然后把老数组的元素都拷贝过来,最后再添加新的元素,如果数组长度很大的话,势必会影响程序性能。
另外,数组是值类型,这意味着作为参数传递到函数时,传递的是数组的值拷贝,也就是说,会先将数组拷贝给形参,然后在函数体中引用的是形参而不是原来的数组,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,这种机制带来的另一个负面影响是当数组很大时,值拷贝会降低程序性能。
综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,这就是下面要介绍的切片类型,实际上,我们在 Go 语言中很少使用
数组,大多数时候会使用切片取代它。
切片同样表示多个同类型元素的连续集合,但切片的长度是可变的,由于长度是可变的,所以可以解决数组长度在数据个数不确定情况下浪费内存的问题。
切片本身并不存储任何元素,而只是对现有数组的引用,(即slice本身没有数据,是对底层array的一个view),所以切片是引用类型。
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s := arr[2:6]
s[0] = 10
fmt.Println(arr)// [0 1 10 3 4 5 6 7]
切片是一个结构体,包含三个成员变量:指针、长度和容量。
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
go语言函数传参,传的都是值,即传切片会把切片的{arrayPointer, len, cap}这3个字段拷贝一份传进来。由于传的是底层数组的指针,所以可以直接修改底层数组里的元素。
func update_slice(s []int) {
s[0] = 888
}
s := []int{1, 2, 3}
update_slice(s)
fmt.Printf("s=%v\n", s) // [888 2 3]
切片的长度和容量都是不固定的,可以通过追加元素使切片的长度和容量增大。
切片主要有三种生成方式:
格式如下:
slice [开始位置:结束位置]
例:
var student = [...]string{"Tom", "Ben", "Peter"}
var student1 = student[1:3] // 从数组生成一个新的切片
var student2 = student1[0:1] // 从切片生成一个新的切片
fmt.Println("student数组:", student)
fmt.Println("student1切片:", student1)
fmt.Println("student2切片:", student2)
fmt.Println("student数组地址为:", &student[1]) // 这里我们取student[1]元素的地址
fmt.Println("student1切片地址为:", &student1[0])
fmt.Println("student2切片地址为:", &student2[0])
fmt.Println("student1切片长度为:", len(student1))
fmt.Println("student1切片容量为:", cap(student1))
fmt.Println("student2切片长度为:", len(student2))
fmt.Println("student2切片容量为:", cap(student2))
上面代码运行结果为:
根据运行结果,我们可以归纳出从数组或切片生成新的切片有如下特性:
var 切片变量名 []元素类型
在声明的同时初始化
var student = []string{"Tom", "Ben", "Peter"} // len = cap = 3
使用make()函数初始化
声明完切片后,可以通过内建函数make()来初始化切片,格式如下:
make ([]元素类型, 切片长度, 切片容量)
对于切片的容量应该有个大概的估值,若容量值过小,对切片的多次扩充会造成性能损耗。
例:
var student []int
student = make([]int, 2, 10) // student切片在初始化后,自动填充0值
二维切片的初始化
s2d := [][]int {
{1},{2,3}, // 二维数组每一行的长度相等,但二维切片各行的长度可以不等
}
s := make([][]int, n)
for i := 0; i < len(s); i++ {
s[i] = make([]int, n)
}
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]
可以思考一下s1的值是什么?s2的值又是什么呢?
s[i]
其中i
不可以超越len(s)
,向后扩展不可以超越底层数组cap(s)
Go语言中,我们可以使用append()函数来对切片进行元素的添加。
var student = [...]string{"Tom", "Ben", "Peter"}
var student1 = student[0:1]
fmt.Println("student数组:", student)
fmt.Println("student1切片:", student1)
student1 = append(student1, "Danny") // 对student1切片的元素添加,会覆盖引用数组对应的元素
fmt.Println("扩充Danny后的student1切片:", student1,", 切片长度为:", len(student1),
",切片容量为:",cap(student1))
fmt.Println("扩充Danny后的student数组:", student)
为student1切片添加元素会覆盖引用数组对应的元素,所以如果切片是从其他数组或切片生成,新切片的元素添加需要考虑对原有数组中的数据的影响。
当切片不能再容纳其他元素时(即当前切片长度值等于容量值),下一次使用append()函数对切片进行元素添加,系统会重新分配更大的底层数组,容量会按2倍(cap < 1024)或1.25倍(cap > 1024)进行扩充,把原内存空间的数据拷贝过来,在新内存空间上执行append操作。(具体扩容机制需要结合源码分析)
s := make([]int, 3, 5)
for i := 0; i < 3; i++ {
s[i] = i + 1
} //s=[1,2,3]
fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)
/*
capacity还够用,直接把追加的元素放到预留的内存空间上
*/
s = append(s, 4, 5) //可以一次append多个元素
fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)
/*
capacity不够用了,得申请一片新的内存,把老数据先拷贝过来,在新内存上执行append操作
*/
s = append(s, 6)
fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)
由于Go语言没有为删除切片元素提供方法,所以需要我们手动将删除元素点前后的元素连接起来,从而实现对切片中元素的删除
var student = []string{"Tom", "Ben", "Peter", "Danny"}
student = append(student[0:1], student[2:]...) // 与下一行注释代码等价
// student = append(student[0:1], student[2], student[3])
fmt.Println("student切片:", student) // [Tom Peter Danny]
fmt.Println("student切片长度:", len(student)) // 3
fmt.Println("student切片容量:", cap(student)) // 4
切片拷贝的基本语法:
copy(切片1, 切片2)
例:
srcSlice := []int{1, 2}
dstSlice := []int{6, 6, 6, 6, 6, 6}
copy(dstSlice, srcSlice)
fmt.Println("dst = ", dstSlice) // [1 2 6 6 6 6]
srcSlice = []int{1, 2}
dstSlice = []int{6, 6, 6, 6, 6, 6}
copy(srcSlice, dstSlice)
fmt.Println("src = ", srcSlice) // [6, 6]
通过上面这个程序,我们可以看出,使用copy函数进行切片拷贝时,拷贝的长度为两个slice中长度较小的长度值。
映射是一种无序
的键值对的集合,map的键类似于索引,指向数据的值。
格式如下:
var map [键类型]值类型
键的类型,必须是支持==和!=操作符的类型,映射、切片、函数以及包含上述三种类型的结构体不能作为map的键,使用这些类型会导致编译错误。
获取元素:m[key]。
在声明的同时初始化
var studentScoreMap = map[string]int {
"Tom" : 80,
"Ben" : 85,
"Peter" : 90,
}
使用make()函数初始化
make(map[键类型]值类型, map容量)
使用make()函数初始化map时可以不指定map容量,但是对于map的多次扩充会造成性能损耗。
字典中不能使用cap()函数,只能使用len()函数,len()函数返回map拥有的键值对的数量。
Go语言通过delete()函数来对map中的指定键值对进行删除操作。格式如下:
delete(map实例,键)
Go语言没有为map提供清空所有元素的方法。
使用range遍历key,value对,或者单独遍历key:
m1 := map[int]string{1:"Luffy", 2:"Sanji"}
// 遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
// 1 ----> Luffy
// 2 ----> Sanji
}
// 遍历2,第一个返回值是key,第二个返回值是value(可省略)
for k := range m1 {
fmt.Printf("%d ----> %s\n", k, m1[k])
// 1 ----> Luffy
// 2 ----> Sanji
}
map的遍历顺序是随机的,所以每次遍历的结果可能都不相同。如需特定顺序,需手动对key或者value进行排序。
我们已经知道 Go 语言的字典是一个无序集合,如果你想要对字典进行排序,可以通过分别为字典的键和值创建切片,然后通过对切片进行排序来实现。
testMap := map[string]int{"three": 3, "two": 2, "one": 1}
keys := make([]string, 0)
for k := range testMap {
keys = append(keys, k)
}
sort.Strings(keys) // 对键进行排序
fmt.Println("Sorted map by key:")
for _, k := range keys {
fmt.Println(k, testMap[k])
}
testMap := map[string]int{"three": 3, "two": 2, "one": 1}
values := make([]int, 0)
for _, v := range testMap {
values = append(values, v)
}
// 键值对调
invMap := make(map[int]string, 3)
for k, v := range testMap {
invMap[v] = k
}
sort.Ints(values) // 对值进行排序
fmt.Println("Sorted map by value:")
for _, v := range values {
fmt.Println(invMap[v], v)
}