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]int
for i:=0;i<len(a1);i++ {
a2[i] = a1[i]
}
a2[2] = 3
1.2. 数组是值类型的
Go语言中的数组是值类型而非引用类型的:
package main
import "fmt"
func changeLocal(num [5]int) {
num[0] = 55
fmt.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 value
fmt.Println("after passing to function ", num)
}
// output
before 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(){
// 底层数组a
a = [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=10
a := [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=10
fmt.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=9
s2=[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=9
s2=[-1 100 4 5 11 12 13 14 15] len=9 cap=16
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