背景
切片的复制是一个容易出错的操作,先看几个的例子:
go
func main() {
listing1()
listing2()
listing3()
}
func listing1() {
s := []int{1, 2, 3}
f(s[:2])
fmt.Println(s) // output: [1,2,10]
}
func listing2() {
s := []int{1, 2, 3}
sCopy := make([]int, 2)
copy(sCopy, s)
f(sCopy)
fmt.Println(sCopy) // output: [1,2]
result := append(sCopy, s[2])
fmt.Println(result) // output: [1,2,3]
}
func listing3() {
s := []int{1, 2, 3}
f(s[:2:2])
fmt.Println(s) // output: [1,2,3]
}
func f(s []int) []int {
return append(s, 10)
}
-
listing1 原切片 s 的 len=3,然后创建了一个 len=2 的临时切片传入 f,当 f 对它 append 新数据后,原切片 s 中的元素被覆盖了
-
listing2 将原切片复制到一个 len=2 的新切片 sCopy,将 sCopy 传入 f,这时 f 中的操作没有影响外面的原切片 sCopy
-
listing3 创建一个 len=2, cap=2 的临时切片传入 f,f 中的操作同样没用影响外面的原切片 s
为什么 listing1 中的原始数据会被覆盖,listing2,listing3 则不会?
知识点
知识点1:切片的数据结构
go
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice 的底层其实是借助数组实现的,数据都保存在数组中。 array 就是指向这个数组的指针,len 代表切片的长度,cap 代表切片的容量。
知识点2:append 操作发现切片容量不足,会创建一个新的底层数组,同时把原数组的内容复制到新数组,再更新切片的 array 指针指向新数组,len,cap 都更新为新数组的长度和容量。
知识点3:go 函数采用值复制(pass-by-value),函数执行前会把参数复制一份,函数体中操作的是复制后的数据。对于指针来说,复制并不会复制底层数据,所以复制后新旧指针指向的是同一份数据。
回看开头的例子
listing1
go
func listing1() {
s := []int{1, 2, 3}
f(s[:2])
fmt.Println(s) // output: [1,2,10]
}
func f(s []int) []int {
return append(s, 10)
}
f 执行 append 操作前,两个切片的数据结构如图,:
lua
s (main) s (in f)
+-------+ +-------+
| ptr |--------> +---+---+---+ <---| ptr |
| len | 3 | 1 | 2 | 3 | | len | 2
| cap | 3 +---+---+---+ | cap | 3
+-------+ ▲ +-------+
|
+-- Shared starting point
执行 append 操作,因为 f 中切片的 len=2,cap=3,go 判断底层数组可以容纳新元素,于是在原数组添加。这导致原数组的最后一个元素就被覆盖了:
lua
s (main) s (in f)
+-------+ +-------+
| ptr |--------> +---+---+---+ <---| ptr |
| len | 3 | 1 | 2 | 10| | len | 3
| cap | 3 +---+---+---+ | cap | 3
+-------+ ▲ +-------+
|
+-- Shared starting point
listing2
go
func listing2() {
s := []int{1, 2, 3}
sCopy := make([]int, 2)
copy(sCopy, s)
f(sCopy)
fmt.Println(sCopy)
result := append(sCopy, s[2])
fmt.Println(result)
}
func f(s []int) []int {
return append(s, 10)
}
首先 s 和 sCopy 是两个独立的切片,它们的底层数组不同,当执行 copy(sCopy, s)
后,数据结构如图:
lua
s (main) sCopy (main)
+-------+ +-------+
| ptr |--------> +---+---+---+ | ptr |--------> +---+---+
| len | 3 | 1 | 2 | 3 | | len | 2 | 1 | 2 | <-- 独立数组2
| cap | 3 +---+---+---+ | cap | 2 +---+---+
+-------+ ^ +-------+ ^
| |
+-- 独立数组1 +-- 独立数组2
f(sCopy)
会创建一个副本:
lua
sCopy (main) sCopy (in f)
+-------+ +-------+
| ptr |--------> +---+---+ <-------| ptr |
| len | 2 | 1 | 2 | | len | 2
| cap | 2 +---+---+ | cap | 2
+-------+ ^ +-------+
|
+-- 共享的底层数组
执行 append 操作,因为 f 中切片的 len=2, cap=2,不够添加。于是会创建一个新的底层数组,并把数据复制过去:
lua
(append 返回的新切片)
+-------+
(sCopy in f) | ptr |---------> +---+---+---+---+
+-------+ | len | 3 | 1 | 2 | 10| 0 | <-- append后创建的新数组
| ptr |---> +---+---+ | cap | 4 +---+---+---+---+
| len | 2 | 1 | 2 | +-------+
| cap | 2 +---+---+
+-------+ ^
|
原始的底层数组
因为 f 中的操作是基于新数组,所以 main 中的原数组不受影响。main 中 fmt.Println(sCopy)
输出的仍是 [1,2]。result := append(sCopy, s[2])
在原数组基础上创建新数组,添加新元素,输出[1,2,3]。
listing3
go
func listing3() {
s := []int{1, 2, 3}
f(s[:2:2])
fmt.Println(s)
}
func f(s []int) []int {
return append(s, 10)
}
s[:2:2]
的意思是基于原切片,创建一个 len=2,cap=2 的新切片:
lua
s (in main) s (in f)
+-------+ +-------+
| ptr |---+-------------+-| ptr |
| len | 3 | | | len | 2
| cap | 3 | | | cap | 2
+-------+ | | +-------+
| |
v |
+---+---+---+ <---+
| 1 | 2 | 3 | <-- 共享的底层数组
+---+---+---+
append 操作,因为此时 f 中的切片 cap=2,不够容纳新元素,go 会分配一个新数组:
lua
s (in f) (append 返回的新切片)
+-------+ +-------+
| ptr |--------> +---+---+---+ | ptr |--------> +---+---+---+---+
| len | 2 | 1 | 2 | 3 | | len | 3 | 1 | 2 | 10| 0 | <-- 新分配的底层数组
| cap | 2 +---+---+---+ | cap | 4 +---+---+---+---+
+-------+ +-------+
^
|
共享的底层数组
因为 f 中的操作是基于新数组,所以 main 中的原数组不受影响。fmt.Println(s)
输出仍是[1,2,3]。
总结
-
假如新旧切片都指向同一个底层数组,且新切片的 len < cap。append 操作会修改原切片的底层数组
-
要想避免影响,可以通过 copy 复制一个新数组,或者用 full slice expression 限制 cap,两者都会在 append 操作时创建一个新底层数组