Golang将项目附加到切片


77

为什么切片a保持不变?会append()产生新的切片吗?

package main

import (
    "fmt"
)

var a = make([]int, 7, 8)

func Test(slice []int) {
    slice = append(slice, 100)
    fmt.Println(slice)
}

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(a)
    fmt.Println(a)
}

输出:

[0 1 2 3 4 5 6 100]
[0 1 2 3 4 5 6]

该代码将解释发生了什么:https : //play.golang.org/p/eJYq65jeqwn。func Test(slice [] int),接收a的slice值的副本。它指向的数组与指向的数组相同。
gihanchanuka

Answers:


59

在您的示例slice中,Test函数的参数在调用者的作用域中接收变量的副本a

由于切片变量包含仅引用基础数组的“切片描述符”,因此您Test可以在函数中slice连续多次修改变量中包含的切片描述符,但这不会影响调用方及其a变量。

Test函数内部,第一个appendslice变量下重新分配支持数组,将其原始内容复制到其上,追加100到该数组中,这就是您要观察的内容。从离开Testslice变量超出范围,切片引用的基础(新)基础数组也将超出范围。

如果要使Test行为类似append,则必须从中返回新切片(就像这样append做一样),并要求的调用者以Test与使用相同的方式来使用它append

func Test(slice []int) []int {
    slice = append(slice, 100)

    fmt.Println(slice)

    return slice
}

a = Test(a)

在解释切片内部的工作原理之后,请彻底阅读本文,因为它基本上向您展示了如何append手动实现。然后阅读


