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 协议》,转载必须注明作者和本文链接
推荐文章: