golang 切片(slice),你真的清楚吗?(Go 1.14)

727 阅读7分钟

slice 是什么

slice 类似于 Java 中的 ArrayList,是一个可以自动扩容的"数组",可以进行添加、删除、修改、查询、遍历且有自动扩容的功能,而且 slice 不是并发安全的,

本文基于 Go 1.14,如果过程中过有疑问、建议等等,欢迎在评论区或者公众号给我留言,我们一起交流学习,码字不易,希望能该你带来帮助

文中有些许汇编知识,可以优先学习 《 go 语言高级编程》--汇编章节,找不到的可以到我公众号(在文末)留言「go语言高级编程」拿到

slice 的内存结构以及寻找思路

思路

func main() {
    a := make([]int, 0)
    a = append(a, 1)
    fmt.Println(a)
}

通过 go tool compile -S -l -N main.go ,找到底层执行的函数

"".main STEXT size=328 args=0x0 locals=0x98
        ...
        0x0042 00066 (main.go:6)        CALL    runtime.makeslice(SB)
        ...
        0x007c 00124 (main.go:7)        CALL    runtime.growslice(SB)
        ...

具体代码如下

// 这里只是表达了 slice 底部的数组地址
func makeslice(et *_type, len, cap int) unsafe.Pointer {
    ...
}

// 这里反映出 slice 的结构
func growslice(et *_type, old slice, cap int) slice {
    ...
    return slice{p, old.len, newcap}
}

思路主要是创建函数的”返回“以及操作过程中函数的 “传参” 或者 “返回“,由此我们可以看到具体的结构如下

内存结构

type slice struct {
    array unsafe.Pointer     // 底层数组的地址
    len   int                // slice 的 len    
    cap   int                // slice 的 cap
}

问题1: var a []int[]int{} 有什么区别

func main() {
    var a []int
    fmt.Println(a == nil) //true
    b := []int{}
    fmt.Println(b == nil) //false
}

首先我们来看切片的创建方式

创建方式代码例子
直接声明var a []int
makea := make([]int, len)
a := make([]int, len, cap)
字面表示a := []int{}
截取a = a[i:j]
a = a[i:j:k]
new(感谢老钱的文章,文末有原地址)a := *new([]int)

直接声明

func main() {
    var a []int
    fmt.Println(a)        //[]
    fmt.Println(len(a))   //0
    fmt.Println(cap(a))   //0
    fmt.Println(a == nil) //true
}

make

方式1:make([]int, len)

func main() {
    a := make([]int, 1) // 如果 make 只有2个参数,则 len 和 cap 都第2个参数的值
    fmt.Println(a)      // [0]
    fmt.Println(len(a)) // 1
    fmt.Println(cap(a)) // 1
}

方式2:make([]int, len, cap)

func main() {
    a := make([]int, 1, 2) // 如果 make 有 3 个参数,则则 len 为第 2 个参数的值,cap 为第 3 个参数的值
    fmt.Println(a)         // [0]
    fmt.Println(len(a))    // 1
    fmt.Println(cap(a))    // 2
}

方式2 我们通过 go tool compile -S -l -N main.go 我们可以知道如下的情况

"".main STEXT size=557 args=0x0 locals=0xe0
        ...
        0x002f 00047 (main.go:6)        PCDATA  $0, $1
        0x002f 00047 (main.go:6)        PCDATA  $1, $0
        0x002f 00047 (main.go:6)        LEAQ    type.int(SB), AX
        0x0036 00054 (main.go:6)        PCDATA  $0, $0
        0x0036 00054 (main.go:6)        MOVQ    AX, (SP)
        0x003a 00058 (main.go:6)        MOVQ    $1, 8(SP)
        0x0043 00067 (main.go:6)        MOVQ    $2, 16(SP)
        0x004c 00076 (main.go:6)        CALL    runtime.makeslice(SB) // 通过这里创建 []int
        0x0051 00081 (main.go:6)        PCDATA  $0, $1
        0x0051 00081 (main.go:6)        MOVQ    24(SP), AX
        0x0056 00086 (main.go:6)        PCDATA  $1, $1
        0x0056 00086 (main.go:6)        MOVQ    AX, "".a+120(SP)
        0x005b 00091 (main.go:6)        MOVQ    $1, "".a+128(SP)
        0x0067 00103 (main.go:6)        MOVQ    $2, "".a+136(SP)
        ...

