1. 数组

在 Go 语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。
Go语言的数组是值类型的,一个数组变量表示整个数组,由于数组类型是内存连续的,因此便于CPU快速寻址访问,数组值在传递过程中,是值拷贝传递,因此,为了节省内存,可以取而代之为传递指向数组的指针。
你可以理解为Go语言的数组是一种有序的struct

1.1. 声明和初始化

需要指定数组内部存储数据的类型及数组长度

  1. // 声明包含5个int元素的数组
  2. var array [5]int
  3. // 声明并初始化
  4. array := [5]int{1, 2, 3, 4, 5}
  5. // 编译器可以自动计算数组长度
  6. array := [...]int{1, 2, 3, 4, 5}

数组初始化后,其内部存储指定类型的零值

  1. array1 := [5]int{1:10, 2:20}

其存储结构如下图所示:
屏幕快照 2019-06-21 上午11.28.03.png
数组一旦声明,数组的长度和内部存储的值的类型即不可修改,如果想要存储更多的数组,需要先声明一个相同类型长度更长的数组,再把原来数组中的值复制到新数组中

  1. a1 := [2]int{1, 2}
  2. // 尝试用append拓展数组a1长度会报错
  3. a1 = append(a1, 3) // error: first argument to append must be slice; have [2]int
  4. // 正确方式
  5. var a2 [3]int
  6. for i:=0;i<len(a1);i++ {
  7. a2[i] = a1[i]
  8. }
  9. a2[2] = 3

1.2. 数组是值类型的

Go语言中的数组是值类型而非引用类型的:

  1. package main
  2. import "fmt"
  3. func changeLocal(num [5]int) {
  4. num[0] = 55
  5. fmt.Println("inside function ", num)
  6. }
  7. func main() {
  8. num := [...]int{5, 6, 7, 8, 8}
  9. fmt.Println("before passing to function ", num)
  10. changeLocal(num) //num is passed by value
  11. fmt.Println("after passing to function ", num)
  12. }
  13. // output
  14. before passing to function [5 6 7 8 8]
  15. inside function [55 6 7 8 8]
  16. after passing to function [5 6 7 8 8]

2. 切片

切片是对一个数组的封装,顾名思义,它描述的是一个数组的片段,可以认为切片在Go语言内部是一个结构体,其大致结构如下:
屏幕快照 2019-06-21 下午1.48.56.png

可以表达为:

  1. type slice struct {
  2. Length int // 切片的长度
  3. Capacity int // 切片的容量
  4. ZerothElement *byte // 该切片指向底层数组的位置(不一定必须是底层数组首部)
  5. }

2.1 切片是对底层数组的引用

切片本身不包含数据,它是对应底层数组的引用

  1. func main(){
  2. // 底层数组a
  3. a = [1, 2, 3, 4, 5]
  4. // 切片as对数组a进行截取
  5. // ZerothElement指定为a[1]的地址
  6. // Length 指定了切片的长度,即明确截取的截止位置
  7. // Capacity 默认从a[1]开始原始数组剩余的容量
  8. as = a[1:4]
  9. }

因此,当切片类型的数据作为参数传入函数时,实际是将切片的长度、容量以及其指向底层数据的指针三个值传入了函数。所以从这个方面理解,切片也是值类型的,对切片内元素的修改,影响的是其指向的底层数组的值。

2.2 正确理解切片指向的底层数组

由于切片可以动态扩容,因此,同一个切片,其指向的底层数组可能是不一样的,来看下面两个例子

2.2.1 修改切片的数据,会影响到底层数组

这个没什么好说的,根据上面切片的定义可以知道,对切片数据的修改,直接修改的是其底层数组的元素,而不同的切片可能指向同一个底层数组,因此,对一个切片进行修改,是有可能影响到其他切片的。

  1. func main() {
  2. // 初始化一个数组a
  3. // len=10,cap=10
  4. a := [10]int{0,1,2,3,4,5,6,7,8,9}
  5. // 切片s1指向底层数组a[1]作为切片s1的起始位置
  6. s1 := a[1:3]
  7. s2 := s1[1:5]
  8. ...
  9. // 对切片s1的元素进行操作
  10. // 此时
  11. //
  12. // len=4, cap=8
  13. // len=10, cap=10
  14. fmt.Printf("s1=%v, len(s1)=%d, cap(s1)=%d", s1, len(s1), cap(s1))
  15. fmt.Printf("s2=%v, len(s2)=%d, cap(s2)=%d", s2, len(s2), cap(s2))
  16. fmt.Printf("a=%v, len(a)=%d, cap(a)=%d", a, len(a), cap(a))
  17. // 向切片s1追加元素
  18. s1 = append(s1,
  19. }

此时:

  1. s1=[1,2] 其中:len=2, cap=9
  2. s2=[2,3,4,5] 其中:len=4, cap=8
  • 对切片s1进行操作
  1. func main() {
  2. ...
  3. s1[1] = -1
  4. ...
  5. }

此时,切片s1指向的底层数组下标为2的元素值被修改为-1,因此,切片s1和s2的对应元素的值均被修改

  1. s1=[1,-1]
  2. s2=[-1,3,4,5]
  3. a=[0,-1,2,3,4,5,6,7,8,9]
  • 向s1追加元素
  1. func main() {
  2. ...
  3. s1 = append(s1, 100)
  4. ...
  5. }

由于当前s1的长度为2, 容量为8,此时向s1追加元素,相当于将s1所指向的底层数组的a[3]的值修改为100

  1. s1 = [1,-1,100]
  2. s2 = [-1,100,4,5]
  3. a = [0,1,-1,100,4,5,6,7,8,9]

可以看出,切片s1s2指向的是同一个底层数组a,不管是直接根据下标修改切片元素的值,还是向切片追加元素,其实修改的是底层数组的值。

2.2.2 切片是可以扩容的

当向切片追加的元素数量超过切片的剩余容量时,切片会进行扩容,而扩容的基本思路是,重新开辟一段连续内存作为切片新的底层数组,将原始底层数组的数据拷贝过来后,再在其尾部追加元素。
举个例子,承接上面的代码,对切片s2追加5个元素:

  1. func main() {
  2. ...
  3. // s1 = [1,-1,100] len=3, cap=9
  4. // s2 = [-1,100,4,5] len=4, cap=8
  5. // a = [0,1,-1,100,4,5,6,7,8,9]
  6. s2 = append(s2, []int{11,12,13,14,15}...)
  7. }

由于切片s2的剩余容量(4)不足以装载新添加的5个数据,因此,切片s2所指向的底层数组不再是原始底层数组a,切片的具体扩容规则可以参考这里
上述操作完成后,各参数变为:

  1. s1=[1 -1 100] len=3 cap=9
  2. s2=[-1 100 4 5 11 12 13 14 15] len=9 cap=16
  3. a=[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