每个语言都有自己的动态数组,甚至有的语言数组本身就是动态的,这里的动态是指长度可变,从而可以更灵活的应对各种情况。Go 语言也不例外,它提供了常规数组,也提供了动态数组 —— 切片。

Struct

切片的结构很简单,只有三个字段,指向底层数组的指针,还有底层数组的容量,以及当前长度。

  1. type slice struct {
  2. array unsafe.Pointer
  3. len int
  4. cap int
  5. }

代码 1:slice struct
Untitled Diagram-Page-4.png
图 1:slice struct

Go 语言支持通过内置函数 len(slice)cap(slice) 获取切片长度以及容量,这两个函数在编译后会自动变为直接取地址,所以不用担心其性能。

Append

追加是切片最常用的操作,通过编译之后,append 等同于下面代码,对于容量足够的切片没什么好说的,就是直接对 len 以及后面的元素赋值就可以了。L5 ~ L9 是在新的切片长度大于底层数组容量时,将底层数组扩大。

  1. // slice = append(slice, 1, 2, 3)
  2. a := &slice
  3. ptr, len, cap := slice
  4. newlen := len + 3
  5. if uint(newlen) > uint(cap) {
  6. newptr, len, newcap = growslice(slice, newlen)
  7. vardef(a)
  8. *a.cap = newcap
  9. *a.ptr = newptr
  10. }
  11. newlen = len + 3
  12. *a.len = newlen
  13. *(ptr+len) = 1
  14. *(ptr+len+1) = 2
  15. *(ptr+len+2) = 3

代码 2:append
Untitled Diagram-Page-5.png
图 2:append

这里可以分三种情况,分别是:

  1. new_cap > 2 * old_cap: cap = new_cap
  2. new_cap < 2 old_cap && old_cap < 1024: cap = 2 old_cap
  3. new_cap < 2 * old_cap && old_cap > 1024: cap = old_cap + old_cap / 4(until cap > new_cap) ```go newcap := old.cap doublecap := newcap + newcap if cap > doublecap {
    1. newcap = cap
    } else {
    1. if old.cap < 1024 {
    2. newcap = doublecap
    3. } else {
    4. // Check 0 < newcap to detect overflow
    5. // and prevent an infinite loop.
    6. for 0 < newcap && newcap < cap {
    7. newcap += newcap / 4
    8. }
    9. // Set newcap to the requested cap when
    10. // the newcap calculation overflowed.
    11. if newcap <= 0 {
    12. newcap = cap
    13. }
    14. }
    }
  1. **代码 3growslice newcap**
  2. ![slice.svg](https://cdn.nlark.com/yuque/0/2022/svg/207821/1647263637458-db77cc20-3301-4971-b13c-0df6b873730e.svg#clientId=uca19e8d0-de4b-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=ud19ac941&margin=%5Bobject%20Object%5D&name=slice.svg&originHeight=341&originWidth=929&originalType=binary&ratio=1&rotation=0&showTitle=false&size=16949&status=done&style=none&taskId=u26bd5c22-1c20-4a63-8723-0a134254601&title=)<br />**图 3:growslice newcap**
  3. ```go
  4. var overflow bool
  5. var lenmem, newlenmem, capmem uintptr
  6. // Specialize for common values of et.size.
  7. // For 1 we don't need any division/multiplication.
  8. // For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
  9. // For powers of 2, use a variable shift.
  10. switch {
  11. case et.size == 1:
  12. lenmem = uintptr(old.len)
  13. newlenmem = uintptr(cap)
  14. capmem = roundupsize(uintptr(newcap))
  15. overflow = uintptr(newcap) > maxAlloc
  16. newcap = int(capmem)
  17. case et.size == sys.PtrSize:
  18. lenmem = uintptr(old.len) * sys.PtrSize
  19. newlenmem = uintptr(cap) * sys.PtrSize
  20. capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
  21. overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
  22. newcap = int(capmem / sys.PtrSize)
  23. case isPowerOfTwo(et.size):
  24. var shift uintptr
  25. if sys.PtrSize == 8 {
  26. // Mask shift for better code generation.
  27. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
  28. } else {
  29. shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
  30. }
  31. lenmem = uintptr(old.len) << shift
  32. newlenmem = uintptr(cap) << shift
  33. capmem = roundupsize(uintptr(newcap) << shift)
  34. overflow = uintptr(newcap) > (maxAlloc >> shift)
  35. newcap = int(capmem >> shift)
  36. default:
  37. lenmem = uintptr(old.len) * et.size
  38. newlenmem = uintptr(cap) * et.size
  39. capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
  40. capmem = roundupsize(capmem)
  41. newcap = int(capmem / et.size)
  42. }

代码 4:multiplication optimization

在计算真正需要分配的内存的大小,这里是根据 size 是否为 2 的倍数,进行了一些优化

  1. size = 1: 不需要处理
  2. size = sys.PtrSIze:这里看上去是直接相乘,但是因为 sys.PtrSize 是常数,所以编译器会优化为左移
  3. size 是 2 的倍数:左移优化
  4. size 不是 2 的倍数:直接相乘

另外还需要注意一下 roundupsize 函数,这个函数就是将分配的内存向上取整,这和 Go 语言的内存分配策略有关,可以减少内存碎片。