我们通过查询 makeslice 找到对应的编译逻辑

// 位置在于 /cmd/compile/internal/gc/typecheck.go
func typecheck1(n *Node, top int) (res *Node) {
    ...

    ok := 0
    switch n.Op {
    ...

    case OMAKE: // 这里是校验 make 语法中
        ok |= ctxExpr
        args := n.List.Slice() // make([]int, 1) \ make([]int, 1, 2),即 args 长度为 2 或者 3,第一是[]int ,第二个是 len ,第三个是 cap
        if len(args) == 0 {
            yyerror("missing argument to make")
            n.Type = nil
            return n
        }

        n.List.Set(nil)
        l := args[0]
        l = typecheck(l, ctxType) // 这里 l 就是 []int
        t := l.Type
        if t == nil {
            n.Type = nil
            return n
        }

        i := 1
        switch t.Etype {
            ...
        case TSLICE:
            if i >= len(args) {
                yyerror("missing len argument to make(%v)", t)
                n.Type = nil
                return n
            }
            
            l = args[i] //len
            i++         //2
            l = typecheck(l, ctxExpr)
            var r *Node
            if i < len(args) { // 如果 make 中有3个参数,则 r 为 cap,否则为nil
                r = args[i]
                i++
                r = typecheck(r, ctxExpr)
            }
            
            ... // 校验
            
            if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 { // 如果 len > cap 则报错
                yyerror("len larger than cap in make(%v)", t)
                n.Type = nil
                return n
            }
            
            n.Left = l
            n.Right = r
            
            /*
               在syntax.go中,有 const 为
               const (
                 ...
                 OMAKESLICE     // make(Type, Left, Right) (type is slice)
                 ...
               )
               正好印证了 n.left 代表的是第二个参数,n.Right 代表的是第三个参数
            */
            n.Op = OMAKESLICE
            ...
        }
        ...
    }
    
    ...
}

接下来我们跳转到 OMAKESLICE的阶段

// 位置在于 cmd/compile/internal/gc/walk.go
func walkexpr(n *Node, init *Nodes) *Node {

opswitch:
    switch n.Op {
        ...
    case OMAKESLICE:
        l := n.Left   //  len
        r := n.Right  // cap
        if r == nil { // 加入没有 cap 这个参数,则与 len 一致
            r = safeexpr(l, init)
            l = r
        }
        t := n.Type

        if n.Esc == EscNone { // EscNone 与之前 map 一样,表示不逃逸到堆上
            // 这里整体看下来,就是如果不逃逸到堆上,其底层是一个数组,然后进行截取 cap ,具体可以细看代码
            ...
        } else {
            ...

            len, cap := l, r

            fnname := "makeslice64"
            argtype := types.Types[TINT64]

            if (len.Type.IsKind(TIDEAL) || maxintval[len.Type.Etype].Cmp(maxintval[TUINT]) <= 0) &&
                (cap.Type.IsKind(TIDEAL) || maxintval[cap.Type.Etype].Cmp(maxintval[TUINT]) <= 0) {
                fnname = "makeslice"
                argtype = types.Types[TINT]
            }

            ...

            fn := syslook(fnname) // 这里可以看到,这里执行 makeslice64 / makeslice

            ...
        }
        ...
    }
    ...
}

因此创建总共有两个,如下

func makeslice(typ *byte, len int, cap int) unsafe.Pointer
func makeslice64(typ *byte, len int64, cap int64) unsafe.Pointer

makeslice64 函数中,本质也是执行 makeslice

