数组
初始化
Go语言数组的初始化是在编译期就已经执行好了。这个是初始化的代码:
// NewArray returns a new fixed-length array Type.
func NewArray(elem *Type, bound int64) *Type {
if bound < 0 {
base.Fatalf("NewArray: invalid bound %v", bound)
}
// 根据TARRAY这个标识做响应的初始化
t := newType(TARRAY)
// 初始化元素类型Elem和数组大小Bound
t.extra = &Array{Elem: elem, Bound: bound}
// 是否应该初始化在堆里面
t.SetNotInHeap(elem.NotInHeap())
// 是否有参数
if elem.HasTParam() {
t.SetHasTParam(true)
}
// 笔者目前还不清楚这个Shape的含义
if elem.HasShape() {
t.SetHasShape(true)
}
return t
}
首先解释一下这个函数的返回值,Type是一个什么东西。
这个是Go中对于Type结构体的定义:
// A Type represents a Go type.
//
// There may be multiple unnamed types with identical structure. However, there must
// be a unique Type object for each unique named (defined) type. After noding, a
// package-level type can be looked up by building its unique symbol sym (sym =
// package.Lookup(name)) and checking sym.Def. If sym.Def is non-nil, the type
// already exists at package scope and is available at sym.Def.(*ir.Name).Type().
// Local types (which may have the same name as a package-level type) are
// distinguished by the value of vargen.
type Type struct {
// extra contains extra etype-specific fields.
// As an optimization, those etype-specific structs which contain exactly
// one pointer-shaped field are stored as values rather than pointers when possible.
//
// TMAP: *Map
// TFORW: *Forward
// TFUNC: *Func
// TSTRUCT: *Struct
// TINTER: *Interface
// TFUNCARGS: FuncArgs
// TCHANARGS: ChanArgs
// TCHAN: *Chan
// TPTR: Ptr
// TARRAY: *Array
// TSLICE: Slice
// TSSA: string
// TTYPEPARAM: *Typeparam
// TUNION: *Union
extra interface{}
// width is the width of this Type in bytes.
width int64 // valid if Align > 0
// list of base methods (excluding embedding)
methods Fields
// list of all methods (including embedding)
allMethods Fields
// canonical OTYPE node for a named type (should be an ir.Name node with same sym)
obj Object
// the underlying type (type literal or predeclared type) for a defined type
underlying *Type
// Cache of composite types, with this type being the element type.
cache struct {
ptr *Type // *T, or nil
slice *Type // []T, or nil
}
vargen int32 // unique name for OTYPE/ONAME
kind Kind // kind of type
align uint8 // the required alignment of this type, in bytes (0 means Width and Align have not yet been computed)
flags bitset8
// For defined (named) generic types, a pointer to the list of type params
// (in order) of this type that need to be instantiated. For instantiated
// generic types, this is the targs used to instantiate them. These targs
// may be typeparams (for re-instantiated types such as Value[T2]) or
// concrete types (for fully instantiated types such as Value[int]).
// rparams is only set for named types that are generic or are fully
// instantiated from a generic type, and is otherwise set to nil.
// TODO(danscales): choose a better name.
rparams *[]*Type
// For an instantiated generic type, the base generic type.
// This backpointer is useful, because the base type is the type that has
// the method bodies.
origType *Type
}
它表示的是一个类型。其实不光是数组,切片,哈希表等在初始化的时候同样会返回这个Type结构体的指针。
func NewSlice(elem *Type) *Type
func NewChan(elem *Type, dir ChanDir) *Type
func NewTuple(t1, t2 *Type) *Type
func NewMap(k, v *Type) *Type
。。。。
来看看NewType的源代码:
// New returns a new Type of the specified kind.
func newType(et Kind) *Type {
t := &Type{
kind: et,
width: BADWIDTH,
}
t.underlying = t
// TODO(josharian): lazily initialize some of these?
switch t.kind {
case TMAP:
t.extra = new(Map)
case TFORW:
t.extra = new(Forward)
case TFUNC:
t.extra = new(Func)
case TSTRUCT:
t.extra = new(Struct)
case TINTER:
t.extra = new(Interface)
case TPTR:
t.extra = Ptr{}
case TCHANARGS:
t.extra = ChanArgs{}
case TFUNCARGS:
t.extra = FuncArgs{}
case TCHAN:
t.extra = new(Chan)
case TTUPLE:
t.extra = new(Tuple)
case TRESULTS:
t.extra = new(Results)
case TTYPEPARAM:
t.extra = new(Typeparam)
case TUNION:
t.extra = new(Union)
}
return t
}
以上是Go初始化最本质的代码。
Go数组的初始化的时候有两种创建方式:
arr1 := [3]int {1, 2, 3}
arr2 := [...]int {1, 2, 3}
- 第一种创建方式是显示的指定的数组的大小
- 第二种会在编译期间通过源代码推导数组的大小
这两种在运行时就已经完全一样了,只是Go给我们的语法糖而已。
因此我们现在讲一下推导的过程
上限推导
会调用函数使用遍历的方式来计算数组中元素的数量。
语句转换
对于由字面量组成的数组,根据数组元素数量的不同,编译器会在负责初始化字面量的函数中做两个优化:
-
当元素小于等于4个的时候,会直接把数组中的元素放在栈上,并且在编译之前把其转换成更原始的语句。
例如:
arr := [3]int {1, 2, 3} // 会被转换成 var arr [3]int arr[0] = 1 arr[1] = 2 arr[2] = 3
-
多于4个的时候,会把数组中的元素放置到静态区并在运行时取出。函数在静态存储区初始化数组中的元素,并将临时变量赋值给数组。
例如:
arr := [5]int {1, 2, 3, 4, 5} var arr [5]int statictmp_0[0] = 1 statictmp_0[1] = 2 statictmp_0[2] = 3 statictmp_0[3] = 4 statictmp_0[4] = 5 arr = statictmp_0
总结:在不考虑逃逸分析的情况下,如果数组元素小于等于4个,那么所有的变量会直接在栈上初始化;如果多于4个,会在静态存储区初始化然后复制到栈上,这些转换后的代码才会继续进入中间代码生成和机器码生成两个阶段,最后生成可执行二进制文件。
访问和赋值
越界问题
-
如果直接使用整数或者常量访问,在编译期间会通过静态类型检查大致的检查越界问题。
-
如果使用变量访问数组或者字符串,编译期间无法发现错误,需要在runtime(运行时)发现错误并进行阻碍。具体过程为:
-
当数组的访问操作OINDEX成功通过编译器检查之后,会被转换成几个SSA指令。例如下面的这个代码生成的SSA:
func outOfRange() int { arr := [3]int{1, 2, 3} i := 4 elem := arr[i] return elem }
-
我们这里展示
elem := arr[i]
生成的SSA代码:b1: ... v22 (6) = LocalAddr <*[3]int> {arr} v2 v20 v23 (6) = IsInBounds <bool> v21 v11 If v23 → b2 b3 (likely) (6) b2: ← b1- v26 (6) = PtrIndex <*int> v22 v21 v27 (6) = Copy <mem> v20 v28 (6) = Load <int> v26 v27 (elem[int]) ... Ret v30 (+7) b3: ← b1- v24 (6) = Copy <mem> v20 v25 (6) = PanicBounds <mem> [0] v21 v11 v24 Exit v25 (6)
可以看到Go为访问操作生成了两个指令:
IsInBounds
:判断数组上限的指令PanicBounds
:条件不满足时触发程序崩溃的指令 -
其中
PanicBounds
指令会转换成runtime.panicIndex
函数。TEXT runtime·panicIndex(SB),NOSPLIT,$0-8 MOVL AX, x+0(FP) MOVL CX, y+4(FP) JMP runtime·goPanicIndex(SB) func goPanicIndex(x int, y int) { panicCheck1(getcallerpc(), "index out of range") panic(boundsError{x: int64(x), signed: true, y: y, code: boundsIndex}) }
如果越界了直接寄。如果没有越界的话编译器会获取数组的内存地址和访问的下标,利用
PtrIndex
计算出目标元素的地址,然后只用Load
操作把指针中的元素加载到内存中。如下:
b1: ... v21 (5) = LocalAddr <*[3]int> {arr} v2 v20 v22 (5) = PtrIndex <*int> v21 v14 v23 (5) = Load <int> v22 v20 (elem[int]) ...
-
-
对于赋值和更新操作
a[i] = 2
也会生成SSA,如下:b1: ... v21 (5) = LocalAddr <*[3]int> {arr} v2 v19 v22 (5) = PtrIndex <*int> v21 v13 v23 (5) = Store <mem> {int} v22 v20 v19 ...
赋值过程中先确定目标数组的地址,再通过
PtrIndex
获取目标元素的地址,最后使用Store
指令将数据存入地址中。可以看到全程在编译阶段完成,没有runtime参与。
切片
数据结构
type sliceHeader struct {
Data unsafe.Pointer // 指向数组的指针
Len int // 当前切片的长度
Cap int // 当前切片的容量
}
这个结构体是一个抽象层,源数组不管怎么发生变化,上层都认为切片没有变化,它不需要关心数组的变化。
初始化
- 通过下标初始化
- 使用字面量
- make
使用下标
最原始的方式,最接近汇编。编译器会把arr[0:3]
这样的操作转换成OpSliceMake
操作。
字面量
[]int{1, 2, 3}
会在编译期间被转换成这样:
var vstat [3]int
vstat[0] = 1
vstat[1] = 2
vstat[2] = 3
var vauto *[3]int = new([3]int)
*vauto = vstat
slice := vauto[:]
- 根据切片中的元素数量推断底层数组的大小并创建一个数组
- 将这些字面量元素存储到初始化的数组中
- 创建一个
[3]int
类型的数组指针 - 将静态存储区的数组
vstat
赋值给vauto
指针所在的地址 - 通过
[:]
获取一个切片
关键字
前面的大部分操作的编译期间完成,而关键字大部分在runtime完成。
具体步骤如下:
-
使用
lxy := make([]int, 10)
-
先判断
len
是否传入,同时会保证cap
一定大于或者等于len
。 -
然后判断两点:
- 切片大小和容量是否足够小
- 切片是否发生了逃逸,最终在堆总初始化
如果切片发生逃逸或者非常大,会使用
runtime.makeslice
在堆上初始化。如果不发生逃逸或者非常小,会直接转换成这个代码:
var arr [4]int n := arr[:3]
这个代码会初始化数组并通过下标得到数组对应的切片,在栈或者静态区域创建。然后就转化成了用下标创建。
-
分支处理完成,进入创建切片的运行时函数
runtime.makeslice
。func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { // NOTE: Produce a 'len out of range' error instead of a // 'cap out of range' error when someone does make([]T, bignumber). // 'cap out of range' is true too, but since the cap is only being // supplied implicitly, saying len is clearer. // See golang.org/issue/4085. mem, overflow := math.MulUintptr(et.size, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() } // 如果小于32KB,会在Go语言调度器的P结构体里面初始化,否则会在堆里面初始化 return mallocgc(mem, et, true) }
该函数的主要功能是计算切片占用的内存空间,并在堆中申请一块连续的内存。
虽然编译期间可以检查出很多错误,但是在创建切片的时候如果发现了这些错误,就出现运行时错误或者崩溃。
- 内存空间大小发生溢出
- 申请的内存大于最大可分配内存
- 传入的长度小于0或者大于容量
访问元素
len(slice)
和cap(slice)
在一些情况下会直接替换成切片的长度或者容量,在编译期完成,不需要在运行时获取。
追加和扩容
- 如果返回值会覆盖原变量
- 如果返回值不需要赋值回原变量
如果返回值不需要赋值回原变量
// append(slice, 1, 2, 3)
ptr, len, cap := slice
newlen := len + 3
if newlen > cap {
ptr, len, cap = growslice(slice, newlen)
newlen = len + 3
}
*(ptr+len) = 1
*(ptr+len+1) = 2
*(ptr+len+2) = 3
return makeslice(ptr, newlen, cap)
如果返回值会覆盖原变量
// 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
可以看到如果返回值回覆盖原变量的话,这里是做了优化的,它是在原切片上动手脚,没有进行复制,提高性能。
当容量不足的时候会调用runtime.growslice
函数为切片扩容。
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
if asanenabled {
asanread(old.array, uintptr(old.len*int(et.size)))
}
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
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.
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
- 如果期望容量大于当前容量的2倍,就会使用期望容量
- 如果当前切片的长度小于1024,就会将容量翻倍
- 如果大于1024,就会每次增加25%的容量,直到新容量大于期望容量
上面的过程只会大概确定切片的容量,还需要根据切片中的元素大小进行内存对齐。
复制切片
要注意大切片复制导致的内存开销问题
总结:不管复制切片的过程是运行时还是非运行时进行的。本质上都是使用runtime.memmove
将整块内存复制到目标内存区域中。