golang笔记——深入了解Slice底层原理

大家可以看下面这道关于slice的题目,通过这道题我们可以对slice的特性和注意事项有一个深入理解。

package main

import "fmt"

func main() {
    a := [...]int{0, 1, 2, 3}
    x := a[:1]
    y := a[2:]
    x = append(x, y...)
    x = append(x, y...)
    fmt.Println(a, x)
}
  • A. [0 1 2 3] [0 2 3 3 3]
  • B. [0 2 3 3] [0 2 3 3 3]
  • C. [0 1 2 3] [0 2 3 2 3]
  • D. [0 2 3 3] [0 2 3 2 3]
    这道题有几个考点:
  1. slice的底层数据结构是什么?给slice赋值,到底赋了什么内容?
  2. 通过:操作得到的新slice和原slice是什么关系?新slice的长度和容量是多少?
  3. append在背后到底做了哪些事情?
  4. slice的扩容机制是什么?

解析


Slice的底层数据结构

slice定义在src/runtime/slice.go第15行,源码地址:https://github.com/golang/go/blob/master/src/runtime/slice.go#L15。

Pointer定义在src/unsafe/unsafe.go第184行,源码地址:https://github.com/golang/go/blob/master/src/unsafe/unsafe.go#L184。

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

type Pointer *ArbitraryType

slice实际上是一个结构体类型,包含3个字段,分别是

  • array: 是指针,指向一个数组,切片的数据实际都存储在这个数组里。
  • len: 切片的长度。
  • cap: 切片的容量,表示切片当前最多可以存储多少个元素,如果超过了现有容量会自动扩容。

因此给slice赋值,实际上都是给slice里的这3个字段赋值。看起来这像是一句正确的废话,但是相信我,记住这句话可以帮助你非常清晰地理解对slice做修改后slice里3个字段的值是怎么变的,slice 指向的底层数组的数据是怎么变的。

:分割操作符

:分割操作符有几个特点:

  1. :可以对数组或者slice做数据截取,:得到的结果是一个新slice

  2. slice结构体里的array指针指向原数组或者原slice的底层数组,新切片的长度是右边的数值减去左边的数值,新切片的容量是原切片的容量减去:左边的数值。

  3. :的左边如果没有写数字,左边的默认值是0,右边如果没有写数字,右边的默认值是被分割的数组或被分割的切片的长度,注意,是长度不是容量。

    a := make([]int, 0, 4) // a的长度是0,容量是4
    b := a[:] // 等价于 b := a[0:0], b的长度是0,容量是4
    c := a[:1] // 等价于 c := a[0:1], c的长度是1,容量是4
    d := a[1:] // 编译报错 panic: runtime error: slice bounds out of range
    e := a[1:4] // e的长度3,容量3
    
  4. :分割操作符右边的数值有上限,上限有2种情况

  • 如果分割的是数组,那上限是是被分割的数组的长度。
  • 如果分割的是切片,那上限是被分割的切片的容量。注意,这个和下标操作不一样,如果使用下标索引访问切片,下标索引的最大值是(切片的长度-1),而不是切片的容量。

一图胜千言,我们通过下面的示例来讲解下切片分割的机制。

下图表示slice结构,ptr表示array指针,指向底层数组,lencap分别是切片的长度和容量。

step1: 我们通过代码s := make([]byte, 5, 5)来创建一个切片s,长度和容量都是5,结构示意如下:

step2: 现在对切片s做分割s2 := s[2:4],得到一个新切片s2,结构如下。

  • s2还是指向原切片s的底层数组,只不过指向的起始位置是下标索引为2的位置。
  • s2的长度len(s2)是2,因为s2 := s[2:4]只是截取了切片s下标索引为2和3的2个元素。
  • s2的容量cap(s2)是3,因为从s2指向的数组位置到底层数组末尾,可以存3个元素。
  • 因为长度是2,所以只有s2[0]s2[1]是有效的下标索引访问。但是,容量为3,s2[0:3]是一个有效的分割表达式。