func makeslice64(et *_type, len64, cap64 int64) unsafe.Pointer {
    // len 和 cap 的校验
    len := int(len64)
    if int64(len) != len64 {
        panicmakeslicelen()
    }

    cap := int(cap64)
    if int64(cap) != cap64 {
        panicmakeslicecap()
    }

    return makeslice(et, len, cap)
}
// 这里我们看到返回一个指针 (unsafe.Pointer),这里表示底层数组的指针,即 slice 的 array
// 要分别判断 len 和 cap 是否内存溢出或者超过可分配的容量
func makeslice(et *_type, len, cap int) unsafe.Pointer { 
    //由于 mallocgc 的 size 为 0 ,则会返回 zerobase 的指针,即 mem 为 0 的时候,因此
    //即当 et.size 为0 或者是 cap 为 0 即可
    mem, overflow := math.MulUintptr(et.size, uintptr(cap)) 
    if overflow || mem > maxAlloc || len < 0 || len > cap {
        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 { 
            panicmakeslicelen()
        }
        panicmakeslicecap()
    }

    return mallocgc(mem, et, true)
}

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    ...
    if size == 0 { // 当 size 为 0,则返回指向 zerobase 的指针
        return unsafe.Pointer(&zerobase)
    }
    ...
}

对于 mallocgc 中 size 为 0的情况,可以看以下代码

func main() {
    e := make([]int, 0)
    fmt.Println(e == nil) //false
    e1 := *(*[3]int)(unsafe.Pointer(&e))
    fmt.Println(e1[0]) // 824634227792,表示 array
    fmt.Println(e1[1]) // 0,表示 len
    fmt.Println(e1[2]) // 0,表示 cap

    fmt.Println("---------------------")

    // ** 这个只是来证明一下
    f := make([]struct{}, 10)
    fmt.Println(f == nil) //false
    f1 := *(*[3]int)(unsafe.Pointer(&f))
    fmt.Println(f1[0]) // 824634227792,表示 array
    fmt.Println(f1[1]) // 10,表示 len
    fmt.Println(f1[2]) // 10,表示 cap
}

上面代码中, e 由于 cap 为 0(不理解的可以看上文),以及 f 中 _type 的 size 为 0,因此里面的 array 的值是相同的,都是 zerobase 的地址

通过 *[3]int 强转的方式后续会在后文讲解思路

字面表示

func main() {
    a := []int{1, 2, 3, 5: 100}
    fmt.Println(a)      // [1,2,3,0,0,100]
    fmt.Println(len(a)) // 6
    fmt.Println(cap(a)) // 6

    b := []int{}
    fmt.Println(b)        // []
    fmt.Println(len(b))   // 0
    fmt.Println(cap(b))   // 0
    fmt.Println(b == nil) // false
}

这里可以看到,我们可以直接索引值来初始化

截取

选择截取数组或slice的一部分,注意截取的范围不能超过底层数组的索引范围

方式1 : arr[i:j]

func main() {
    a := make([]int, 5, 10)
    for i := 0; i < len(a); i++ {
        a[i] = i
    }
    i := 1
    j := 3
    b := a[i:j]         // 长度索引范围是 [1:3),但是 cap 是 cap(a) - i
    fmt.Println(b)      // [1,2]
    fmt.Println(len(b)) // 2,这里表示 b 的长度,即 j-i
    fmt.Println(cap(b)) // 9,这里表示 b 的容量,即 cap(a) - i
}

方式2 : arr[i:j:k]

func main() {
    a := make([]int, 5, 10)
    for i := 0; i < len(a); i++ {
        a[i] = i
    }
    i := 1
    j := 3
    k := 5
    b := a[i:j:k]       // 长度索引范围是 [1:3),但是 cap 是 k-i
    fmt.Println(b)      // [1,2]
    fmt.Println(len(b)) // 2,这里表示 b 的长度,即 j-i
    fmt.Println(cap(b)) // 4,这里表示 b 的容量,即 k - i
}

以下对于不能超过底层数组的边界有一个图解

