Go slice append 陷阱

2021/05/21 Go

Go slice append 陷阱

Go中的slice也叫切片,经常用于处理动态数组,简单方便。但是,有时它们的行为并不总是像人们期望的那样。 我猜想,如果您正在阅读此文章,则说明您已经对Go有所了解,因此我不会以任何方式介绍它,而我只会关注slice

Go中的slice是非常有用的Go数组抽象。 slicearray都有类型化的值,其中array定义了静态长度,我们通常不会使用array,因为array基本不能操作什么,而slice则可以追加、剪切和一般化操作,而这些都是array缺少的。

slice最常用的就是使用内置函数append:

package main
import "fmt"

func main() {
	// create new slice with few elements:
	s := []int{1,2,3}
	fmt.Printf("slice content: %v\n", s)
	
	// append new element to slice:
	s = append(s, 4)
	fmt.Printf("slice after append: %v\n", s)
}

append函数第一个参数是slice,第二个参数是需要追加的数据,这里需要注意的是append返回的是一个全新的slice,而不是单纯的在第一个slice上拼接一个数据,所以我们一般情况下会将append的返回值赋值给原来的slice,以此达到在原来的slice上追加数据的目的。

是不是看来很简单? 但是我要告诉你,这里有一个陷阱:

package main

import "fmt"

func create(iterations int) []int {
    a := make([]int, 0)
    for i := 0; i < iterations; i++ {
        a = append(a, i)
    }
    return a
}

func main() {
    sliceFromLoop()
    sliceFromLiteral()

}

func sliceFromLoop() {
    fmt.Printf("** NOT working as expected: **\n\n")
    i := create(11)
    fmt.Println("initial slice: ", i)
    j := append(i, 100)
    g := append(i, 101)
    h := append(i, 102)
    fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}

func sliceFromLiteral() {
    fmt.Printf("\n\n** working as expected: **\n")
    i := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println("initial slice: ", i)
    j := append(i, 100)
    g := append(i, 101)
    h := append(i, 102)
    fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}

你可以试着运行下,看看结果:

** NOT working as expected: **

initial slice:  [0 1 2 3 4 5 6 7 8 9 10]
i: [0 1 2 3 4 5 6 7 8 9 10]
j: [0 1 2 3 4 5 6 7 8 9 10 102]
g: [0 1 2 3 4 5 6 7 8 9 10 102]
h:[0 1 2 3 4 5 6 7 8 9 10 102]


** working as expected: **
initial slice:  [0 1 2 3 4 5 6 7 8 9 10]
i: [0 1 2 3 4 5 6 7 8 9 10]
j: [0 1 2 3 4 5 6 7 8 9 10 100]
g: [0 1 2 3 4 5 6 7 8 9 10 101]
h:[0 1 2 3 4 5 6 7 8 9 10 102]

sliceFromLoop函数运行后打印的结果和我们预期不一样:我们预期的jg最后追加的应该是100101,但是这里都是102!也就是最后一个append的值会修改前面两个append的值!

为什么会造成这样的结果呢?

这是因为Goappend修改的是slice中底层的数组,并且新的slice也是基于此数组的。这就意味着基于append返回的新的slice可能会导致难以发现的问题。

怎么解决这种问题呢?

只用append在老的slice增加元素,不用来创建新的slice

someSlice = append(someSlice, newElement)

如果你有需求必须从老的slice创建新的slice,那么首先要使用内置函数copy函数复制老的slice底层的数组数据到新的slice,然后在新的slice上执行append操作:

func copyAndAppend(i []int, vals ...int) []int {
    j := make([]int, len(i), len(i)+len(vals))
    copy(j, i)
    return append(j, vals...)
}

或者是创建老的slice浅拷贝(shallow copy):

newSlice := append(T(nil), oldSlice...)

这个毕竟常规用法,写了几年的Go的生产代码才发现这个问题😂确实不太容易理解,另外就是容易让人产生过误解和困惑。

原文地址(需要翻墙):https://medium.com/@Jarema./golang-slice-append-gotcha-e9020ff37374

Search

    欢迎关注我的微信公众号

    够浪程序员

    Table of Contents