step3: 对切片s做分割s3 := s2[:cap(s2)],得到一个新切片s3,结构如下:

  • s3指向切片s2的底层数组,同样也是s的底层数组,指向的起始位置是s2的起始位置,对应数组下标索引为2的位置。
  • s3的长度len(s3)是3,因为s3 := s2[:cap(s2)]截取了切片s2 下标索引为0,1,2的3个元素。
  • s3的容量cap(s3)是3,因为从s3指向的数组位置到底层数组末尾,可以存3个元素。

因此,对数组或者切片做:分割操作产生的新切片还是指向原来的底层数组,并不会把原底层数组的元素拷贝一份到新的内存空间里。

正是因为他们指向同一块内存空间,所以对原数组或者原切片的修改会影响分割后的新切片的值,反之亦然。

append机制

要了解append的机制,直接看源码说明。

// 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 = append(slice, elem1, elem2)
//  slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//  slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
  • append函数返回的是一个切片,append在原切片的末尾添加新元素,这个末尾是切片长度的末尾,不是切片容量的末尾。

    func test() {
      a := make([]int, 0, 4)
      b := append(a, 1) // b=[1], a指向的底层数组的首元素为1,但是a的长度和容量不变
      c := append(a, 2) // a的长度还是0,c=[2], a指向的底层数组的首元素变为2
      fmt.Println(a, b, c) // [] [2] [2]
    }
    
  • 如果原切片的容量足以包含新增加的元素,那append函数返回的切片结构里3个字段的值是:

    • array指针字段的值不变,和原切片的array指针的值相同,也就是append是在原切片的底层数组添加元素,返回的切片还是指向原切片的底层数组
    • len长度字段的值做相应增加,增加了N个元素,长度就增加N
    • cap容量不变
  • 如果原切片的容量不够存储append新增加的元素,Go会先分配一块容量更大的新内存,然后把原切片里的所有元素拷贝过来,最后在新的内存里添加新元素。append函数返回的切片结构里的3个字段的值是:

    • array指针字段的值变了,不再指向原切片的底层数组了,会指向一块新的内存空间
    • len长度字段的值做相应增加,增加了N个元素,长度就增加N
    • cap容量会增加

注意:append不会改变原切片的值,原切片的长度和容量都不变,除非把append的返回值赋值给原切片。

那么问题来了,新切片的容量是按照什么规则计算得出来的呢?

slice扩容机制

slice的扩容机制随着Go的版本迭代,是有变化的。目前网上大部分的说法是下面这个:

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
这里明确告诉大家,这个结论是错误的。

Go 1.18的slice扩容机制为:

  1. 当申请的容量(原 slice 容量+新元素个数)大于2倍原slice容量的时候,新slice 容量变成申请的容量;
  2. 否则原 slice 容量小于 256的时候, 新 slice 容量变成原来的 2 倍;如果原 slice 容量>= 256,则循环执行newcap += (newcap + 3*threshold) / 4,直到newcap>cap
  3. 如果newcap过大越界,则新slice 容量变成申请的容量。

Go 1.18的扩容实现代码如下,growslice的参数et是切片里的元素类型,old是原切片,cap等于原切片的长度+append新增的元素个数。(注意第3个参数cap的值是原切片的长度+append新增元素个数,不是原切片容量+新增元素个数,可以在growslice里打印cap的值来验证)

