几个常见的 slice 错误
共 1663字,需浏览 4分钟
·
2022-02-20 09:36
最近看到 medium 上有篇文章[1]把关于 slice 的常见错误总结出来了,有些甚至是老司机也容易犯的。每个错误都先描述问题,再给出修改建议,最后再展示一个代码样例。
之前饶大写过一篇关于 slice 的文章《深度解密 Go 语言之 Slice》,如果看懂了,很多相关的问题都能理解。
新旧 slice 共用底层数组问题
如果我们用类似 b := a[:3]
这样的方式基于 a 创建一个新的 slice,a 和 b 这时指向同一个底层数组。如果对 a 进行的一些操作,影响到了底层数组,最后也会影响到 b。这个隐蔽的 bug 可能会耗费你不少时间来排查。
修复
解决办法很简单,从老 slice 拷贝一个新 slice,这样对老 slice 的修改就不会影响新 slice。
demo
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
b := a[4:7]
fmt.Printf("before sorting a, b = %v\n", b) // before sorting a, b = [5 6 7]
sort.Slice(a, func(i, j int) bool {
return a[i]> a[j]
})
// b is not [5,6,7] anymore. If we code something to use [5,6,7] in b, then
// there can be some unpredicted behaviours
fmt.Printf("after sorting a, b = %v\n", b) // after sorting a, b = [5 4 3]
// Fix:
// To avoid that, that part of the slice should be copied to a different slice.
// Then that values are in different underlying array and changes to a will
// not be affected to that
c := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
d := make([]int, 3)
copy(d, c[4:7])
fmt.Printf("before sorting c, d = %v\n", d) // before sorting c, d = [5 6 7]
sort.Slice(c, func(i, j int) bool {
return c[i]> c[j]
})
fmt.Printf("after sorting c, d = %v\n", d) //after sorting c, d = [5 6 7]
}
view raw
将循环变量取址赋给 slice问题
如果一个 slice 里面的元素是指针类型,当我们在遍历另一个 slice 的过程中将循环变量取址后 append 到这个指针类型的 slice,那么每次 append 的是其实是同一个元素。这是因为在整个循环的过程中,循环变量是同一个,对它的取址当然也是一样的。
修复
将循环变量赋值给一个新变量,将新变量取址后 append 到这个指针类型的 slice。
demo
package main
import "fmt"
func main() {
a := make([]*int, 0)
// simplest scenario of the mistake. create *int slice and
// put elements in a loop using iterator variable's pointer
for i := 0; i < 3; i++ {
a = append(a, &i)
}
// all elements have same pointer value and value is the last value of
// the iterator variable because i is the same variable throughout the loop
fmt.Printf("a = %v\n", a) // a = [0xc000018058 0xc000018058 0xc000018058]
fmt.Printf("a[0] = %v, a[1] = %v, a[2] = %v\n\n", *a[0], *a[1], *a[2])
// a[0] = 3, a[1] = 3, a[2] = 3
type A struct {
a int
}
b := []A{
{a: 2},
{a: 4},
{a: 6},
}
// append pointer to iteration variable a and it's memory address is same
// through out the loop so all the elements will append same pointer and value
// is the last value of the loop because a is the same variable throughout
// the loop
aa := make([]*A, 0)
for _, a := range b {
aa = append(aa, &a)
}
fmt.Printf("aa = %v\n", a) // aa = [0xc000018058 0xc000018058 0xc000018058]
fmt.Printf("aa[0] = %v, aa[1] = %v, aa[2] = %v\n\n", *aa[0], *aa[1], *aa[2])
// aa[0] = {6}, aa[1] = {6}, aa[2] = {6}
// Fix:
// To avoid that iteration value should be copied to a different variable
// and pointer to that should be appended
bb := make([]*A, 0)
for _, a := range b {
a := a
bb = append(bb, &a)
}
fmt.Printf("bb = %v\n", a) // bb = [0xc000018058 0xc000018058 0xc000018058]
fmt.Printf("bb[0] = %v, bb[1] = %v, bb[2] = %v\n", *bb[0], *bb[1], *bb[2])
// bb[0] = {2}, bb[1] = {4}, bb[2] = {6}
}
当函数参数是 slice 时,执行 append问题
当一个函数的参数(形参)是 slice 时,如果在函数内部向这个 slice append 元素,那么原始的 slice(实参)将不受影响。因为 append 之后会形成一个新的 slice,原 slice 不会变。
修复
- 将形参 slice,改成指针类型:*slice,并且将 append 之后得到的 slice 赋给这个指针。
- 或者将新 slice 通过返回值返回后,将它赋给原来的那个 slice。
demo
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
fmt.Printf("before append, a = %v\n", a ) // before append, a = [1 2 3]
// a is not changed because append returns a new slice with appended elements
myAppend(a, 4)
fmt.Printf("after append, a = %v\n", a ) // after append, a = [1 2 3]
// Fix:
// to fix this, use pointer to slice to append in separate function or
// get returned appended slice from that function
myAppend2(&a, 4)
fmt.Printf("after append with pointer, a = %v\n", a )
// after append with pointer, a = [1 2 3 4]
a = myAppend3(a, 5)
fmt.Printf("after append with return, a = %v\n", a )
// after append with return, a = [1 2 3 4 5]
}
func myAppend(a []int, i int) {
a = append(a, i)
}
func myAppend2(a *[]int, i int) {
*a = append(*a, i)
}
func myAppend3(a []int, i int) []int {
a = append(a, i)
return a
}
用 range 遍历 slice 时企图改变元素值问题
当我们在遍历一个 slice 时,如果想通过循环变量需要改变元素值。因为循环变量只是 slice 元素的一个拷贝,修改循环变量并不能影响原来的 slice。
修复
想要修改原 slice,用切片下标来访问 slice 元素并做修改。
demo
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
fmt.Printf("before adding 1 to elements, a = %v \n", a)
// before adding 1 to elements, a = [1 2 3]
for _, n := range a {
n += 1
}
// slice elements haven't changed because n is a copy of slice elements.
fmt.Printf("after adding 1 to elements, a = %v \n", a)
// after adding 1 to elements, a = [1 2 3]
// Fix:
// to change that address the elements with the index and it should be changed
for i, _ := range a {
a[i] += 1
}
fmt.Printf("after adding 1 to elements with index, a = %v \n", a)
// after adding 1 to elements with index, a = [2 3 4]
}
向相同 slice append 元素来构建不同 slice问题
如果想通过每次向原 slice append 不同的元素,从而创建出多个 slice。假如原 slice 的容量恰好够用,那么这些新创建的 slice 和最后创建出来的 slice 内容相同。
修复
明确指定长度来创建一个新 slice,并使用 copy 将原 slice 拷贝到新 slice。之后,将元素 append 到新 slice。
demo
package main
import "fmt"
func main() {
a := make([]int, 3, 10)
a[0], a[1], a[2] = 1, 2, 3
b := append(a, 4)
c := append(a, 5)
// c == b because both refer to same underlying array and capacity of that is 10
// so appending to a will not create new array.
fmt.Printf("b = %v \n", b) // b = [1 2 3 5]
fmt.Printf("c = %v \n\n", c) // c = [1 2 3 5]
// fix:
// to avoid this, a should be copied to b and c and then append
b = make([]int, 3)
copy(b, a)
b = append(b, 4)
c = make([]int, 3)
copy(c, a)
c = append(c, 5)
fmt.Printf("after copy\n")
fmt.Printf("b = %v \n", b) // b = [1 2 3 4]
fmt.Printf("c = %v \n", c) // c = [1 2 3 5]
}
使用内建的 copy 函数向一个空的 slice 里拷贝元素问题
向一个空的 slice 里面拷贝元素什么也不会发生。拷贝时,只有 min(len(a), len(b))
个元素会被成功拷贝。
修复
想从原 slice 拷贝多少个元素过来,就先创建一个指定长度的 slice,再执行拷贝。
demo
package main
import "fmt"
func main() {
a := []int{2, 4, 6}
// b has no any elements of a because copy copies
// min(len(a), len(b)) number of elements
b := make([]int, 0)
n := copy(b, a)
fmt.Printf("b = %v\n", b) // b = []
fmt.Printf("%d elements copied from a to b\n\n", n)
// 0 elements copied from a to b
//Fix: make slice with a length that number of elements need to be copied.
c := make([]int, 2)
n = copy(c, a)
fmt.Printf("c = %v\n", c) // c = [2 4]
fmt.Printf("%d elements copied from a to c\n\n", n)
// 2 elements copied from a to c
}
可以看到,如果对之前的那篇文章有足够的理解,这些错误一眼就能看出原因。不过理解是一回事,平时在工作中其实也难免写出有问题的代码来。多看看类似的陷阱总会有好处。
参考资料
[1]文章: https://medium.com/@nsspathirana/common-mistakes-with-go-slices-95f2e9b362a9