go 复杂类型之 Slice
Slice
中文翻译为切片,容易和切片表达式的切片弄混,为了便于理解,这里统一使用Slice
来表述。
go
语言中的Slice
和其他语言中的Slice
有点不一样,go
中的Slice
是一个拥有相同类型元素的可变长度的序列。 我们先来看一下Slice
的初始化。
声明和初始化
Slice
的声明语法如下:
var name []Type
name
:Slice
的名称Type
:Slice
的元素类型
package main
import "fmt"
func def_slice() {
// 声明一个字符串类型 Slice,未初始化的 slice == nil
var a []string
// 声明一个整型 Slice 并初始化
var b = []int{}
// 声明一个布尔 Slice 并初始化
var c = []bool{false, true}
// 声明一个布尔 Slice 并初始化
//var d = []bool{false, true}
//使用make函数声明 Slice 并初始化
e := make([]int, 3)
fmt.Printf("a: %v\n", a)
fmt.Printf("b: %v\n", b)
fmt.Printf("c: %v\n", c)
fmt.Printf("e: %v\n", e)
fmt.Printf("a == nil ? %v\n", a == nil)
fmt.Printf("b == nil ? %v\n", b == nil)
fmt.Printf("c == nil ? %v\n", c == nil)
fmt.Printf("e == nil ? %v\n", e == nil)
// slice can only be compared to nil
//fmt.Println(c == d)
}
func main() {
def_slice()
}
// 输出
a: []
b: []
c: [false true]
e: [0 0 0]
a == nil ? true
b == nil ? false
c == nil ? false
e == nil ? false
和数组一样,初始化的时候可以同时给Slice
赋值,如果没有赋值,则为对应元素类型的默认零值。看完Slice
的初始化,是不是感觉Slice
和数组很像,区别也就是在声明初始化的时候不用指定元素长度。实际上Slice
一开始就是为了解决数组个数固定,和传参值拷贝开销太大的问题而设计出来的,所以,你基本上可以把Slice
看做为数组的 Plus 版本。
Slice
在设计上实际是一个复合类型,运行时表现为一个struct
,其内部结构如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
:指向底层数组中第一个可访问元素的指针。len
:Slice
的长度,即切片中的元素数目,可以使用内置的len()
函数求长度。cap
:Slice
的容量,即切片分配的底层内存空间可容纳的最大元素数目,可使用内置的cap()
函数求切片的容量。
上面的图示为一个Slice
的初始化过程,可以看到,go
运行时为Slice
创建了一个底层数组用来容纳Slice
的值。Slice
可以理解成一个对底层数组的包装,操作Slice
,实际上就是对底层的数组进行操作。
Slice
的长度很好理解,Slice
有多少元素,Slice
的长度就是多少,那为什么Slice
的容量是干嘛的呢?这就涉及到Slice
的自动扩容了。
自动扩容
go
提供了内置函数 append(slice, element...)
可以为切片动态添加元素,append()
函数会返回一个新的切片,将其存储在变量中。
上图为使用append
函数给 s1 添加一个元素的过程。可以看到,扩容后的Sclice
s1 其底层数组指向了一个新的数组 arr2,len
长度变为了 6,cap
变为 10。
原因前面已经解释过了,Slice
底层数据是存储在数组中的,而数组的长度是不可变,之前的数组长度不够了,就要创建一个更大的数组来容纳Slice
的元素。现在你理解这个cap
的什么意思了吧。
但现在你可能又会问了,当底层数组的空间不够用,go 运行时会创建一个更大的数组,那到底多大呢?我们写个代码来测试一下:
package main
import "fmt"
func sliceAppend() {
var numbers []int
for i := 0; i < 10; i++ {
// 使用append函数向numbers切片中追加元素
// 注意append不支持数组,数组是不可变长度。
numbers = append(numbers, i)
// 打印结果,注意观察切片的容量cap的变化
fmt.Printf("i=%d\tlen=%d\tcap=%d\t%v\n", i, len(numbers), cap(numbers), numbers)
}
}
func main() {
sliceAppend()
}
// 输出
i=0 len=1 cap=1 [0]
i=1 len=2 cap=2 [0 1]
i=2 len=3 cap=4 [0 1 2]
i=3 len=4 cap=4 [0 1 2 3]
i=4 len=5 cap=8 [0 1 2 3 4]
i=5 len=6 cap=8 [0 1 2 3 4 5]
i=6 len=7 cap=8 [0 1 2 3 4 5 6]
i=7 len=8 cap=8 [0 1 2 3 4 5 6 7]
i=8 len=9 cap=16 [0 1 2 3 4 5 6 7 8]
i=9 len=10 cap=16 [0 1 2 3 4 5 6 7 8 9]
在上面的代码中,声明了一个名为 numbers
的Slice
,然后使用for
循环,调用append()
函数,向Slice
中追加0-9
的数字,仔细观察cap
的值可以发现, 当Slice
容量不足以容纳更多元素时,Slice
的容量将翻倍, 即新的底层数组的长度翻倍。
那结果真的这么简单么?如果我们把上面代码中for
循环的退出条件改为1000
,你会发现,当切片容量为512
时,下一次扩容时cap
的值是848
, 并不是我们所期待的1024
。
建议拿上面的代码去www.goplay.tools/,实际运行一下,印象会更深刻。
实际上,当切片的容量超过一定数值以后,扩容将不再翻倍,而是变成一点几倍,具体的数字由运行时决定的,这样的目的是为了减少内存的浪费发生。
切片表达式
看到这里,相信你对Slice
已经有了一个基本的了解,接下来我们聊一聊切片表达式。和大多数语言一样,go
也支持切片运算符,格式如下:
s[low:high:max]
s
:数组或切片low
:数组或切片的第一个元素的下标。high
:数组或切片中的最后一个元素的位置。max
:底层数组或引用切片的最后一个位置,另外 max 可以省略,这时新切片的cap
默认等于底层数组的长度。切片表达式中的
low
和high
表示新切片的一个索引引用范围 (左包含,右不包含)。而新切片的长度len = high - low
,容量cap = max - low
。
上图是一个使用切片表达式从Slice
s1 创建Slice
s2 的过程。可以清楚的看到,s1 和 s2 共用同一个底层数组 arr,此时,修改任何一个Slice
的值都会影响到另一个,因为实际修改都是作用在底层数组上的。
package main
import (
"fmt"
)
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
fmt.Printf("s1:%v\n", s1)
fmt.Printf("len(s1):%v\n", len(s1))
fmt.Printf("cap(s1):%v\n\n", cap(s1))
s2 := s1[1:6:8]
fmt.Printf("s2:%v\n", s2)
fmt.Printf("len(s2):%v\n", len(s2))
fmt.Printf("cap(s2):%v\n\n", cap(s2))
s2[0] = 100
fmt.Printf("s1:%v\n", s1)
fmt.Printf("s2:%v\n", s2)
}
// 输出
s1:[1 2 3 4 5 6 7 8 9 10]
len(s1):10
cap(s1):10
s2:[2 3 4 5 6]
len(s2):5
cap(s2):7
s1:[1 100 3 4 5 6 7 8 9 10]
s2:[100 3 4 5 6]
这是一个非常容易踩坑的地方,一定要多多注意。
另外值得一提的是,当你的Slice
是基于切片表达式生成的,对它使用append
操作,如果底层数组空间足够的话,同样会影响到原Slice
。
package main
import (
"fmt"
)
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s2 := s1[1:6:8]
s2 = append(s2, 200)
fmt.Printf("s1:%v\n", s1)
fmt.Printf("s2:%v\n", s2)
}
// 输出
s1:[1 2 3 4 5 6 200 8 9 10]
s2:[2 3 4 5 6 200]
而当你添加的元素足够多,超出底层数组的容量范围了,根据我们前面讲到的,go 运行时会为其创建一个新的底层数组,这时候这个切片表达式生成的Slice
就和原来的底层数组“解绑”了,后续两个 Slice 就没有任何关系了。
package main
import (
"fmt"
)
func main() {
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
s2 := s1[1:6:8]
s2 = append(s2, 200, 300, 400, 500)
fmt.Printf("s1:%v\n", s1)
fmt.Printf("s2:%v\n", s2)
}
// 输出
s1:[1 2 3 4 5 6 7 8 9 10]
s2:[2 3 4 5 6 200 300 400 500]
Slice 的赋值拷贝
直接将一个切片赋值给另一个数组,前后两个切片共享底层数组,对一个切片的修改会影响另一个切片的内容。
在了解完前面的内容后,上面这句话对你来说应该是轻松理解了。
package main
import "fmt"
func main() {
s1 := make([]int, 3)
fmt.Println(s1)
s2 := s1
fmt.Println(s2)
s2[0] = 100
fmt.Println(s1)
fmt.Println(s2)
}
// 输出
[0 0 0]
[0 0 0]
[100 0 0]
[100 0 0]
因此,Go 提供了内置函数copy()
,可以迅速地将一个Slice
的数据复制到另外一个Slice
空间中。copy()
函数的使用格式如下:
copy(dst, src []Type)
- dst:目标
Slice
- src:数据来源
Slice
package main
import "fmt"
func slice_copy() {
a := []int{1, 2, 3, 4, 5}
b := make([]int, 5, 5)
c := make([]int, 3, 3)
d := make([]int, 8, 8)
copy(b, a)
copy(c, a)
copy(d, a)
fmt.Printf("Slice a:%v\n", a)
fmt.Printf("Slice b:%v\n", b)
fmt.Printf("Slice c:%v\n", c)
fmt.Printf("Slice d:%v\n", d)
fmt.Println()
c[0] = 1000
fmt.Printf("Slice a:%v\n", a)
fmt.Printf("Slice b:%v\n", b)
fmt.Printf("Slice c:%v\n", c)
fmt.Printf("Slice d:%v\n", d)
}
func main() {
slice_copy()
}
// 输出
Slice a:[1 2 3 4 5]
Slice b:[1 2 3 4 5]
Slice c:[1 2 3]
Slice d:[1 2 3 4 5 0 0 0]
Slice a:[1 2 3 4 5]
Slice b:[1 2 3 4 5]
Slice c:[1000 2 3]
Slice d:[1 2 3 4 5 0 0 0]
copy()
函数的目标Slice
需要提前声明初始化,并且一般最好和原Slice
一样大小,否则就会出现上面代码所示Slice
被截断的情况。copy()
函数产生的Slicer
和原Slice
完全不相干,改变其中一个,并不会影响另一个。
删除 Slice 中的元素
最后,Go 没有内置函数用于从Slice
中删除元素。想要删除元素,可以使用append
函数配合切片运算符 s[i:p]
来新建一个仅包含所需元素的Slice
来实现。
package main
import "fmt"
func main() {
a := []int{1, 2, 3, 4, 5}
// 删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a)
}
// 输出
[1 2 4 5]
本作品采用《CC 协议》,转载必须注明作者和本文链接
我把他理解成动态数组
另外值得一提的是,当你的 Slice 是基于切片表达式生成的,对它使用 append 操作,如果底层数组空间足够的话,同样会影响到原 Slice。
而当你添加的元素足够多,超出底层数组的容量范围了,根据我们前面讲到的,go 运行时会为其创建一个新的底层数组,这时候这个切片表达式生成的 Slice 就和原来的底层数组 “解绑” 了,后续两个 Slice 就没有任何关系了。
这就很离谱了,关联和解绑 容易触坑,要注意