// src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
    // ...
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                // Transition from growing 2x for small slices
                // to growing 1.25x for large slices. This formula
                // gives a smooth-ish transition between the two.
                newcap += (newcap + 3*threshold) / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    // Specialize for common values of et.size.
    // For 1 we don't need any division/multiplication.
    // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
    // For powers of 2, use a variable shift.
    switch {
    case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        capmem = roundupsize(uintptr(newcap))
        overflow = uintptr(newcap) > maxAlloc
        newcap = int(capmem)
    case et.size == goarch.PtrSize:
        lenmem = uintptr(old.len) * goarch.PtrSize
        newlenmem = uintptr(cap) * goarch.PtrSize
        capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
        newcap = int(capmem / goarch.PtrSize)
    case isPowerOfTwo(et.size):
        var shift uintptr
        if goarch.PtrSize == 8 {
            // Mask shift for better code generation.
            shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
        } else {
            shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
        }
        lenmem = uintptr(old.len) << shift
        newlenmem = uintptr(cap) << shift
        capmem = roundupsize(uintptr(newcap) << shift)
        overflow = uintptr(newcap) > (maxAlloc >> shift)
        newcap = int(capmem >> shift)
    default:
        lenmem = uintptr(old.len) * et.size
        newlenmem = uintptr(cap) * et.size
        capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
        capmem = roundupsize(capmem)
        newcap = int(capmem / et.size)
    }
    // ...
    return slice{p, old.len, newcap}
}

// src/runtime/msize.go:13
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize {
        if size <= smallSizeMax-8 {
            return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]])
        } else {
            return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]])
        }
    }
    if size+_PageSize < size {
        return size
    }
    return alignUp(size, _PageSize)
}

// src/runtime/sizeclass.go:84
const (
    _MaxSmallSize   = 32768
    smallSizeDiv    = 8
    smallSizeMax    = 1024
    largeSizeDiv    = 128
    _NumSizeClasses = 68
    _PageShift      = 13
)

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
var class_to_divmagic = [_NumSizeClasses]uint32{0, ^uint32(0)/8 + 1, ^uint32(0)/16 + 1, ^uint32(0)/24 + 1, ^uint32(0)/32 + 1, ^uint32(0)/48 + 1, ^uint32(0)/64 + 1, ^uint32(0)/80 + 1, ^uint32(0)/96 + 1, ^uint32(0)/112 + 1, ^uint32(0)/128 + 1, ^uint32(0)/144 + 1, ^uint32(0)/160 + 1, ^uint32(0)/176 + 1, ^uint32(0)/192 + 1, ^uint32(0)/208 + 1, ^uint32(0)/224 + 1, ^uint32(0)/240 + 1, ^uint32(0)/256 + 1, ^uint32(0)/288 + 1, ^uint32(0)/320 + 1, ^uint32(0)/352 + 1, ^uint32(0)/384 + 1, ^uint32(0)/416 + 1, ^uint32(0)/448 + 1, ^uint32(0)/480 + 1, ^uint32(0)/512 + 1, ^uint32(0)/576 + 1, ^uint32(0)/640 + 1, ^uint32(0)/704 + 1, ^uint32(0)/768 + 1, ^uint32(0)/896 + 1, ^uint32(0)/1024 + 1, ^uint32(0)/1152 + 1, ^uint32(0)/1280 + 1, ^uint32(0)/1408 + 1, ^uint32(0)/1536 + 1, ^uint32(0)/1792 + 1, ^uint32(0)/2048 + 1, ^uint32(0)/2304 + 1, ^uint32(0)/2688 + 1, ^uint32(0)/3072 + 1, ^uint32(0)/3200 + 1, ^uint32(0)/3456 + 1, ^uint32(0)/4096 + 1, ^uint32(0)/4864 + 1, ^uint32(0)/5376 + 1, ^uint32(0)/6144 + 1, ^uint32(0)/6528 + 1, ^uint32(0)/6784 + 1, ^uint32(0)/6912 + 1, ^uint32(0)/8192 + 1, ^uint32(0)/9472 + 1, ^uint32(0)/9728 + 1, ^uint32(0)/10240 + 1, ^uint32(0)/10880 + 1, ^uint32(0)/12288 + 1, ^uint32(0)/13568 + 1, ^uint32(0)/14336 + 1, ^uint32(0)/16384 + 1, ^uint32(0)/18432 + 1, ^uint32(0)/19072 + 1, ^uint32(0)/20480 + 1, ^uint32(0)/21760 + 1, ^uint32(0)/24576 + 1, ^uint32(0)/27264 + 1, ^uint32(0)/28672 + 1, ^uint32(0)/32768 + 1}
var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
var size_to_class128 = [(_MaxSmallSize-smallSizeMax)/largeSizeDiv + 1]uint8{32, 33, 34, 35, 36, 37, 37, 38, 38, 39, 39, 40, 40, 40, 41, 41, 41, 42, 43, 43, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67, 67}

