本文已参与「新人创作礼」活动,一起开启掘金创作之路。
底层分析
type slice struct {
array unsafe.Pointer // point to actual data
len int // number of elements
cap int // allocated number of elements
}
slice是对数组中某个部分的引用
针对于数组来说, [3]int 与 [4]int两个是不同的类型,数组的长度是类型的一部分,与slice不同
在内存中模型是一个包含三个域的结构体,包含了指向slice源数组第一个元素的指针ptr,slice的长度len,slice的容量cap
长度是下标操作的上界,如果操作cap内,len外的下标也会导致越界。
容量是分割操作的上界,分割操作下标操作cap的操作是不被允许的
底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
\
对于切分操作,只是新创建了一个包含三个域的结构体,而不会去真正的拷贝一份源数组
分割操作不需要分配内存
\
当slice作为函数参数传递的时候会发生什么
由于go中只有值传递,因此参数会是一个拷贝的slice结构体
但是ptr指针还是指向的同一个底层数组,通过ptr操作这个底层数组会对实参slice的底层数组造成影响
但是对形参的slice的作用是不会改变外层实参的slice的
比如s = append(s, 100)实际上只是修改副本的len/cap而已
如果真的想对实参的slice造成影响,可以将返回的slice赋值给原来的slice,或者说传入一个slice的pointer
slice的扩容
底层是一个动态数组实现
struct Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};
对slice进行append等操作的时候,可能会导致slice自动扩容
// 函数原型如下
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type
append的参数长度可变,能直接追加多个值,也可以通过在参数后方加...来传入slice
go编译器不允许调用append后不使用返回值
计算预估容量(元素个数)规则如下
- 如果新的大小是当前大小的2倍以上,则增长为新大小
- 否则循环如下操作,直到增长的大小超过或等于新的大小
-
- 当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍
- 原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
真正分配内存的时候还是要按照内存对齐来分配
语言自身的内存管理模块会提前向os申请一批内存,分成常用规格管理起来,如8,16,32,48,64,80,96,112。
实际申请时,会将预估申请内存匹配到合适的内存规格分配
申请到适合的内存后,将旧 slice 中的数据复制过去,并且将 append 的元素添加到新的底层数组中
相关源码如下(go v1.18)
func growslice(et *_type, old slice, cap int) slice {
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
// 两倍大,直接将新的当成容量
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
// 1.25倍
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
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))
// 这两句对newcap作了一个内存对齐,和内存分配策略有关
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
...
return slice{p, old.len, newcap}
}