func main() {
    a := make([]int, 5, 10)
    fmt.Println(a)      // [0,0,0,0,0]
    fmt.Println(len(a)) // 5
    fmt.Println(cap(a)) // 10

    b := a[2:7]         // 这里已经超过了 a 的 len
    fmt.Println(b)      // [0,0,0,0,0]
    fmt.Println(len(b)) // 5
    fmt.Println(cap(b)) // 8

    // 以下超过了选择范围超过了 a 的 cap 的范围
    //c := a[3:11]
    //fmt.Println(c)

    d := a[1:2:5]
    fmt.Println(d)      // [0]
    fmt.Println(len(d)) // 1
    fmt.Println(cap(d)) // 4

    //e := a[4:7:11]
    //fmt.Println(e)
    //fmt.Println(len(e))
    //fmt.Println(cap(e))

    f := b[2:8]         // 这里也超过了 b 的 len
    fmt.Println(f)      // [0,0,0,0,0,0]
    fmt.Println(len(f)) // 6
    fmt.Println(cap(f)) // 6

    //g := b[2:9]
    //fmt.Println(g)
    //fmt.Println(len(g))
    //fmt.Println(cap(g))
}

具体细节如下图

切片

注意其中的 f 和 g ,是从 b 切片取出来的,但从 f 和 g 来看,我们知道这里的边界不是按照 b ,还是按照底层的 a 的 len 和 cap 来判断

new

func main() {
    a := *new([]int)
    fmt.Println(a)      // []
    fmt.Println(len(a)) // 0
    fmt.Println(cap(a)) // 0
}

空切片 和 nil切片 的概念

以上4个方式在日常使用上并没有什么不同,不过具体底层的数据是不同的,这里再次感谢老钱(原文在文末),引申出「空切片」和 「nil 切片」这两个概念,看下图

nil切片和空切片

这里先来一个延伸,在代码中执行 fmt.Println(slice) 的时候,汇编中会调用 runtime.convTslice ,如下

func convTslice(val []byte) (x unsafe.Pointer) {
    if (*slice)(unsafe.Pointer(&val)).array == nil { // 标识
        x = unsafe.Pointer(&zeroVal[0])
    } else {
        x = mallocgc(unsafe.Sizeof(val), sliceType, true)
        *(*[]byte)(x) = val
    }
    return
}

在上面代码中,我们可以将一个 slice对象 对应其本质也是 []type 来转化(「标识」 部分),下面代码可以看到

nil 指针如下

func main() {
    var a []int
    fmt.Println(a == nil) //true

    // 这里不用 []int 是因为已知 slice 结构只有3个字段
    //a1 := *(*[]int)(unsafe.Pointer(&a))
    a1 := *(*[3]int)(unsafe.Pointer(&a))
    fmt.Println(a1[0]) // 0,表示 array
    fmt.Println(a1[1]) // 0,表示 len
    fmt.Println(a1[2]) // 0,表示 cap

    fmt.Println("---------------------")

    b := *new([]int)
    fmt.Println(b == nil) //true
    b1 := *(*[3]int)(unsafe.Pointer(&b))
    fmt.Println(b1[0]) // 0,表示 array
    fmt.Println(b1[1]) // 0,表示 len
    fmt.Println(b1[2]) // 0,表示 cap
}

我们可以看到,a 和 b 对应的 array 字段的值都是 0

空指针如下

func main() {
    c := []int{}
    fmt.Println(c == nil) //false
    c1 := *(*[3]int)(unsafe.Pointer(&c))
    fmt.Println(c1[0]) // 824634227792,表示 array
    fmt.Println(c1[1]) // 0,表示 len
    fmt.Println(c1[2]) // 0,表示 cap

    fmt.Println("---------------------")

    e := make([]int, 0)
    fmt.Println(e == nil) //false
    e1 := *(*[3]int)(unsafe.Pointer(&e))
    fmt.Println(e1[0]) // 824634227792,表示 array
    fmt.Println(e1[1]) // 0,表示 len
    fmt.Println(e1[2]) // 0,表示 cap
}

可以看到 c、e 底层对应的 array 为固定值,从上文的「创建方式- make」可以知道为 zerobase 的地址

总计如下

