一文了解Golang的切片slice

301 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

底层分析

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}
}