Go底层原理--切片

151 阅读2分钟

Go底层原理--切片

切片的组成

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
​
SliceHeader 是切片的运行时表示形式。
它不能安全或便携地使用,其表示形式可能在更高版本中的更改。
此外,“数据”字段不足以保证数据它的引用不会被垃圾回收,所以程序必须保持指向基础数据的单独、正确键入的指针。
​
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  • Data是指向切片元素对应的底层数组元素的地址的指针
  • Len表示切片中元素的数目
  • Cap表示切片的容量

切片的初始化

切片字面量的初始化,会以数组的形式存储与静态区中。在使用make函数初始化时,如果make函数初始化了一个大于64KB的切片,那么这个切片会逃逸到堆中,在运行时调用makeslice函数创建切片,小于64KB的切片直接在栈中初始化

切片值复制和数据引用

数组的复制是值复制,对于数组副本的修改不会影响原来的数组,但是对于切片的副本的修改会影响原来的切片。这说明切片副本与原始切片共用一个内存。

但是在Go语言中,切片的复制也是值复制,但是不同于复制整个切片,切片的值复制指的是对于运行时SliceHeader结构的复制。如图7-3所示,底层指针仍然指向相同的底层数据的数组地址。

40BDB69BF9A9D5CF1CD32C9BC2306447.png

切片扩容原理

func growslice(et *_type, old slice, cap int) slice {
    //新容量是旧切片的容量
    newcap := old.cap
    //双倍容量是两个新(旧)容量相加
    doublecap := newcap + newcap
    
    if cap > doublecap {
        //如果新申请的容量(cap)大于2倍的old.cap,新容量(newcap)就是申请的容量
        newcap = cap
    } else {
    //当新申请的容量(cap)不大于2倍的old.cap
        //定义一个常量threshold(门槛)长度为256
        const threshold = 256
        //如果旧容量小于256
        if old.cap < threshold {
            //新容量就是2倍旧容量
            newcap = doublecap
        } else {
            //检查新容量(newcap)是否大于0并且新容量(newcap)是否小于申请的容量(cap),以防溢出并防止无限循环。
            for 0 < newcap && newcap < cap {
                //从小切片增长 2 倍过渡到大切片增长1.25倍。此公式在两者之间提供平滑的过渡。
                newcap += (newcap + 3*threshold) / 4
            }
            //在以下情况下将新上限设置为请求的上限
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    
    return slice{p, old.len, newcap}
}

growslice函数会根据切片的类型,分配不同大小的内存。为了内存对齐,申请的内存可能存在大于实际的类型大小*容量大小。

如果切片需要扩容,那么最终需要到堆区申请内存。需要注意的是,扩容后新的切片不一定拥有新的地址。因此在使用append函数时,通常会采用a=append(a,T)的形式。根据et.ptrdata判断是否切片类型为指针,指向不同的逻辑。

当切片类型不是指针时,分配内存后只需要将内存后面的值清空,memmove(p,old.array,lenmem)函数用于将旧切片的值赋值给新的切片。整个过程的抽象如下。

old = make([]int,3,3)
new = append(old,1) => new = malloc(newcap*sizeof(int))
new[1]=old[1]
new[2]=old[2]
new[3]=old[3]

当切片类型为指针时,涉及垃圾回收写屏障开启时,对旧切片中指针指向的对象进行标记。