首先我们来看段代码的输出

 

    s := make([][]int, 4)
    for i := 0; i < 4; i++ {
        s[i] = make([]int, 4)
        s[i][0] = 1
    }

    s0 := s[0]
    s0 = append(s0, 5)
    fmt.Println(s)

输出的结果是

 

[[1 0 0 0] [1 0 0 0] [1 0 0 0] [1 0 0 0]]

append的值5并没有输出,那么究竟是s0并不等价于s[0],还是有其他原因呢?
首先,肯定的是在Go中,所有的拷贝都是值拷贝,不存在引用拷贝;
其次,slice由一个指针,两个整型(分别是size和cap)来实现,既然s0包含的指针和s[0]包含的指针相同,为什么append操作并没有达到预期效果呢?

带着这些疑问,我们来看看slice的底层实现。

slice的结构

 

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array指向一个数组元素的地址,这个数组可能在makeslice时创建,也可能之前就存在,而slice被"attach"上去,例如 s := a[0:5];

 

//代码比较简单
func makeslice(et *_type, len, cap int) slice {
   maxElements := maxSliceCap(et.size)
   
   if len < 0 || uintptr(len) > maxElements {
       panic(errorString("makeslice: len out of range"))
   }

   if cap < len || uintptr(cap) > maxElements {
       panic(errorString("makeslice: cap out of range"))
   }
   //分配数组空间
   p := mallocgc(et.size*uintptr(cap), et, true)
   return slice{p, len, cap}
}

e.g.

 

s := make([]int, 1, 3)
fmt.Printf("%p, %v, len:%d, cap:%d", unsafe.Pointer(&s[0]), s, len(s), cap(s))

输出:

 

0xc42007c0a0, [0], len:1, cap:3

对于直接"attach"到数组的情形,类似下面这样

 

a := [10]int{0,1,2,3,4,5,6,7,8,9}

fmt.Printf("%p, %v \n", &a, a)

s1 := a[1:5:7]

fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s1[0]), s1, len(s1), cap(s1), unsafe.Pointer(&s1) )

输出:

 

0xc42006e050, [0 1 2 3 4 5 6 7 8 9] 
0xc42006e058, [1 2 3 4], len:4, cap:6, self:0xc42006c140

根据两个指针的关系,可以看出,slice直接指向了数组中的元素

 

slice attach1

同理,slice2还可以通过另一个slice1构造,但其属性依赖slice1,并不是slice1底层的数组

 

s2 := s1[2:]
fmt.Printf("%p, %v, len:%d, cap:%d, self:%p \n", unsafe.Pointer(&s2[0]), s2, len(s2), cap(s2), unsafe.Pointer(&s2) )

输出

 

0xc42006e068, [3 4], len:2, cap:4, self:0xc42006a140 

slice attach2

修改slice中元素的值,实际上修改的是底层数组元素的值

 

s2[0] = 100
fmt.Println(a, s1, s2)

输出

 

[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4]

扩张

当我们对slice进行append操作时,若len + 追加元素个数 <= cap时,不会发生内存扩张;否则,新的内存被申请,同时旧的数据被拷贝至新内存的前部;

先上代码

 

func growslice(et *_type, old slice, cap int) slice {
    ......

    if et.size == 0 {
        if cap < old.cap {
            panic(errorString("growslice: cap out of range"))
        }
        
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
    // 计算新的cap,针对不同情况分别处理
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            //2倍
            newcap = doublecap
        } else {
            //每次尝试扩1/4
            for newcap < cap {
                newcap += newcap / 4
            }
        }
    }

    var lenmem, newlenmem, capmem uintptr
    const ptrSize = unsafe.Sizeof((*byte)(nil))
    switch et.size {
    case 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        newcap = int(capmem)
    case ptrSize:
        lenmem = uintptr(old.len) * ptrSize
        newlenmem = uintptr(cap) * ptrSize
        capmem = roundupsize(uintptr(newcap) * ptrSize)
        newcap = int(capmem / ptrSize)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem = roundupsize(uintptr(newcap) * et.size)
        newcap = int(capmem / et.size)
    }
    ......
    var p unsafe.Pointer
    if et.kind&kindNoPointers != 0 {
        //这里有个优化细节,先不zero,因为前部会发生覆盖
        p = mallocgc(capmem, nil, false)
        memmove(p, old.array, lenmem)
        //只对后部zero
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if !writeBarrier.enabled {
            memmove(p, old.array, lenmem)
        } else {
            for i := uintptr(0); i < lenmem; i += et.size {
                typedmemmove(et, add(p, i), add(old.array, i))
            }
        }
    }
    return slice{p, old.len, newcap}
}

当不需要扩容时,append会修改底层数组的值

 

s2 = append(s2, 1000)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

 

[0 1 2 100 4 1000 6 7 8 9] [1 2 100 4] [100 4 1000]
len(s1)=4, cap(s1)=6, len(s2)=3, cap(s2)=4

注意,对s2的append,仅改变了s2的len,并没有改变s1的len
当append过多时,slice底层数组会发生变化

 

s2 = append(s2, 1000, 1001, 1002)
fmt.Printf("%p, %v \n", unsafe.Pointer(&s2[0]), s2)
fmt.Println(a, s1, s2)
fmt.Printf("len(s1)=%d, cap(s1)=%d, len(s2)=%d, cap(s2)=%d", len(s1), cap(s1), len(s2), cap(s2))

输出

 

0xc420014280, [100 4 1000 1001 1002] 
[0 1 2 100 4 5 6 7 8 9] [1 2 100 4] [100 4 1000 1001 1002]
len(s1)=4, cap(s1)=6, len(s2)=5, cap(s2)=8

原数组a,切片s1的属性未受影响;但s2底层的数组已发生变化,cap也是之前的2倍。

总结

1、多个slice指向相同的底层数组时,修改其中一个slice,可能会影响其他slice的值;
2、slice作为参数传递时,比数组更为高效,因为slice的结构比较小;
3、slice在扩张时,可能会发生底层数组的变更及内存拷贝;
4、因为slice引用了数组,这可能导致数组空间不会被gc,当数组空间很大,而slice引用内容很少时尤为严重;