likes
comments
collection
share

Go语言中常见100问题-#25 append操作陷阱与解决方法

作者站长头像
站长
· 阅读数 4

前言

append函数是Go语言中操作切片高频函数,但是append函数如果使用不当在某些情况下将会产生意料之外的结果,本文将通过案例详细分析问题原因及解决方法。

案例引入

下面来看一个具体的例子,先初始化一个切片s1,基于s1创建切片s2,最后对s2执行append操作创建切片s3.

s1 := []int{1, 2, 3}
s2 := s1[1:2]
s3 := append(s2, 10)

运行上述程序后,你知道s1、s2和s3的状态是什么样的吗?先不要看下面的分析,看看自己是否答对。

原因分析

下面通过图解的形式进行说明,在执行第二条语句后,s1和s2在内存中的状态如下图所示。

Go语言中常见100问题-#25 append操作陷阱与解决方法

s1是一个长度为3,容量为3的切片,s2是一个长度为1,容量为2的切片,s1和s2底层共用同一个数组。通过append向切片里面添加元素的时候,会检查切片是否满了(length==capacity). 如果没有满,则底层数组的下一个索引位置添加元素,并将切片的长度加1. 在本例中,切片s2还未满,所以对它执行append操作后,s3在内存中的状态如下。

Go语言中常见100问题-#25 append操作陷阱与解决方法

底层数组中的最后一个元素的值被更新为10,打印s1、s2和s3得到输出内容如下。切片s1的内容被更新了,尽管我们没有直接对s1[2]或者s2[1]进行赋值操作。需要牢记这种操作可能引发意料之外的结果。

s1=[1 2 10], s2=[2], s3=[2 10]

切片传给函数后进行append操作会引发什么问题?

下面来看将切片传递给函数产生的效果。切片s有3个元素,然后截取前2个值传给函数f. 如果在函数f中更新s的值,main函数中也会感知到s的变化。

func main() {
    s := []int{1, 2, 3}
    f(s[:2])
    // Use s
}

func f(s []int) {
    // Update s
}

如果在函数f中执行append操作,将会更新s中的第三个元素,尽管传递给f的只是前两个值。例如下面的程序,main函数中打印s的值,输出的是 [1 2 10]

func main() {
    s := []int{1, 2, 3}
    f(s[:2])
    fmt.Println(s) // [1 2 10]
}

func f(s []int) {
    _ = append(s, 10)
}

如果我们想防御性保护第三个元素不被函数f修改,有两种处理方法。

解决方法1:采用深拷贝

传递给函数f的切片与main函数中的切片彻底隔离。实现代码如下。sCopy切片是对切片s的深拷贝。

func main() {
    s := []int{1, 2, 3}
    sCopy := make([]int, 2)
    copy(sCopy, s)
    f(sCopy)
    result := append(sCopy, s[2])
    // Use result
}

func f(s []int) {
    // Update s
}

在函数f内部对s进行操作不会影响到main函数中的切片s,因为它们是完全不同的两个切片。这种方法的缺点是代码比较复杂阅读较困难,并且需要进行深拷贝操作,当要拷贝的切片很大时是一个问题。

解决方法2:限制切片的容量

采用所谓的完整切片表达式(s[low:high:max]). 该表达式与创建切片 s[low:high]类似,唯一的不同是设置切片的容量为 max-low. 具体的实现如下:

func main() {
    s := []int{1, 2, 3}
    f(s[:2:2])
    // Use s
}

func f(s []int) {
    // Update s
}

传递给函数f的切片不是s[:2]而是s[:2:2],容量为2(2-0),在内存中的状态如下图。当在函数f中向里面append元素时,因为切片已满会进行扩容操作,在新的切片上添加元素而不会影响main函数。

Go语言中常见100问题-#25 append操作陷阱与解决方法

总结

在使用切片的时候,要留意不当操作可能引发的副作用。向切片里面添加元素时,如果切片未满,append操作会修改原切片。如果不想对原切片有影响,可以采用上面深拷贝或完整表达式的方法阻止副作用的产生。