译文 Go 常见错误集锦之 append 操作 slice 时的副作用

yudotyang · 2021年10月08日 · 185 次阅读
本帖已被设为精华帖!

大家好,我是 Go 学堂的渔夫子。本文是对《100 Go Mistackes:How to Avoid Them》一书的翻译。因翻译水平有限,难免存在翻译准确性问题,敬请谅解。

我们知道,对 slice 的切分实际上是作用在 slice 的底层数组上的操作。对一个已存在的 slice 进行切分操作会创建一个新的 slice,但都会指向相同的底层数组。因此,如果一个索引值对两个 slice 都是可见的,那么使用索引更新一个 slice 的时候(例如 s1[1] = 10),同时该更新也会影响另外一个 slice。

本文将介绍使用 append 时的一种常见的错误,该操作在某些场景下会导致副作用。

首先,我们有以下示例:初始化一个切片 s1,然后通过切分 s1 的方式创建切片 s2,再然后通过在 s2 上进行 append 操作创建切片 s3:

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

通过以上代码可知,s1 包含 3 个元素。对 s1 进行切分操作来创建 s2。然后对 s2 进行 append 操作创建 s3。那么,最后这 3 个切片的状态是什么呢?

下图是 s1 和 s2 在内存中的状态示例图:

s1 是长度为 3,容量为 3 的切片结构,而 s2 是长度为 1,容量为 2 的切片结构,s1 和 s2 的都指向相同的底层数组。

当使用 append 给切片添加元素的时候 会检查切片是否已满:切片的长度等于切片容量时判定为元素已满。如果没有满,还有空间,那么 append 函数则将元素添加到原底层数据的空闲空间中,并返回一个新的结构体。

在该示例中,s2 还没有满,还能接收一个元素。因此,下图是 3 个切片最终的状态,如图:

由图可看出,3 个切片共享一个底层数据,数据的最后一个元素被更新为 10。那么,如果我们打印这 3 个切片,则会有以下输出:

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

可见,即使我们没有修改 s1[2],也没修改 s1[1],但 s1 的内容被修改了。因此,我们应该牢记该规则,以避免造成意外的错误。

我们再来看下另外一个影响:当将通过切分得到的新切片作为函数参数传递时的影响。

我们看下面的示例代码:

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

    f(s[:2])
    // Use s
}

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

这种实现非常危险。实际上,函数 f 会对输入的切片产生副作用。例如,如果函数 f 调用 append(s, 10),那么 main 函数中的 s 的内容就不再是 [1 2 3],而是 [1 2 10]。

我们该如何解决上述问题呢?

方案一:拷贝切片 我们可以通过对原切片进行拷贝,然后构建一个新的切片变量,如下代码所示:

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
}

① 将 s 的前两个元素拷贝到 sCopy 中

② 通过 append 函数将 s[2] 增加到 sCopy 中构建一个新的结果切片

因为我们在函数 f 中传递了一个拷贝,即使在函数中调用了 append,也不会对该切片造成副作用。该方案的缺点就是需要对已存在的切片进行一次拷贝,如果切片很大,那拷贝时存储和性能就会成为问题

方案二:限制切片容量 该方案是通过限制切片容量,在对切片进行操作时自动产生一个新的底层数据的方式来避免对原有切片副作用的产生。该方案就是所谓的满切片表达式:s[low:high:max]。这种满切片表达式和 s[low:high] 的区别在于 s[low:high:max] 的切片的容量是 max-low,而 s[low:high] 的容量是 s 中底层数据的最大容量减去 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 - 0 = 2,如下所示:

这种解决方案既共享了切片的底层数组,又通过限制容量避免了副作用。

我们必须时刻注意,从一个切片切分成子切片时,在这两个切片之间有可能会产生数据副作用。当直接修改一个元素或使用 append 函数的时候,这种副作用就会产生。如果我们想解决这种副作用,可以通过满切片表达式的方式来解决。这种方式避免了额外的拷贝,还算是比较高效的。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 10月08日 14:32
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册