目录

Go 源码之切片 Slice

一、简介

  • slice是引用类型,底层是一个指向指针的数组,支持动态扩容

  • 底层数据结构(三个字段)

    • array:指向所引用的数组指针(unsafe.Pointer 可以表示任何可寻址的值的指针)

    • len:长度,当前引用切片的元素个数

    • cap:容量,当前引用切片的容量(底层数组的元素总数)

  • 底层扩容机制:当前容量小于1024(256),则 2 倍扩容,反之则为 1.25 倍(2~1.25之间平滑过渡)

  • 扩容创建新的切片,将旧切片数据迁移到新切片,清除旧切片

  • 数组与切片的区别:切片是引用类型,数组是值类型,切片可以动态扩容,底层也是一个数组

二、源码

$GOROOT$\src\runtime\slice.go

(一)数据结构

type slice struct {
    array unsafe.Pointer                                 // 指向所引用的数组指针(`unsafe.Pointer` 可以表示任何可寻址的值的指针)
    len   int                                                     // 长度,当前引用切片的元素个数
    cap   int                                                      // 容量,cap 一定是大于或等于 len 的。否则会导致 panic
}

(二)创建Slice

makeslice64函数只是做了些校验,其内部也是调用makeslice,

makeslice函数主要是通过len、cap计算slice所需内存大小,然后调用mallocgc进行内存的分配。

// 该函数传入需要初始化的切片的类型,长度以及容量,返回的指针会通过调用方组建成一个完成的slice结构体
func makeslice(et *_type, len, cap int) unsafe.Pointer {
   // 调用MulUintptr函数:获取创建该切片需要的内存,以及是否溢出
    mem, overflow := math.MulUintptr(et.size, uintptr(cap))
  //  如果溢出 | 超过能够分配的最大内存(2^32 - 1) | 非法输入, 报错并返回
    if overflow || mem > maxAlloc || len < 0 || len > cap {
    // 下面的注释:
   // 在创建一个长度非常大的切片时,如果超出了底层数组的容量,通常会发生“cap out of range”错误。
    // 但是go会优先返回 len 溢出的错误

        mem, overflow := math.MulUintptr(et.size, uintptr(len))
        if overflow || mem > maxAlloc || len < 0 {
      // panic 错误:len溢出
            panicmakeslicelen()
        }
     // panic 错误:容量溢出
        panicmakeslicecap()
    }
 // 调用mallocgc函数分配一块连续内存并返回该内存的首地址
    return mallocgc(mem, et, true)
}
//MulUintptr返回a*b以及乘法是否溢出。
//在受支持的平台上,这是编译器降低的内在值。
func MulUintptr(a, b uintptr) (uintptr, bool) {
    if a|b < 1<<(4*goarch.PtrSize) || a == 0 {
        return a * b, false
    }
    overflow := b > MaxUintptr/a
    return a * b, overflow
}

(三)append-扩容-growslice

append操作其实是调用了runtime/slice.go中的growslice函数

  • 重新申请内存,之后将数据赋值过来
  • 当原切片cap<1024 时,<新cap> = 2 * <老cap>
  • 当原切片cap>1024 时,<新cap> = 1.25*<老cap>
  • 之后进行内存对齐,内存对齐相关可参考【Golang】详解内存对齐

1.18 最新版本的扩容机制:

  • 小于 256 则 newcap=2 x oldcap
  • 大于 256,则扩容机制 在 2 ~ 1.25 倍 之间,newcap += (newcap + 3*threshold) / 4

(四)切片深拷贝

当我们使用copy时,底层调用的源码函数为makeslicecopy;

这个函数最终就是调用 runtime.memmove 来实现 slice 的 copy 的

func slicecopy(to, fm slice, width uintptr) int {
      // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 
      if fm.len == 0 || to.len == 0 {
        return 0
      }
      // n 记录下源切片或者目标切片较短的那一个的长度
      n := fm.len
      if to.len < n {
        n = to.len
      }
      // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度
      if width == 0 {
        return n
      }
      //如果开启竞争检测
      if raceenabled {
        callerpc := getcallerpc()
        pc := funcPC(slicecopy)
        racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)
        racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)
      }
      if msanenabled {
        msanwrite(to.array, uintptr(n*int(width)))
        msanread(fm.array, uintptr(n*int(width)))
      }
      size := uintptr(n) * width
      if size == 1 { // common case worth about 2x to do here
        // TODO: is this still worth it with new memmove impl?
        //如果只有一个元素,那么直接进行地址转换
        *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
      } else {
        //如果不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址之后,拷贝个数为size
        memmove(to.array, fm.array, size)
      }
      return n
    }

常见问题

1. 为什么要初始化切片的容量?

在已知容量的情况,使用 make([]T, 0 , cap) 初始化切片的容量,可以减少切片的扩容次数,因为每次扩容都需要重新初始化一个新的切片,重新分配内存,并且需要将旧切片的所有元素迁移到新切片,整个过程有一定的消耗。