Kodewick
开始学 →
第 7 课,共 24 课目录

切片怎么变长

第 7 课,共 24 课 · 第 2 部分 · 数据 · 约 15 分钟

上一课的切片用起来像一串能变长的格子。这一课看看它里面是什么样的:切片自己不放元素,元素放在它背后的一个数组里。切片记着三样东西:从数组的哪儿开始、现在有几个元素(len)、这个数组还能放几个(cap,容量)。

prog.go
1package main2 3import "fmt"4 5func main() {6	s := make([]int, 3, 5)7	fmt.Println(s, len(s), cap(s))8	s = append(s, 7)9	fmt.Println(s, len(s), cap(s))10}
输出
[0 0 0] 3 5
[0 0 0 7] 4 5

make([]int, 3, 5) 建了一个能放 5 个的数组,切片现在用了前 3 个。append 一个,长度变成 4,容量还是 5:数组里本来就有空位。

len 和 cap

len 是你能用下标读写的元素个数,cap 是背后那个数组从切片开头算起一共有几格。下标只能到 len-1,就算容量还有空位也不行:

prog.go
1package main2 3import "fmt"4 5func main() {6	s := make([]int, 2, 10)7	fmt.Println(len(s), cap(s))8	s[1] = 59	s[2] = 910	fmt.Println(s)11}
输出
2 10
panic: runtime error: index out of range [2] with length 2

goroutine 1 [running]:
main.main(...)
	/tmp/sandbox/prog.go:9 +0x1d
exit status 2

发生了什么: 下标 2 越界了:只有 2 个元素,下标只能是 0~1。

index out of range [2] with length 2:报错说的是长度,不是容量。空位要用 append 才用得上。

容量不够就搬家

append 的时候,数组里有空位,就放进空位;没有空位,Go 会建一个更大的新数组,把原来的元素搬过去,再放新的。一个个加,看容量怎么涨:

prog.go
1package main2 3import "fmt"4 5func main() {6	var nums []int7	for i := range 10 {8		nums = append(nums, i)9		fmt.Println("len", len(nums), "cap", cap(nums))10	}11}
输出
len 1 cap 1
len 2 cap 2
len 3 cap 4
len 4 cap 4
len 5 cap 8
len 6 cap 8
len 7 cap 8
len 8 cap 8
len 9 cap 16
len 10 cap 16

容量是 1、2、4、8、16:不够时大约翻一倍(切片很长以后涨得慢一些)。所以大多数 append 不用搬家,很快。点单步,看格子:容量变的那几步,切片换到了一排新的格子上。

搬家以后各走各的

正因为会搬家,append 的结果一定要赋回去。也因为会搬家,下面的 a 和 b 已经不是同一个数组了:

prog.go
1package main2 3import "fmt"4 5func main() {6	a := []int{1, 2, 3}7	b := append(a, 4)8	b[0] = 1009	fmt.Println("a:", a, cap(a))10	fmt.Println("b:", b, cap(b))11}
输出
a: [1 2 3] 3
b: [100 2 3 4] 6

a 的容量是 3,已经满了,所以 append 把元素搬到了一个新数组(容量 6),b 用的是新的那个。改 b[0] 不影响 a。点单步,看 b := append(a, 4) 这一步,b 的格子是新的一排。

几个切片共用一个数组

切一段 s[a:b] 不复制元素:切出来的切片指向同一个数组。改它,原来的切片也变了:

prog.go
1package main2 3import "fmt"4 5func main() {6	scores := []int{90, 75, 88, 62}7	firstTwo := scores[0:2]8	firstTwo[1] = 809	fmt.Println("firstTwo:", firstTwo)10	fmt.Println("scores:", scores)11}
输出
firstTwo: [90 80]
scores: [90 80 88 62]

firstTwo[1] 和 scores[1] 是同一格。点单步,看两个切片指着同一排格子。

这是一个常见的坑:容量还有空位时,append 也会写进共用的数组。

先猜一猜

这段程序显示什么?

1package main2 3import "fmt"4 5func main() {6	base := make([]int, 3, 10)7	x := append(base, 1)8	y := append(base, 2)9	fmt.Println(x[3], y[3])10}

copy:真的复制一份

想要一份互不影响的副本,先用 make 建一个一样长的切片,再用 copy 把元素抄过去。copy(目标, 来源) 返回抄了几个:

prog.go
1package main2 3import "fmt"4 5func main() {6	prices := []int{12, 30, 8}7	backup := make([]int, len(prices))8	n := copy(backup, prices)9	prices[0] = 010	fmt.Println("抄了", n, "个")11	fmt.Println("prices:", prices)12	fmt.Println("backup:", backup)13}
输出
抄了 3 个
prices: [0 30 8]
backup: [12 30 8]

copy 只抄两边都放得下的那几个:目标的长度是 0,就一个也不抄。所以 make 的长度要给够。

小技巧: slices.Clone(prices) 一步就能复制一份,要 import "slices"。

预先留好位置

知道大概要放多少个,可以用 make([]int, 0, 100):长度 0,容量 100。之后一直 append,在 100 个以内都不用搬家:

prog.go
1package main2 3import "fmt"4 5func main() {6	orders := make([]int, 0, 100)7	for i := range 100 {8		orders = append(orders, i+1)9	}10	fmt.Println(len(orders), cap(orders))11	fmt.Println(orders[:5])12}
输出
100 100
[1 2 3 4 5]

长度从 0 开始,所以用 append 加,不用下标。

切一段以后的 cap

s[a:b] 的长度是 b-a;它的容量从 a 算到原来数组的末尾。所以从中间切出来的切片,后面还「看得见」原来的格子:

prog.go
1package main2 3import "fmt"4 5func main() {6	week := []int{1, 2, 3, 4, 5, 6, 7}7	mid := week[2:4]8	fmt.Println(mid, len(mid), cap(mid))9	mid = append(mid, 0)10	fmt.Println(week)11}
输出
[3 4] 2 5
[1 2 3 4 0 6 7]

mid 的容量是 5(下标 2 到 6),所以 append 没有搬家,直接改了 week[4]。不想让它碰到后面的格子,可以写 week[2:4:4]:第三个数是容量到哪儿为止,这样 append 就会搬到新数组。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。

练习 1

打折但不改原价

discounted 应该返回一个新切片,每个价格减 10,原来的切片不能变。现在它把原价也改了。改好它,让程序显示下面两行。

要显示
[40 20 10] [50 30 20]
[89 5] [99 15]
exercise1.go
1package main2 3import "fmt"4 5func discounted(prices []int) []int {6	for i := range prices {7		prices[i] -= 108	}9	return prices10}11 12func main() {13	a := []int{50, 30, 20}14	fmt.Println(discounted(a), a)15	b := []int{99, 15}16	fmt.Println(discounted(b), b)17}
输出
点「运行」或按 Ctrl+Enter
练习 2

数一数搬了几次家

写好 moves:从空切片开始一个个 append 到 n 个元素,数一数容量变了几次(每变一次就是搬了一次家)。应该显示 5 和 8。

要显示
5
8
exercise2.go
1package main2 3import "fmt"4 5func moves(n int) int {6	var s []int7	count := 08	_ = s // 先占个位,免得 s 没用到;写好循环就可以删掉9	return count10}11 12func main() {13	fmt.Println(moves(10))14	fmt.Println(moves(100))15}
输出
点「运行」或按 Ctrl+Enter
练习 3

修好它:两张清单写到一起了

两个人的清单都从 base 开始,各加一样。可是显示出来两张都是 鸡蛋。改好它,让它显示 [牛奶 面包 苹果] 和 [牛奶 面包 鸡蛋]。

要显示
[牛奶 面包 苹果]
[牛奶 面包 鸡蛋]
exercise3.go
1package main2 3import "fmt"4 5func main() {6	base := make([]string, 0, 5)7	base = append(base, "牛奶", "面包")8	anna := append(base, "苹果")9	ben := append(base, "鸡蛋")10	fmt.Println(anna)11	fmt.Println(ben)12}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 切片背后是一个数组;len 是能用的元素个数,cap 是数组从切片开头算起有几格。
  • append 有空位就放进去;没空位就搬到更大的新数组([]int 的容量 1、2、4、8、16…)。所以结果一定要赋回去。
  • 切一段 s[a:b] 不复制:几个切片可以共用一个数组,改一个,另一个也变。容量有空位时,append 也会改到共用的格子。
  • copy(dst, src) 抄元素,dst 要先 make 够长;slices.Clone 一步复制。
  • make([]int, 0, n) 预先留好位置,append 时不用搬家。
  • s[a:b] 的容量从 a 算到数组末尾;s[a:b:c] 的容量只到下标 c 为止,也就是 c-a。
这一课学完了吗?点一下标记。进度存在这个浏览器里,随时可以撤销。