6
我实际上认为此描述在微妙的方面是不正确的。@doun在下面的答案实际上是对内部情况的更正确表示:appendinTest不会重新分配任何内存,因为数组支持切片的原始分配a仍然可以容纳单个附加项。换句话说,由于该程序被写入时,返回值Test(a)a是具有不同长度的不同的切片标头,但它们指向完全相同的底层数组。打印fmt.Println(a[:cap(a)]为功能的最后一行main可以使这一点变得清楚。
杰夫·李

这个说法是错误的;“在您的示例中,Test函数的slice参数接收调用方范围内变量a的副本”。如Go切片用法中所述,func接收一个指针。尝试更改slice = append(slice, 100)-> slice[1] = 13。您将被打印[0 13 2 3 4 5 6]两次。@kostix您能解释一下吗?请参阅-play.golang.org/p/QKRnl5CTcM1
gihanchanuka,

@gihanchanuka,在的情况下func Test(slice []int),该函数未收到“指针”。在Go中,所有事物永远都是通过价值传递的;只是某些类型碰巧具有指针表示形式或包含指针。Go中的slice是后者的变体:任何slice值都是三个字段的结构,其中之一实际上是指向包含该slice的元素的内存块的指针。
kostix

@gihanchanuka,现在内置append函数获取切片值并返回切片值。在这两种情况下,都是三字段结构,它在输入和输出上复制(复制append的堆栈框架中,然后在其外部)。现在,如果append必须重新分配切片的存储空间以为要附加的数据腾出空间,则它返回的切片值将包含一个与输入切片值中的指针不同的指针。而且只有在append必须重新分配时才会发生这种情况,否则就不会发生(基础数组具有未使用的空间)。这就是“问题”的要点。
kostix

@kostix我知道了“ +1”,谢谢!该代码将说明发生了什么:https : //play.golang.org/p/zJT7CW-pfp8func Test(slice []int),接收切片值的副本a。它指向的数组与a指向的数组相同。我无法编辑上面的评论,将其删除将使这次谈话变得混乱。
gihanchanuka

34

典型append用法是

a = append(a, x)

因为它append可能就地修改其参数,或者返回带有附加条目的参数副本,具体取决于其输入的大小和容量。使用先前附加到的切片可能会产生意外的结果,例如

a := []int{1,2,3}
a = append(a, 4)
fmt.Println(a)
append(a[:3], 5)
fmt.Println(a)

可以打印

[1 2 3 4]
[1 2 3 5]

1
谢谢,larsmans,我修改了一些代码。“ make”将提供足够的容量。结果是一样的,我很困惑。
Pole_Zhang 2013年

这需要立即_ = append(a[:3], 5)进行编译
y3sh

6

为了使您的代码正常工作而不必从Test返回切片,您可以传递这样的指针:

package main

import (
    "fmt"
)

var a = make([]int, 7, 8)

func Test(slice *[]int) {
    *slice = append(*slice, 100)

    fmt.Println(*slice)
}

func main() {

    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(&a)

    fmt.Println(a)
}

5

如果上限不足,则append会生成一个新的切片。@kostix的答案是正确的,或者您可以通过指针传递切片参数!


1
您对指针是正确的,但是我绝对没有提到它们,因为切片的发明主要是为了使程序员摆脱处理数组的指针。在参考实现中(来自Go),slice变量包含一个指针和两个整数,因此复制它很便宜,这就是为什么这slice = append(slice, a, b, c)是惯用法,不通过指针传递slice变量并“就地”对其进行修改,以便调用者可以看到更改。
kostix

1
@kostix是的,代码的目的应该明确。但是我认为整个故事只是关于传递一个存储指针的值并传递一个指向指针的指针。如果我们修改了引用,那么两者都可以工作,但是如果我们替换了引用,第一个将失去作用。程序员应该知道他在做什么。
吉萨克2013年

5

试试看,我认为这很清楚。底层数组已更改,但我们的切片未更改,print只是将len()字符打印出另一个切片到cap(),您可以看到更改后的数组:

func main() {

  for i := 0; i < 7; i++ {
      a[i] = i
  }

  Test(a)

  fmt.Println(a) // prints [0..6]
  fmt.Println(a[:cap(a)] // prints [0..6,100]
}

所以'a'和'a [:cap(a)]'是不同的切片吗?
Pole_Zhang

2
是的,如果您运行代码,您会发现的。因为cap(a)在test(a)调用过程中被更改
doun 2013年

3

说明(阅读嵌入式注释):


package main

import (
    "fmt"
)

var a = make([]int, 7, 8)
// A slice is a descriptor of an array segment. 
// It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).
// The length is the number of elements referred to by the slice.
// The capacity is the number of elements in the underlying array (beginning at the element referred to by the slice pointer).
// |-> Refer to: https://blog.golang.org/go-slices-usage-and-internals -> "Slice internals" section

func Test(slice []int) {
    // slice receives a copy of slice `a` which point to the same array as slice `a`
    slice[6] = 10
    slice = append(slice, 100)
    // since `slice` capacity is 8 & length is 7, it can add 100 and make the length 8
    fmt.Println(slice, len(slice), cap(slice), " << Test 1")
    slice = append(slice, 200)
    // since `slice` capacity is 8 & length also 8, slice has to make a new slice 
    // - with double of size with point to new array (see Reference 1 below).
    // (I'm also confused, why not (n+1)*2=20). But make a new slice of 16 capacity).
    slice[6] = 13 // make sure, it's a new slice :)
    fmt.Println(slice, len(slice), cap(slice), " << Test 2")
}

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    fmt.Println(a, len(a), cap(a))
    Test(a)
    fmt.Println(a, len(a), cap(a))
    fmt.Println(a[:cap(a)], len(a), cap(a))
    // fmt.Println(a[:cap(a)+1], len(a), cap(a)) -> this'll not work
}

输出:

[0 1 2 3 4 5 6] 7 8
[0 1 2 3 4 5 10 100] 8 8  << Test 1
[0 1 2 3 4 5 13 100 200] 9 16  << Test 2
[0 1 2 3 4 5 10] 7 8
[0 1 2 3 4 5 10 100] 7 8

参考资料1:https//blog.golang.org/go-slices-usage-and-internals

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

2

Go在执行此操作时采用了一种更加精益和懒惰的方法。它会不断修改同一基础数组,直到达到分片的容量为止。

参考: http //criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/

链接中示例的输出说明了Go中切片的行为。

创建切片

Slice a len=7 cap=7 [0 0 0 0 0 0 0]

切片b指切片a中的2、3、4索引。因此,容量为5(= 7-2)。

b := a[2:5]
Slice b len=3 cap=5 [0 0 0]

修改切片b也会修改a,因为它们指向同一基础数组。

b[0] = 9
Slice a len=7 cap=7 [0 0 9 0 0 0 0]
Slice b len=3 cap=5 [9 0 0]

将1附加到切片b。覆盖一个。

Slice a len=7 cap=7 [0 0 9 0 0 1 0]
Slice b len=4 cap=5 [9 0 0 1]

将2附加到切片b。覆盖一个。

Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=5 cap=5 [9 0 0 1 2]

将3附加到切片b。在此,由于容量超载,将创建一个新副本。

Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 0 0 1 2 3]

