【问题标题】:Golang append an item to a sliceGolang 将项目附加到切片
【发布时间】:2013-12-10 07:26:57
【问题描述】:

为什么切片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 是一个struct,它通过值传递,而不是通过引用或指针传递。等号只是打破了Test 中的slices 链。
  • 与 C 家族中的所有语言一样,Go 中的所有内容都是 [golang.org/doc/faq#pass_by_value](passed 按值)。这意味着对于切片类型,传递指针值会复制指针,但不会复制它指向的数据。返回切片特别重要,因为当它重新分配结果切片时,它描述了一个完全不同的数组。

标签: go


【解决方案1】:

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

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

Test 函数内部,第一个append 重新分配slice 变量下的支持数组,复制其原始内容,将100 附加到它,这就是您所观察到的。从Test 退出时,slice 变量超出范围,切片引用的(新)底层数组也是如此。Jeff Lee 是正确的,它不是真正发生的,所以更新版本如下;正如他正确指出的那样,this answer 是正确的,虽然可能有点过于简洁。)

Test 函数之外,分配了一个长度为 7、容量为 8 的切片,并填充了它的 7 个元素。
Test 函数中,第一个append 看到切片的容量仍然比它的长度大一个元素——换句话说,在不重新分配的情况下还有空间可以添加一个元素。 所以它会“吃掉”剩余的元素并将100 放置到它上面,之后它会调整切片描述符副本中的长度以使其等于切片的容量。 这不会影响调用者范围内的切片描述符。

这就是你所观察到的。从Test 退出后,slice 变量将超出范围,切片引用的(新)底层数组也是如此。

如果你想让Test 表现得像append你必须从中返回新切片——就像append 所做的那样——并要求Test 的调用者以他们使用append的相同方式使用它:

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

    fmt.Println(slice)

    return slice
}

a = Test(a)

请仔细阅读this article,因为它基本上向您展示了如何手动实现append,在解释了切片如何在内部工作之后。然后阅读this

【讨论】:

  • 我实际上认为这种描述在微妙的方面是不正确的。 @doun 下面的回答实际上是对内部情况的更正确表示:Test 中的 append 不会重新分配任何内存,因为数组支持切片 a 的原始分配仍然可以容纳单个附加项。换句话说,在编写这个程序时,Test(a)a 的返回值是不同长度的切片头,但它们指向完全相同的底层数组。将fmt.Println(a[:cap(a)] 打印为main 函数的最后一行可以清楚地说明这一点。
  • 这个说法是错误的; “在您的示例中, Test 函数的 slice 参数在调用者的范围内接收变量 a 的副本”。正如Go slice usage 中提到的,func 接收一个指针。尝试更改 slice = append(slice, 100) -> slice[1] = 13。您将被打印两次[0 13 2 3 4 5 6]。 @kostix 你能解释一下吗?参考 - play.golang.org/p/QKRnl5CTcM1
  • @gihanchanuka,在func Test(slice []int)的情况下,函数没有收到“指针”。在 Go 中,一切都是按值传递的;只是某些类型碰巧有指针表示或 contain 指针。 Go 中的切片属于后一种类型:任何切片值都是三个字段的结构,其中一个实际上是指向保存切片元素的内存块的指针。
  • @gihanchanuka,现在内置的append 函数接受一个切片值并返回一个切片值。在这两种情况下,它都是三字段结构,它在输入和输出上复制(进入append 的堆栈帧,然后在它之外)。现在,如果append 必须重新分配切片的存储空间来为要附加的数据腾出空间,它返回的切片值包含一个与输入切片值中的指针不同 的指针。只有当append 必须重新分配时才会发生这种情况,否则不会发生(底层数组有未使用的空间)。这就是“问题”的要点。
  • @kostix 我知道了“+1”,谢谢!这段代码将解释发生了什么:https://play.golang.org/p/zJT7CW-pfp8func Test(slice []int),接收切片值a 的副本。它指向与a 指向相同的数组。我无法编辑我的上述评论,删除它会混淆这次对话。
【解决方案2】:

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]

【讨论】:

  • 谢谢,larsmans,我修改了一些代码。 “make”将提供足够的容量。结果是一样的,我很困惑。
  • 这需要_ = append(a[:3], 5) 现在编译
  • append(a[:3], 5) 与以下 hack 中的 a[3] = 5 相同。我想这是一个意外发生的例子。
【解决方案3】:

为了使您的代码无需从 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)
}

