每个语言都有自己的动态数组,甚至有的语言数组本身就是动态的,这里的动态是指长度可变,从而可以更灵活的应对各种情况。Go 语言也不例外,它提供了常规数组,也提供了动态数组 —— 切片。
Struct
切片的结构很简单,只有三个字段,指向底层数组的指针,还有底层数组的容量,以及当前长度。
type slice struct {
array unsafe.Pointer
len int
cap int
}
代码 1:slice struct
图 1:slice struct
Go 语言支持通过内置函数 len(slice)
和 cap(slice)
获取切片长度以及容量,这两个函数在编译后会自动变为直接取地址,所以不用担心其性能。
Append
追加是切片最常用的操作,通过编译之后,append 等同于下面代码,对于容量足够的切片没什么好说的,就是直接对 len
以及后面的元素赋值就可以了。L5 ~ L9 是在新的切片长度大于底层数组容量时,将底层数组扩大。
// slice = append(slice, 1, 2, 3)
a := &slice
ptr, len, cap := slice
newlen := len + 3
if uint(newlen) > uint(cap) {
newptr, len, newcap = growslice(slice, newlen)
vardef(a)
*a.cap = newcap
*a.ptr = newptr
}
newlen = len + 3
*a.len = newlen
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
代码 2:append
图 2:append
这里可以分三种情况,分别是:
- new_cap > 2 * old_cap: cap = new_cap
- new_cap < 2 old_cap && old_cap < 1024: cap = 2 old_cap
- 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 {
} else {newcap = cap
}if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
**代码 3:growslice newcap**
![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**
```go
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 == 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)
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)
}
代码 4:multiplication optimization
在计算真正需要分配的内存的大小,这里是根据 size 是否为 2 的倍数,进行了一些优化
- size = 1: 不需要处理
- size = sys.PtrSIze:这里看上去是直接相乘,但是因为 sys.PtrSize 是常数,所以编译器会优化为左移
- size 是 2 的倍数:左移优化
- size 不是 2 的倍数:直接相乘
另外还需要注意一下 roundupsize
函数,这个函数就是将分配的内存向上取整,这和 Go 语言的内存分配策略有关,可以减少内存碎片。