分别区分两段程序的结果

func main(){
    s := []int{1}
    s = append(s, 2)
    s = append(s, 3)
    x := append(s, 4)
    y := append(s, 5)
   fmt.Println(s, x, y)
}

func main(){
	s := []int{1, 2, 3}
	x := append(s, 4)
	y := append(s, 5)
	fmt.Println(s,x,y)
}

的结果, 是不是乍一看结果应该是一样的?
but, 出乎你意料:

第一个程序的结果:

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

第二个程序的结果:

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

问题分析

  1. 官方文档的讲解:

The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself.

slice 作为参数传递的时候是传递sliceHeader的copy值, 如果对slice进行操作, 其实是对内部的ptr参数的引用进行操作, 如果在不改变ptr的情况是操作的原始数组. 在修改数组的时候对标头上的长度数据修改是对原始slice无效的, 所以想让修改生效就必须返回结果并且重新赋值给原始值.

cap是底层数据的容量大小, 如果append超出了cap值就会重新分配数据, 并且修改底层ptr指向. 如果要从底层数据切出大于cap值的切片一定会引起恐慌.

看下面的代码:

func modiA(a []int)  {
	a = append(a, 1)
}
func main() {
	s := []int{1, 2, 3, 4, 5}
	sc := s[2:4]
	fmt.Printf("%v, %v, cap=%d \n", s, sc, cap(sc))
	modiA(sc)
	fmt.Printf("%v , %v \n", s, sc)
}

输出结果:

[1 2 3 4 5], [3 4], cap=3 
[1 2 3 4 1] , [3 4] 

modiA传递参数sc,内部执行a = append(a, 1), 再次打印结果, 不会影响sc本身, 但是原始切片受到了影响.

仅调用append()操作但不分配返回值是没有任何意义的,例如:append(s, 10)并不会往s中添加任何值.

本质上append是在原有空间中添加,若空间不足时,采用 newSlice := make([]int, len(slice), 2*len(slice)+1)的方式进行扩容。在空间不足的情况下,append在空间扩展之后,通过copy,将原有的slice拷贝到了新的newSlice中。 因此,对扩容时,会有一个内存地址变化。但是如果在满足空间大小时,内存地址不会发生变化,附加是用过内存操作实现的。slice从c语言上看是一个结构体,内部包含起始元素指针长度等等信息。

slice的底层结构

type slice struct {
    array unsafe.Pointer // 是指向底层数组的指针
    len   int // slice的长度
    cap   int // 底层数组的长度
}

x := append(s, 4)实际上是创建了一个新的struct x,x的属性如下:len: s.len+1 cap: s.cap(len <= s.cap),2*s.cap(len > s.cap) addr: 不变(cap未变,容量够用,无需重新分配)改变(cap改变,需要扩容,重新分配一个底层数组,先将s的内容复制过去,再在复制后的数组中执行append,最后将x的addr指向该数组)

slice有个特性是允许多个slice指向同一个底层数组,这是一个有用的特性,在很多场景下都能通过这个特性实现 no copy 而提高效率。但共享同时意味着不安全。 切片是引用类型,但是共享的是已知长度数组的指针,而非切片的指针

第一个程序的结果的s在追加4之前cap=4, 实际上是没有重新生成array的时候对s[3]赋值,得到的结果是[1,2,3,4],
对于y也是如此,对s[3]赋值5,得到结果[1,2,3,5], 放到后面一起打印的结果就是1,2,3,5, 因为两者的底层数组是一样的.

第二个程序的结果s的cap=3,后续append数据都会导致底层重新生成array并且指向新的array, 所以x和y的底层数组不是同一个. 打印结果就不相同.

怎么才能规避这种问题

防止共享数据的出现问题需要注意两条,只读和复制,或者统一归纳为不可变。

  1. make出一个新slice,然后先copy前缀到新数组上再追加:
func main() {
    a := make([]int, 2, 2)
    a[0], a[1] = 1, 2

    b := make([]int, 1)
    copy(b, a[0:1])
    b = append(b, 3)

    c := make([]int, 1)
    copy(c, a[1:2])
    c = append(c, 4)

    fmt.Println(b, c)
}
  1. 利用go中slice的一个小众语法,a[0:1:1] (源[起始index,终止index,cap终止index]),强迫追加时复制到新数组。
func main() {
    a := make([]int, 2, 2)
    a[0], a[1] = 1, 2

    b := append(a[0:1:1], 3)
    c := append(a[1:2:2], 4)

    fmt.Println(b, c)
}

reslice内存变化跟踪实验

package main

import "fmt"

func main(){
  s := []int{5}
	
  s = append(s,7)
  fmt.Println("cap(s) =", cap(s), "ptr(s) =", &s[0])
	
  s = append(s,9)
  fmt.Println("cap(s) =", cap(s), "ptr(s) =", &s[0])
	
  x := append(s, 11)
  fmt.Println("cap(s) =", cap(s), "ptr(s) =", &s[0], "ptr(x) =", &x[0])
	
  y := append(s, 12)
  fmt.Println("cap(s) =", cap(s), "ptr(s) =", &s[0], "ptr(y) =", &y[0])
}

输出结果:

cap(s) = 2 ptr(s) = 0x10328008
cap(s) = 4 ptr(s) = 0x103280f0
cap(s) = 4 ptr(s) = 0x103280f0 ptr(x) = 0x103280f0
cap(s) = 4 ptr(s) = 0x103280f0 ptr(y) = 0x103280f0

总结

看下面两个程序

func main() {
	s := []int{1, 2, 3, 4, 5}
	sc := s[2:4]
	fmt.Printf("%v, %v, cap=%d \n", s, sc, cap(sc))
	sc = append(sc, 2)// 未超出cap值,直接修改底层数组(原ptr的数组)对应位置的值
	fmt.Printf("%v , %v \n", s, sc)
}

func main() {
	s := []int{1, 2, 3, 4, 5}
	sc := s[2:4]
	fmt.Printf("%v, %v, cap=%d \n", s, sc, cap(sc))
	sc = append(sc, 2,3)    // 超出cap值,重新分配一个数组并且更改底层ptr指向
	fmt.Printf("%v , %v \n", s, sc)
}

他们的结果是什么样的呢?

没错:

[1 2 3 4 5], [3 4], cap=3 
[1 2 3 4 2] , [3 4 2] 

[1 2 3 4 5], [3 4], cap=3 
[1 2 3 4 5] , [3 4 2 3] 

参考资料

Arrays, slices (and strings): The mechanics of 'append', 最最权威讲解
Go 语言的 slice 为啥有这样的奇怪问题呢?
slice的坑
从append到共享
phper 的Goland学习之路--- 切片(数组的视图)