newcap是扩容后的容量,先根据原切片的长度、容量和要添加的元素个数确定newcap大小,最后再对newcap做内存对齐得到最后的newcap。

答案

我们回到本文最开始的题目,逐行解析每行代码的执行结果。

代码 切片对应结果
a := [...]int{0, 1, 2, 3} a是一个数组,长度是4,值是[ 0 1 2 3]
x := a[:1] x是一个切片,切片里的指针指向数组a的首元素,值是[0],长度1,容量4
y := a[2:] y是一个切片,切片里的指针指向数组a的第2个元素,值是[2 3],长度2,容量2
x = append(x, y...) x的剩余容量还有3个,足以存储y里的2个元素,所以x不会扩容,x的值是[0 2 3],长度3,容量4。因为x, a, y都指向同一块内存空间,所以x的修改影响了a和y。
a的值变为[0 2 3 3],长度4,容量4
y的值变为[3 3],长度2,容量2
x = append(x, y...) x的剩余容量只有1个,不足以存储y里的2个元素,所以要扩容。append(x, y)的结果是得到一个新切片,值是[0 2 3 3 3],长度5,容量8。
append的返回值赋值给x,所以切片x会指向扩容后的新内存。
fmt.Println(a, x) a的值还是[0 2 3 3]没有变化,所以打印结果是[0 2 3 3] [0 2 3 3 3 ],答案是B

加餐:copy机制

Go的内置函数copy可以把一个切片里的元素拷贝到另一个切片,源码定义在src/builtin/builtin.go,代码如下:

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int

copy会从原切片src拷贝 min(len(dst), len(src))个元素到目标切片dst
因为拷贝的元素个数min(len(dst), len(src))不会超过目标切片的长度len(dst),所以copy执行后,目标切片的长度不会变,容量不会变。
注意:原切片和目标切片的内存空间可能会有重合,copy后可能会改变原切片的值,参考下例。

package main
import "fmt"
func main() {
    a := []int{1, 2, 3}
    b := a[1:] // [2 3]
    copy(a, b) // a和b内存空间有重叠
    fmt.Println(a, b) // [2 3 3] [3 3]
}

slice打印和底层数组地址

打印要弄清楚3个问题:

  1. fmt.Println(slice)打印到切片底层数组的哪个元素截止?

    根据切片的长度len,打印到下标索引为len-1的元素截止。比如下例里,虽然切片a的底层数组下标索引len(a)-1后面还有个值1,但是因为a的长度为1,就只打印[0],切片b的长度为2,所以会打印[0 1]

    a := make([]int, 1, 4) // a的长度是1,容量是4
    b := append(a, 1) // 往a的末尾添加元素1,b=[0 1], a的长度还是1,a和b指向同一个底层数组
    fmt.Println(a, b) // [0] [0 1]
    
  2. 如何打印slice结构体变量的地址?

    s := []int{1, 2}
    fmt.Printf("%p\n", &s)
    
  3. 如何打印slice底层数组的地址?有2种方法

    s = make([]int, 2, 3)
    fmt.Printf("%p %p\n", s, &s[0])
    

总结

