Go 语言切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

源码

slice 相关源码位于/src/runtime/slice.go

底层结构

1
2
3
4
5
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • array 指向一块连续内存,该内存中存放的是 slice 中的数据
  • len slice 当前长度
  • cap slice 当前容量

构造切片

该函数用来构造一个 slice 对象,源码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func makeslice(et *_type, len, cap int) unsafe.Pointer {
   // MulUintptr 函数利用一些已有常量计算所需内存大小并判断是否溢出。
   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()
   }
   
   // 申请 mem 大小的内存并返回内存首地址
   return mallocgc(mem, et, true)
}

// 总的来说就是申请一个在内存足够下的容量为

切片扩容

append()发现需要扩容时,则会调用growslice() 方法

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
func growslice(et _type, old slice, cap int) slice {
    if raceenabled {
        callerpc := getcallerpc()
        racereadrangepc(old.array, uintptr(old.lenint(et.size)), callerpc, funcPC(growslice))
    }
    if msanenabled {
        msanread(old.array, uintptr(old.len*int(et.size)))
    }
    // 如果 cap < old.cap,说明 cap 溢出
    if cap < old.cap {
        panic(errorString("growslice: cap out of range"))
    }
    // 如果 et.size, 说明类型大小为 0, 直接返回 zerobase 类型的新切片
    if et.size == 0 {
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
    
    newcap := old.cap
    doublecap := newcap + newcap
    // 如果需要的容量大于老容量的2倍, 直接将扩容后新容量设置为需要的容量大小
    if cap > doublecap {
        newcap = cap
    } else {
        // 如果老容量小于1024, 2倍扩容
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // 以1.25倍增大新容量, 直到溢出或者新容量大于需要的容量为止
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // 如果溢出,那么将新容量设置为需要的容量
            if newcap <= 0 {
                newcap = cap
            }
        }
    }
    
    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    switch {
      // 当类型大小为1时,不需要乘除计算就能够得到所需要的值  
      case et.size == 1:
        lenmem = uintptr(old.len)
        newlenmem = uintptr(cap)
        // 返回mallocgc将分配的内存块的大小。
        // 也就是,由Go语言的内存管理模块返回给你需要的内存块,
        // 通常这些内存块都是预先申请好,并且被分为常用的规格,比如8,16, 32, 48, 64等。
        capmem = roundupsize(uintptr(newcap))
        // 判断是否溢出
        overflow = uintptr(newcap) > maxAlloc
        // 内存对齐,进一步调整newcap
        newcap = int(capmem)
      // 当类型大小是8个字节时  
      case et.size == sys.PtrSize:
        lenmem = uintptr(old.len) * sys.PtrSize
        newlenmem = uintptr(cap) * sys.PtrSize
        capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
        overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
        newcap = int(capmem / sys.PtrSize)
      // 当类型大小是2的幂次方时  
      case isPowerOfTwo(et.size):
        var shift uintptr
        if sys.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)
    }
    
    // 如果发生了溢出
    if overflow || capmem > maxAlloc {
        panic(errorString("growslice: cap out of range"))
    }
    
    var p unsafe.Pointer
    // 如果是切片类型是指针类型,那么会调用 memclrNoHeapPointers 将
    // newlenmem 以后的 capmem-newlenmem 全部置 0
    if et.ptrdata == 0 {
        // 申请一块capmem大小的连续内存并返回首地址
        p = mallocgc(capmem, nil, false)
        memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
    } else {
        p = mallocgc(capmem, et, true)
        if lenmem > 0 && writeBarrier.enabled {
            bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
        }
    }
    // 将老切片上的元素移动到新的内存中
    memmove(p, old.array, lenmem)
    // 返回新的切片
    return slice{p, old.len, newcap}
}

总结一下:

变量 含义 说明
old.cap 扩容前切片容量
newcap 预估容量 默认为扩容前切片容量(old.cap)
cap 扩容后至少需要的最小容量 old.cap + 本次新增的元素数量
doublecap 扩容前切片的2倍容量 old.cap * 2

大致的流程如下:

  • 切片类型大小为 0 则直接返回新的切片
  • cap > doublecap
    • 是 :newcap 直接扩容到 cap大小
    • 否:判读 cap < 1024
      • 是:newcap = doublecap 容量较小直接进行双倍扩容
      • 否:newcap = oldcap + oldcap / 4 进行1.25倍扩容
  • 根据切片类型大小计算的得到新的切片长度以及容量,其中 roundupsize()会对齐内存,提高内存的分配效率并减少碎片;
  • 拷贝旧数据到新切片中并返回新切片

切片拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
   if fromLen == 0 || toLen == 0 {
      return 0
   }

   n := fromLen
   if toLen < n {
      n = toLen
   }
   
   if width == 0 {
      return n
   }

   size := uintptr(n) * width
   if raceenabled {
      callerpc := getcallerpc()
      pc := funcPC(slicecopy)
      racereadrangepc(fromPtr, size, callerpc, pc)
      racewriterangepc(toPtr, size, callerpc, pc)
   }
   if msanenabled {
      msanread(fromPtr, size)
      msanwrite(toPtr, size)
   }

   if size == 1 {
      // 如果就1个元素直接赋值
      *(*byte)(toPtr) = *(*byte)(fromPtr)
   } else {
      memmove(toPtr, fromPtr, size)
   }
   return n
}

基础操作

  • slice init
声明方式 示例
直接声明 var slice []int
new new([]int)
字面量 []int{1, 2, 3}
make make([]int, 1, 2)
  • copy()调用 slicecopy 方法实现
1
2
3
4
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
  • append(),在指定切片后追加元素;当append()发现需要扩容时,则会调用growslice() 方法
1
2
3
4
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

append()函数调用时,有可能会调用 growslice()方法发生切片扩容,扩容后会返回一个新的切片,所以我们在 append()调用时应接收返回值。

  • 截取
    • s[n] 切片s中索引位置为n的项
    • s[:] 从切片s的索引位置0到len(s)-1处所获得的切片
    • s[low:] 从切片s的索引位置low到len(s)-1处所获得的切片
    • s[:high] 从切片s的索引位置0到high处所获得的切片,len=high
    • s[low:high] 从切片s的索引位置low到high处所获得的切片,len=high-low
    • s[low:high:max] 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low

由于截取的后的切片与原切片共用一份数组内存,所以在截取后的切片中修改切片元素会导致内存中数据被修改,被截取的切片 s也会被修改,使用截取时需要注意。