在上一步中的容量过载之后,验证片a和b指向不同的基础阵列。

b[1] = 8
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 8 0 1 2 3]

2
package main

import (
    "fmt"
)

func a() {
    x := []int{}
    x = append(x, 0)
    x = append(x, 1)  // commonTags := labelsToTags(app.Labels)
    y := append(x, 2) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    z := append(x, 3) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    fmt.Println(y, z)
}

func b() {
    x := []int{}
    x = append(x, 0)
    x = append(x, 1)
    x = append(x, 2)  // commonTags := labelsToTags(app.Labels)
    y := append(x, 3) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    z := append(x, 4) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    fmt.Println(y, z)
}

func main() {
    a()
    b()
}

First guess could be

[0, 1, 2] [0, 1, 3]
[0, 1, 2, 3] [0, 1, 2, 4]

but in fact it results in

[0, 1, 2] [0, 1, 3]
[0, 1, 2, 4] [0, 1, 2, 4]

在此处输入图片说明

在此处输入图片说明

更多详细信息请参见https://allegro.tech/2017/07/golang-slices-gotcha.html


1

我认为原始答案并不完全正确。append()即使基础数组已更改但仍由两个切片共享,但它们同时更改了切片和基础数组。

如Go Doc所指定:

切片不存储任何数据,它仅描述基础数组的一部分。(链接)

切片只是数组周围的包装器值,这意味着它们包含有关如何切片用于存储一组数据的基础数组的信息。因此,默认情况下,一个切片在传递给另一个方法时实际上是按值传递的,而不是按引用/指针传递的,即使它们仍将使用相同的基础数组。通常,数组也按值传递,因此我假定切片指向基础数组,而不是将其存储为值。关于您的问题,在运行时将分片传递给以下函数:

func Test(slice []int) {
    slice = append(slice, 100)
    fmt.Println(slice)
}

您实际上传递了切片的副本以及指向同一基础数组的指针。这意味着,您对 slice不会影响main函数中的。切片本身存储有关切片和公开多少数组的信息。因此,当您运行时append(slice, 1000),在扩展基础数组的同时,您也更改了切片信息slice,该信息在您的Test()函数中保持私有状态。

但是,如果您按照以下方式更改了代码,则可能会起作用:

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(a)
    fmt.Println(a[:cap(a)])
}

原因是您扩大了 a通过说说a[:cap(a)]其已更改的基础数组(由Test()功能更改)进行了扩展。如此处指定:

如果切片具有足够的容量,则可以通过对其进行切片来延长切片的长度。(链接)


0

这是切片附加的一个很好的实现。我猜它类似于引擎盖下发生的事情:

package main

import "fmt"

func main() {
    slice1 := []int{0, 1, 2, 3, 4}
    slice2 := []int{55, 66, 77}
    fmt.Println(slice1)
    slice1 = Append(slice1, slice2...) // The '...' is essential!
    fmt.Println(slice1)
}

// Append ...
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

// Extend ...
func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Slice is full; must grow.
        // We double its size and add 1, so if the size is zero we still grow.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.