对于slice,时刻想着对slice做了修改后,slice里的3个字段:指针,长度,容量是怎么变的。

  • slice是一个结构体类型,里面包含3个字段:指向数组的array指针,长度len和容量cap。给slice赋值是对slice里的指针,长度和容量3个字段分别赋值。

  • :分割操作符的结果是一个新切片,slice结构体里的array指针指向原数组或者原slice的底层数组,新切片的长度是右边的数值减去左边的数值,新切片的容量是原切片的容量减去:左边的数值。

  • :分割操作符右边的数值上限有2种情况:

    • 如果分割的是数组,那上限是是被分割的数组的长度。
    • 如果分割的是切片,那上限是被分割的切片的容量。注意,这个和下标操作不一样,如果使用下标索引访问切片,下标索引的最大值是(切片的长度-1),而不是切片的容量。
  • 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。

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

  • 打印slice时,是根据slice的长度来打印的

    a := make([]int, 1, 4) // a的长度是1,容量是4
    b := append(a, 1) // 往a的末尾添加元素1,b=[0 1], a的长度还是1,a和b指向同一个底层数组
    fmt.Println(a, b) // [0] [0 1]
    fmt.Printf("%p %p\n", a, b) // 切片a和b的底层数组地址相同
    
  • Go在函数传参时,没有传引用这个说法,只有传值。网上有些文章写Go的slicemapchannel作为函数参数是传引用,这是错误的,可以参考文章Go有引用变量和引用传递么?

思考题

留下2道思考题,欢迎大家在评论区留下你们的答案。

  • 题目1:

    package main
    
    import "fmt"
    
    func main() {
      a := []int{1, 2}
      b := append(a, 3)
    
      c := append(b, 4)
      d := append(b, 5)
    
      fmt.Println(a, b, c[3], d[3])
    }
    
  • 题目2

    package main
    
    import "fmt"
    
    func main() {
      s := []int{1, 2}
      s = append(s, 4, 5, 6)
      fmt.Println(len(s), cap(s))
    }
    

答案

  • 题目1:
代码 切片对应结果
a := []int{1, 2} a是一个数组,长度是2,值是[ 1 2 ]
b := append(a, 3) a执行append操作后,a的底层数组将扩容,扩容时newcap < doublecap && newcap < 256,最终容量值为4,返回新切片。b接收新切片,长度是3,容量是4。 [ 1 2 3 ]
c := append(b, 4) b执行append操作后,b的底层数组不会扩容。c为一个切片,长度为4, 容量为4,与b共享同一底层数组,底层数组值为[ 1 2 3 4 ]
d := append(b, 5) b执行append操作后,将从b的最后1位元素(3)之后追加值5。d为一个切片,长度为4, 容量为4,与b共享同一底层数组,底层数组值为[ 1 2 3 5 ]
fmt.Println(a, b, c[3], d[3]) 底层数组最终值为[ 1 2 3 5 ],所以打印结果是[ 1 2 ] [ 1 2 3 ] 5 5

  • 题目2:
s := []int{1, 2}
s = append(s, 4, 5, 6)
 // 根据基础的cap增长规则我们可以计算出newcap为5,
// 但是计算出了新容量之后,出于内存的高效利用考虑,还要进行内存对齐
// capmem := roundupsize(uintptr(newcap) * uintptr(et.size))
// newcap就是前文中计算出的newcap,et.size代表slice中一个元素的大小,
// capmem计算出来的就是此次扩容需要申请的内存大小。roundupsize函数就是处理内存对齐的函数。
// 根据源码,我们最终会获得uintptr(class_to_size[size_to_class8[(size+7)>>3]])为内存大小(capmem = 48)
// 执行内存对齐后,最后通过newcap = int(capmem / goarch.PtrSize)获得newcap = 6
fmt.Println(len(s), cap(s)) // 5 6

References

  • https://github.com/jincheng9/go-tutorial/tree/main/workspace/senior/p8
  • https://zhuanlan.zhihu.com/p/61121325
  • https://jodezer.github.io/2017/05/golangSlice的扩容规则
  • https://go101.org/quizzes/slice-1.html
  • https://go.dev/blog/slices-intro
  • https://github.com/golang/go/blob/master/src/runtime/slice.go#L156
  • https://qcrao91.gitbook.io/go/shu-zu-he-qie-pian/qie-pian-de-rong-liang-shi-zen-yang-zeng-chang-de

你可能感兴趣的:(golang笔记——深入了解Slice底层原理)