切片怎么变长
上一课的切片用起来像一串能变长的格子。这一课看看它里面是什么样的:切片自己不放元素,元素放在它背后的一个数组里。切片记着三样东西:从数组的哪儿开始、现在有几个元素(len)、这个数组还能放几个(cap,容量)。
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,就算容量还有空位也不行:
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 会建一个更大的新数组,把原来的元素搬过去,再放新的。一个个加,看容量怎么涨:
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 已经不是同一个数组了:
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] 不复制元素:切出来的切片指向同一个数组。改它,原来的切片也变了:
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}
base 的容量是 10,还有空位,所以两次 append 都没有搬家,都写进了同一个数组的第 3 格。先写了 1,又被 y 的 append 改成了 2。x 和 y 共用这个数组,所以都是 2。
copy:真的复制一份
想要一份互不影响的副本,先用 make 建一个一样长的切片,再用 copy 把元素抄过去。copy(目标, 来源) 返回抄了几个:
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 个以内都不用搬家:
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 算到原来数组的末尾。所以从中间切出来的切片,后面还「看得见」原来的格子:
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 就会搬到新数组。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
打折但不改原价
✓discounted 应该返回一个新切片,每个价格减 10,原来的切片不能变。现在它把原价也改了。改好它,让程序显示下面两行。
[40 20 10] [50 30 20] [89 5] [99 15]
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}
数一数搬了几次家
✓写好 moves:从空切片开始一个个 append 到 n 个元素,数一数容量变了几次(每变一次就是搬了一次家)。应该显示 5 和 8。
5 8
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}
修好它:两张清单写到一起了
✓两个人的清单都从 base 开始,各加一样。可是显示出来两张都是 鸡蛋。改好它,让它显示 [牛奶 面包 苹果] 和 [牛奶 面包 鸡蛋]。
[牛奶 面包 苹果] [牛奶 面包 鸡蛋]
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}
这一课学会了
- 切片背后是一个数组;
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。