【讨论】:

    【解决方案4】:

    注意,如果 cap 不够,append 会生成一个新切片。 @kostix 的回答是正确的,或者你可以通过指针传递 slice 参数!

    【讨论】:

    • 您对指针的看法是正确的,但我绝对没有提到它们,因为发明切片主要是为了让程序员从处理指向数组的指针中解放出来。在参考实现(来自 Go)中,切片变量包含一个指针和两个整数,因此复制它很便宜,这就是为什么 slice = append(slice, a, b, c) 是惯用的,而不是通过指针传递切片变量并“就地”修改它,以便调用者看到了变化。
    • @kostix 你是对的,代码的目的应该是明确的。但我认为整个故事只是传递一个存储指针的值并传递一个指向指针的指针。如果我们修改引用,两者都可以工作,但如果我们替换引用,第一个失去效果。程序员应该知道他在做什么。
    【解决方案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)]' 是不同的切片?
    • 是的,如果你运行代码你会发现。因为 cap(a) 在 test(a) 调用期间发生了变化
    【解决方案6】:

    解释(阅读内联 cmets):

    
    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
    }
    

    【讨论】:

      【解决方案7】:

      Go 在这方面采取了更精简和懒惰的方法。它使 修改相同的底层数组,直到切片的容量为 到达。

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

      链接中的示例输出解释了 Go 中切片的行为。

      创建切片 a。

      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。覆盖 a。

      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。覆盖 a。

      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]
      

      【讨论】:

        【解决方案8】:
        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

        【讨论】:

          【解决方案9】:

          答案:如果没有足够的容量,append() 将返回新的底层数组。在您的示例中,

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

          您将底层数组(容量为 8)的切片(长度为 7)分配给 a,然后将其作为参数 slice 传递给函数。当调用append() 时,它找到了1 容量,然后只需将slicelen 从7 更新为8,并将值100 放入该位置。

          切片a 与切片slice 不同,具有不同的len 属性。 lencap 是 slice 的属性,而不是底层数组。更多详情:Are slices passed by value?.

          运行下面的例子:

          package main
          
          import (
              "fmt"
          )
          
          var a = make([]int, 7, 8)
          
          func Test(slice []int) {
              fmt.Printf("slice's address is %p\n", &slice)
              fmt.Println("slice: cap=",cap(slice),"len=",len(slice))
              slice = append(slice, 100)
              fmt.Println("slice: cap=",cap(slice),"len=",len(slice))
              fmt.Println(slice)
          }
          
          func main() {
              for i := 0; i < 7; i++ {
                  a[i] = i
              }
              fmt.Printf("a's address is %p\n", &a)
              fmt.Println("a: cap=",cap(a),"len=",len(a))
              Test(a)
              fmt.Println("a: cap=",cap(a),"len=",len(a))
              fmt.Println(a)
              fmt.Println(a[:8]) // manully extend a's len to cap of 8
          }
          

          结果是:

          ❯❯  Temp  17:33  go run .\test.go
          a's address is 0x2cbfc0
          a: cap= 8 len= 7
          slice's address is 0xc000098060
          slice: cap= 8 len= 7
          slice: cap= 8 len= 8
          [0 1 2 3 4 5 6 100]
          a: cap= 8 len= 7
          [0 1 2 3 4 5 6]
          [0 1 2 3 4 5 6 100]
          

          【讨论】:

            【解决方案10】:

            我认为最初的答案并不完全正确。 append() 更改了切片和底层数组,即使底层数组已更改但仍由两个切片共享。

            按照 Go Doc 的规定:

            切片不存储任何数据,它只是描述底层数组的一部分。 (Link)

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

            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() 函数更改。在这里指定:

            您可以通过重新切片来扩展切片的长度,前提是它有足够的容量。 (Link)

            【讨论】:

              【解决方案11】:

              是的,当您使用append() 向 Slice 添加值时,它通常会创建一个新 Slice 而不会覆盖原始 Slice。

              检查下面的代码sn-p

              package main
              
              import "fmt"
              
              func main() {
              
                  var ages = []int{3,6,8,1,9}
              
                  fmt.Println(append(ages, 13))
              
                  fmt.Println("Original Slice is still: ", ages)
              }
              

              如果需要覆盖原来的Slice,需要将append()设置为Slice名称,如下所示。

              ages = append(ages, 12)
              fmt.Println("Now original Slice is: ", ages)
              

              【讨论】:

                【解决方案12】:

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

                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
                }
                

                【讨论】:

                  【解决方案13】:

                  追加到切片的末尾,如果切片为空,则创建一个新条目

                  // in := [][]int{{}}, in := [][]int{{1,3},{2,3}}
                  // addtoEndofSliceArray(in,10)
                  // out=[[10]], out=[[1,3],[2,3,10]]
                  
                  func addtoEndofSliceArray(in [][]int,element int)(out [][]int){
                      if len(in) >0 {
                          k :=in[len(in)-1]
                          k = append(k,element)
                          in = in[:len(in)-1]
                          in = append(in, k)
                      }else{
                          in = [][]int{{element}}
                      }
                      return in
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 2016-05-25
                    • 2019-01-02
                    • 1970-01-01
                    • 2017-08-11
                    • 1970-01-01
                    • 2016-12-24
                    • 2021-09-21
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多