1. 数组
在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。
Go语言的数组是值类型的,一个数组变量表示整个数组,由于数组类型是内存连续的,因此便于CPU快速寻址访问,数组值在传递过程中,是值拷贝传递,因此,为了节省内存,可以取而代之为传递指向数组的指针。
你可以理解为Go语言的数组是一种有序的struct
1.1. 声明和初始化
需要指定数组内部存储数据的类型及数组长度
// 声明包含5个int元素的数组var array [5]int// 声明并初始化array := [5]int{1, 2, 3, 4, 5}// 编译器可以自动计算数组长度array := [...]int{1, 2, 3, 4, 5}
数组初始化后,其内部存储指定类型的零值
array1 := [5]int{1:10, 2:20}
其存储结构如下图所示:
数组一旦声明,数组的长度和内部存储的值的类型即不可修改,如果想要存储更多的数组,需要先声明一个相同类型长度更长的数组,再把原来数组中的值复制到新数组中
a1 := [2]int{1, 2}// 尝试用append拓展数组a1长度会报错a1 = append(a1, 3) // error: first argument to append must be slice; have [2]int// 正确方式var a2 [3]intfor i:=0;i<len(a1);i++ {a2[i] = a1[i]}a2[2] = 3
1.2. 数组是值类型的
Go语言中的数组是值类型而非引用类型的:
package mainimport "fmt"func changeLocal(num [5]int) {num[0] = 55fmt.Println("inside function ", num)}func main() {num := [...]int{5, 6, 7, 8, 8}fmt.Println("before passing to function ", num)changeLocal(num) //num is passed by valuefmt.Println("after passing to function ", num)}// outputbefore passing to function [5 6 7 8 8]inside function [55 6 7 8 8]after passing to function [5 6 7 8 8]
2. 切片
切片是对一个数组的封装,顾名思义,它描述的是一个数组的片段,可以认为切片在Go语言内部是一个结构体,其大致结构如下:
可以表达为:
type slice struct {Length int // 切片的长度Capacity int // 切片的容量ZerothElement *byte // 该切片指向底层数组的位置(不一定必须是底层数组首部)}
2.1 切片是对底层数组的引用
切片本身不包含数据,它是对应底层数组的引用
func main(){// 底层数组aa = [1, 2, 3, 4, 5]// 切片as对数组a进行截取// ZerothElement指定为a[1]的地址// Length 指定了切片的长度,即明确截取的截止位置// Capacity 默认从a[1]开始原始数组剩余的容量as = a[1:4]}
因此,当切片类型的数据作为参数传入函数时,实际是将切片的长度、容量以及其指向底层数据的指针三个值传入了函数。所以从这个方面理解,切片也是值类型的,对切片内元素的修改,影响的是其指向的底层数组的值。
2.2 正确理解切片指向的底层数组
由于切片可以动态扩容,因此,同一个切片,其指向的底层数组可能是不一样的,来看下面两个例子
2.2.1 修改切片的数据,会影响到底层数组
这个没什么好说的,根据上面切片的定义可以知道,对切片数据的修改,直接修改的是其底层数组的元素,而不同的切片可能指向同一个底层数组,因此,对一个切片进行修改,是有可能影响到其他切片的。
func main() {// 初始化一个数组a// len=10,cap=10a := [10]int{0,1,2,3,4,5,6,7,8,9}// 切片s1指向底层数组a[1]作为切片s1的起始位置s1 := a[1:3]s2 := s1[1:5]...// 对切片s1的元素进行操作// 此时//// len=4, cap=8// len=10, cap=10fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d", s1, len(s1), cap(s1))fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d", s2, len(s2), cap(s2))fmt.Printf("a=%v, len(a)=%d, cap(a)=%d", a, len(a), cap(a))// 向切片s1追加元素s1 = append(s1,}
此时:
s1=[1,2] 其中:len=2, cap=9s2=[2,3,4,5] 其中:len=4, cap=8
- 对切片s1进行操作
func main() {...s1[1] = -1...}
此时,切片s1指向的底层数组下标为2的元素值被修改为-1,因此,切片s1和s2的对应元素的值均被修改
s1=[1,-1]s2=[-1,3,4,5]a=[0,-1,2,3,4,5,6,7,8,9]
- 向s1追加元素
func main() {...s1 = append(s1, 100)...}
由于当前s1的长度为2, 容量为8,此时向s1追加元素,相当于将s1所指向的底层数组的a[3]的值修改为100
s1 = [1,-1,100]s2 = [-1,100,4,5]a = [0,1,-1,100,4,5,6,7,8,9]
可以看出,切片s1和s2指向的是同一个底层数组a,不管是直接根据下标修改切片元素的值,还是向切片追加元素,其实修改的是底层数组的值。
2.2.2 切片是可以扩容的
当向切片追加的元素数量超过切片的剩余容量时,切片会进行扩容,而扩容的基本思路是,重新开辟一段连续内存作为切片新的底层数组,将原始底层数组的数据拷贝过来后,再在其尾部追加元素。
举个例子,承接上面的代码,对切片s2追加5个元素:
func main() {...// s1 = [1,-1,100] len=3, cap=9// s2 = [-1,100,4,5] len=4, cap=8// a = [0,1,-1,100,4,5,6,7,8,9]s2 = append(s2, []int{11,12,13,14,15}...)}
由于切片s2的剩余容量(4)不足以装载新添加的5个数据,因此,切片s2所指向的底层数组不再是原始底层数组a,切片的具体扩容规则可以参考这里。
上述操作完成后,各参数变为:
s1=[1 -1 100] len=3 cap=9s2=[-1 100 4 5 11 12 13 14 15] len=9 cap=16a=[0 1 -1 100 4 5 6 7 8 9] len=10 cap=10
这里,对切片s2的修改,并没有影响到数组a,因为s2经过扩容后,其底层数组早已不再是a
使用append对切片进行操作,需要注意的是:append总是对其第一个切片参数进行操作的。
遍历
需要进一步消化的内容
https://draveness.me/golang/keyword/golang-for-range.html
参考
快速理解Go数组和切片的内部实现原理
go in action
Go Slices: usage and internals
Golang tutorial series-Part 11: Arrays and Slices