创建方式切片种类
直接声明nil 切片
make空切片
newnil 切片
字面表示([]int{})空切片

要如何选择呢

情况1:

判断json格式的时候,会产生如下的情况,看场景的需要

type Out struct {
    Tmp []int //这里要保证是大写开头
}

func main() {
    s := Out{}
    var a []int
    s.Tmp = a
    byt, _ := json.Marshal(s)
    fmt.Println(string(byt)) //{"Tmp":null}

    b := make([]int, 0)
    s.Tmp = b
    byt, _ = json.Marshal(s)
    fmt.Println(string(byt)) //{"Tmp":[]}
}

情况2: 在其他日常使用的时候,官方推荐选择 nil 切片,可以看如下链接查看详情 github.com/golang/go/w…

问题2: 子切片 append 是否影响父切片

场景1: b 进行了 a 的切片 ,然后再 append 的时候,为啥会影响了 a 呢

func main() {
    a := make([]int, 1, 10)
    fmt.Println("before:", a) // before : [0]
    add(a)
    fmt.Println("after:", a) // after : [10] // 这里进行了变更
}

func add(src []int) {
    src = append(src, 1)
    src[0] = 10
    fmt.Println("add:", src) //do : [10,1]
}

场景2:按 「场景1」 中的描述,使用 append 会影响老的部分,此时为啥又没有影响,原因是什么呢?

func main() {
    a := make([]int, 1)
    fmt.Println("before:", a) // before : [0]
    add(a)
    fmt.Println("after:", a)  // after : [0] // 这里原来的没有变更
}

func add(src []int) {
    src = append(src, 1)
    src[0] = 10
    fmt.Println("add:", src) //do : [10,1]
}

我们看下文

append 使用方式

通过 append 给 slice 尾部进行添加,如果底层数组的空间不够会重新生产一个 slice ,然后将数据复制过去(这里后续会进行讲解)

方式1:append(slice, elem)

func main() {
    a := make([]int, 1)
    fmt.Println(a)      // [0]
    fmt.Println(len(a)) // 1
    fmt.Println(cap(a)) // 1

    a = append(a, 2)
    fmt.Println(a)      // [0,2]
    fmt.Println(len(a)) // 2
    fmt.Println(cap(a)) // 2
}

方式2: append(slice, slice2...)

func main() {
    a := []int{1, 2, 3, 4, 5, 6}
    var b []int
    //虽然方式2中有3个参数,但是 append 之后,我们还是只要其 len,而不是 cap
    b = append(b, a[1:3:5]...) // 
    fmt.Println(b)             // [2,3]
    fmt.Println(len(b))        // 2
    fmt.Println(cap(b))        // 2
}

问题解析

场景1如下图,虽然 b 来添加,但是 a 和 b 的底层数组是一致的,因此 b 的操作影响了 a 的数据

append和切片

那场景2 为啥又不会造成影响呢,我们将 add 函数用汇编语言来看下

