疯狂吸饶大

自问:

  1. 切片append如何工作
  2. 所有的拷贝都是值拷贝
  3. 切片的结构
  4. 切片的扩容(同append)
  5. 数组和切片有什么不一样
// runtime/slice.go
type slice struct {
    array unsafe.Pointer // 元素指针指向底层数组
    len   int // 长度 表示切片可用元素的个数,也就是说使用下标对 slice 的元素进行访问时,下标不能超过 slice 的长度
    cap   int // 容量 层数组的元素个数,容量 >= 长度。在底层数组不进行扩容的情况下,容量也是 slice 可以扩张的最大限度
}
func main() {
    a := make([]int, 2, 2)
    b := a
    fmt.Printf("a: cap %d, len %d, val %v, pointer %p \n",cap(a), len(a), a, a)
    fmt.Printf("b: cap %d, len %d, val %v, pointer %p \n",cap(b), len(b), b, b)
    
    a = append(a, 1)
    fmt.Printf("a: cap %d, len %d, val %v, pointer %p \n",cap(a), len(a), a, a)
    fmt.Printf("b: cap %d, len %d, val %v, pointer %p \n",cap(b), len(b), b, b)
}

//a: cap 2, len 2, val [0 0], pointer 0xc00001e340 
//b: cap 2, len 2, val [0 0], pointer 0xc00001e340 
//a: cap 4, len 3, val [0 0 1], pointer 0xc00001a1e0 
//b: cap 2, len 2, val [0 0], pointer 0xc00001e340

由此可看出,a和b一开始是共享内存的,append操作在保证由于容量 >= 长度的前提下,不会重新分配内存,上述代码append使len = 3 > cap,此时导致扩容,把容量改为3,则共享内存


创建方式

如果遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化

func main() {
    a :=  []int{}  //empty slice
    var b []int   //nil slice

    c :=  make([]int, 1, 3)  //empty slice
    var d = *new([]int)         //nil slice  等价于 var b []int
    var e = new([]int)      //empty slice
    fmt.Printf("a is nil %v, pointer %p \n", a == nil, a)
    fmt.Printf("b is nil %v, pointer %p \n", b == nil, b)
    fmt.Printf("c is nil %v, pointer %p \n", c == nil, c)
    fmt.Printf("d is nil %v, pointer %p \n", d == nil, d)
    fmt.Printf("e is nil %v, pointer %p \n", e == nil, e)
}
//a is nil false, pointer 0x133a180 
//b is nil true, pointer 0x0 
//c is nil false, pointer 0xc0000b60c0 
//d is nil true, pointer 0x0 
//e is nil false, pointer 0xc0000a82a0


append:

//先将数据copy到新的内存,然后再将append的数据追加,可以看到当老切片容量大于1024后,并不是2倍或者1.25倍的增长
func main() {
    s := make([]int, 0)

    oldCap := cap(s)

    for i := 0; i < 2048; i++ {
        s = append(s, i)

        newCap := cap(s)

        if newCap != oldCap {
            fmt.Printf("[%d -> %4d] pre cap = %-4d  |  after append %-4d  now cap = %-4d len: %v\n", 0, i-1, oldCap, i, newCap, len(s))
            oldCap = newCap
        }
    }
}

[0 ->   -1] pre cap = 0     |  after append 0     now cap = 1    len: 1
[0 ->    0] pre cap = 1     |  after append 1     now cap = 2    len: 2
[0 ->    1] pre cap = 2     |  after append 2     now cap = 4    len: 3
[0 ->    3] pre cap = 4     |  after append 4     now cap = 8    len: 5
[0 ->    7] pre cap = 8     |  after append 8     now cap = 16   len: 9
[0 ->   15] pre cap = 16    |  after append 16    now cap = 32   len: 17
[0 ->   31] pre cap = 32    |  after append 32    now cap = 64   len: 33
[0 ->   63] pre cap = 64    |  after append 64    now cap = 128  len: 65
[0 ->  127] pre cap = 128   |  after append 128   now cap = 256  len: 129
[0 ->  255] pre cap = 256   |  after append 256   now cap = 512  len: 257
[0 ->  511] pre cap = 512   |  after append 512   now cap = 1024 len: 513
[0 -> 1023] pre cap = 1024  |  after append 1024  now cap = 1280 len: 1025
[0 -> 1279] pre cap = 1280  |  after append 1280  now cap = 1696 len: 1281
[0 -> 1695] pre cap = 1696  |  after append 1696  now cap = 2304 len: 1697


func main() {
    //len 2, cap 2
    s := []int{1,2}
    fmt.Printf("s append before len=%d, cap=%d \n",len(s),cap(s))
    //调用growslice(s, 3), doublecap = old.cap + old.cap newcap = doublecap  newcap: 4
    s = append(s,4)
    fmt.Printf("s append 4 len=%d, cap=%d \n",len(s),cap(s))
    
    //此时append 5, 之前的容量为4,进行append发现容量够用,则不用调用growslice方法
    s = append(s,5)
    fmt.Printf("s append 5 len=%d, cap=%d \n",len(s),cap(s))
        
    //同上,调用growslice(), 后cap为8 
    s = append(s,6)
    fmt.Printf("s append 6 len=%d, cap=%d \n",len(s),cap(s))

    fmt.Println("====================")

    s1 := []int{1,2}
    fmt.Printf("s1 append before len=%d, cap=%d \n",len(s1),cap(s1))
   // len 2, cap 2,调用growslice(s1, 5) doublecap = 4,  roundupsize() 得到6
    s1 = append(s1, 4,5,6)
    fmt.Printf("s1 append after  len=%d, cap=%d \n",len(s1),cap(s1))
}

结果
s append before len=2, cap=2 
s append 4 len=3, cap=4 
s append 5 len=4, cap=4 
s append 6 len=5, cap=8 
====================
s1 append before len=2, cap=2 
s1 append after  len=5, cap=6 


对于上面的代码,在看饶大的文章的时候我一脸蒙蔽,本来周一就晕乎的,看完解释,就更晕乎了,不过过一晚上,脑子就清醒了,瞬间就明白了,代码就是这样,他不会欺骗你,如果你觉得他欺骗了你,那就证明是你太菜了,接下来我们来研究为什么一次append三个数字,三次append的结果不一样


append在扩容时真实调用的方法


// go 最新版本中 src/runtime/slice.go:82
//这个函数的参数依次是 元素的类型,老的 slice,新 slice 最小求的容量 !!!!!!!再强调一遍第三个参数是:新 slice 最小求的容量
func growslice(et *_type, old slice, cap int) slice {
    // ……
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.cap < 1024{
            newcap = doublecap
        } else {
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }
    // ……

    capmem = roundupsize(uintptr(newcap) * ptrSize)
    newcap = int(capmem / ptrSize)
}




总结:

切片是对底层数组的一个抽象,描述了它的一个片段。

切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。

多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。

append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。

扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。

当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。