"".add STEXT size=440 args=0x18 locals=0x98
        ...
        0x002f 00047 (main.go:13)       MOVQ    "".src+160(SP), DX // DX 表示 src 的 array
        0x0037 00055 (main.go:13)       MOVQ    "".src+168(SP), BX // BX 表示 src 的 len
        0x003f 00063 (main.go:13)       PCDATA  $1, $1
        0x003f 00063 (main.go:13)       MOVQ    "".src+176(SP), SI // SI 表示 src 的 cap
        0x0047 00071 (main.go:13)       LEAQ    1(BX), DI // Di=BX+1,即 len+1
        0x004b 00075 (main.go:13)       CMPQ    DI, SI // 这就是将 len+1 和cap 对比
        0x004e 00078 (main.go:13)       JLS     85 // 如果 len+1 不高于 cap,则跳到 85
        0x0050 00080 (main.go:13)       JMP     349 // 这里跳转到 349
        ...
        0x015d 00349 (main.go:13)       PCDATA  $0, $1
        0x015d 00349 (main.go:13)       MOVQ    BX, ""..autotmp_5+64(SP)
        0x0162 00354 (main.go:13)       PCDATA  $0, $5
        0x0162 00354 (main.go:13)       LEAQ    type.int(SB), AX // 获取 int 类型的地址
        0x0169 00361 (main.go:13)       PCDATA  $0, $1
        0x0169 00361 (main.go:13)       MOVQ    AX, (SP)   // 表示 slice 的 type
        0x016d 00365 (main.go:13)       PCDATA  $0, $0
        0x016d 00365 (main.go:13)       MOVQ    DX, 8(SP)  // 表示 old slice 的 array
        0x0172 00370 (main.go:13)       MOVQ    BX, 16(SP) // 表示 old slice 的 len
        0x0177 00375 (main.go:13)       MOVQ    SI, 24(SP) // 表示 old slice 的 cap
        0x017c 00380 (main.go:13)       MOVQ    DI, 32(SP) // 表示传参对应的 cap 
        0x0181 00385 (main.go:13)       CALL    runtime.growslice(SB) // 这里表示新建一个 slice
        0x0186 00390 (main.go:13)       PCDATA  $0, $1
        0x0186 00390 (main.go:13)       MOVQ    40(SP), DX // 这里表示 new slice 的 array 
        0x018b 00395 (main.go:13)       MOVQ    48(SP), AX // 这里表示 new slice 的 len
        0x0190 00400 (main.go:13)       MOVQ    56(SP), SI // 这里表示 new slice 的 cap
        ...

可以看到「汇编代码的第9行」,场景 2 中由于 len >= cap,所以会新建了一个 slice ,所以此时的 slice 的底层数组地址与原来的不一致,所以不会影响老的 slice 的底层数组,具体的 growslice 相关部分如下

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

    var p unsafe.Pointer
    // 执行到这里,说明已经拿到了新的地址
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        ...
    } else {
        p = mallocgc(capmem, et, true)
        ...
    }
    memmove(p, old.array, lenmem) // 这里将老的数组数据来进行迁移

    return slice{p, old.len, newcap} //然后返回新的 slice
}

那按以上的说法,如果没有新建一个slice ,那就会有影响了,以下代码来证明

func main() {
    a := make([]int, 1, 2)
    fmt.Println("before:", a) // before : [0]
    add(a)
    fmt.Println("after:", a) // after : [10] //这里原来的受到了影响
}

func add(src []int) {
    src = append(src, 1)
    src[0] = 10
    fmt.Println("do:", src) // do : [10,1]
}

问题3:append 对于 nil 的切片是有什么特殊处理吗

如下代码,一般对于go中对于 nil 都是报 panic 的处理,那底层有什么神奇力量呢?

func main() {
    var a []int
    fmt.Println(a == nil) //true
    a = append(a, 1)
    fmt.Println(a) //[1]
}

问题解析

通过汇编 go tool compile -S -l -N main.go 得到,看到

"".main STEXT size=152 args=0x0 locals=0x60
        ...
        0x0030 00048 (main.go:5)        LEAQ    type.int(SB), AX
        0x0037 00055 (main.go:5)        PCDATA  $0, $0
        0x0037 00055 (main.go:5)        MOVQ    AX, (SP) // 这里表示 *_type
        0x003b 00059 (main.go:5)        XORPS   X0, X0
        0x003e 00062 (main.go:5)        MOVUPS  X0, 8(SP)  // 这里表示 old.array
        0x0043 00067 (main.go:5)        MOVQ    $0, 24(SP) // 这里表示 old.cap 
        0x004c 00076 (main.go:5)        MOVQ    $1, 32(SP) // 这里表示传参数对应的cap,为1
        0x0055 00085 (main.go:5)        CALL    runtime.growslice(SB) // 这里具体看上文
        0x005a 00090 (main.go:5)        PCDATA  $0, $1
        0x005a 00090 (main.go:5)        MOVQ    40(SP), AX // 这里表示新生成的slice的array
        0x005f 00095 (main.go:5)        MOVQ    48(SP), CX // 这里表示新生成的 slice的 len
        0x0064 00100 (main.go:5)        MOVQ    56(SP), DX // 这里表示新生成的slice的cap
        0x0069 00105 (main.go:5)        INCQ    CX            // 这里表示新生成的 len + 1
        0x006c 00108 (main.go:5)        JMP     110
        0x006e 00110 (main.go:5)        MOVQ    $1, (AX)   // 这里表示将新生成的 array [0]的位置,设置为1
        ...

因此分别对应上 func growslice(et *_type, old slice, cap int) slice 的部分,得到内部会创建一个 cap 为 1 的切片,可以看上面的注释,关键点在「第9行」

问题4:切片扩容的规律是什么呢,每次乘以2?

如下代码,看到每次 append 一个数字后,cap 每次扩容都是乘以 2,真的是这样吗?

func main() {
    a := make([]int, 0)
    a = append(a, 1)
    fmt.Println(len(a)) //1
    fmt.Println(cap(a)) //1

    a = append(a, 1)
    fmt.Println(len(a)) //2
    fmt.Println(cap(a)) //2

    a = append(a, 1)
    fmt.Println(len(a)) //3
    fmt.Println(cap(a)) //4
}

我们来整体看下 growslice 的源码

growslice 源码解析

func growslice(et *_type, old slice, cap int) slice {
    ...
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }

    // et.size 表示切片的类型大小
    if et.size == 0 { // 类型为0,则表示 struct{}类型
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }

    newcap := old.cap // 老的 slice 的cap
    doublecap := newcap + newcap //这里表示老的 slice 的 cap*2
    if cap > doublecap {// 如果这次要求 cap 大于 oldcap*2,则最终的cap为所需要的cap
        newcap = cap
    } else {
        if old.len < 1024 { // 如果 老slice 的len 小于1024,则最终的cap 为老cap*2
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap { // 如果newcap小于要求的cap,则 newcap 以 5/4倍增加,直到超过cap
                newcap += newcap / 4
            }
            if newcap <= 0 {//如果这里newcap 的数值溢出,则最终cap为cap 
                newcap = cap
            }
        }
    }

    ...

    var p unsafe.Pointer
    // 执行到这里,说明已经拿到了新的地址
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        ...
    } else {
        p = mallocgc(capmem, et, true)
        ...
    }
    memmove(p, old.array, lenmem) // 这里将老的数组数据来进行迁移

    return slice{p, old.len, newcap} //然后返回新的 slice
}

规律总结

当 老slice 的len 小于 1024 的时候

问题5:Golang 中 slice 和数组有什么区别呢?

代码写的比较草率,目的达到就行

func main() {
    a := []int{0, 0}
    fmt.Println(a)      //[0,0]
    fmt.Println(len(a)) // 0
    fmt.Println(cap(a)) // 0
    doSlice(a)
    fmt.Println(a) //[1,0]

    fmt.Println("-----")

    b := [2]int{}
    fmt.Println(b)      //[0,0]
    fmt.Println(len(b)) // 2
    fmt.Println(cap(b)) // 2
    doArray(b)
    fmt.Println(b) //[0,0]

    a = append(a, 1)
    //b = append(b, 1) // 数组不能使用 append
}

func doArray(src [2]int) {
    src[0] = 1
}

func doSlice(src []int) {
    src[0] = 1
}

不同的点:

  • 数组的 len 和 cap 是固定的且一致,而切片的 len <= cap ,且两者随着 append 而改变

  • 数组不会影响原来的数据,slice 在不扩容的情况下,会影响原来的数据

  • 数组不能使用 append 来填充,而 slice 可以

切片也可以通过 数组截取来获得,但注意不要超过数组的底层数组的索引范围

参考资料(不分先后)

《深度解密 go 语言之slice - 饶全成大佬》

《深度解析 Go 语言中「切片」的三种特殊状态 - 老钱大佬》

《golang 汇编》

《golang汇编条件跳转命令》

《汇编指令解释》

---------------------------- 这是分割线 ------------------------

以下是我的公众号,欢迎调戏

公号