:= vauto[:]
- 根据切片中的元素数量对底层数组的大小进行推断并创建一个数组;
- 将这些字面量元素存储到初始化的数组中;
- 创建一个同样指向
[3]int
类型的数组指针;
- 将静态存储区的数组
vstat
赋值给 vauto
指针所在的地址;
- 通过
[:]
操作获取一个底层使用 vauto
的切片;
[:]
以及类似的操作 [:10]
其实都会在 SSA 代码生成 阶段被转换成 OpSliceMake
操作,这个操作会接受四个参数创建一个新的切片,切片元素类型、数组指针、切片大小和容量。
关键字
如果使用字面量的方式创建切片,大部分的工作就都会在编译期间完成,但是当我们使用 make
关键字创建切片时,在 类型检查 期间会检查 make
『函数』的参数,调用方必须传入一个切片的大小以及可选的容量:
func typecheck1(n *Node, top int) (res *Node) {
switch n.Op {
// ...
case OMAKE:
args := n.List.Slice()
i := 1
switch t.Etype {
case TSLICE:
if i >= len(args) {
yyerror("missing len argument to make(%v)", t)
return n
}
l = args[i]
i++
var r *Node
if i < len(args) {
r = args[i]
}
// ...
if Isconst(l, CTINT) && r != nil && Isconst(r, CTINT) && l.Val().U.(*Mpint).Cmp(r.Val().U.(*Mpint)) > 0 {
yyerror("len larger than cap in make(%v)", t)
return n
}
n.Left = l
n.Right = r
n.Op = OMAKESLICE
}
// ...
}
}
make
参数的检查都是在 typecheck1
函数中完成的,它不仅会检查 len
,而且会保证传入的容量 cap
一定大于或者等于 len
;随后的中间代码生成阶段会把这里的 OMAKESLICE
类型的操作都转换成如下所示的函数调用:
makeslice(type, len, cap)
当切片的容量和大小不能使用 int
来表示时,就会实现 makeslice64
处理容量和大小更大的切片,无论是 makeslice
还是 makeslice64
,这两个方法都是在结构逃逸到堆上初始化时才需要调用的,如果当前的切片不会发生逃逸并且切片非常小的时候,make([]int, 3, 4)
才会被转换成如下所示的代码:
var arr [4]int
n := arr[:3]
在这时,数组的初始化和 [:3]
操作就都会在编译阶段完成大部分的工作,前者会在静态存储区被创建,后者会被转换成 OpSliceMake
操作。
接下来,我们回到用于创建切片的 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 {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
上述代码的主要工作就是用切片中元素大小和切片容量相乘计算出切片占用的内存空间,如果内存空间的大小发生了溢出、申请的内存大于最大可分配的内存、传入的长度小于 0 或者长度大于容量,那么就会直接报错,当然大多数的错误都会在编译期间就检查出来,mallocgc
就是用于申请内存的函数,这个函数的实现还是比较复杂,如果遇到了比较小的对象会直接初始化在 Golang 调度器里面的 P 结构中,而大于 32KB 的一些对象会在堆上初始化。
初始化后会返回指向这片内存空间的指针,在之前版本的 Go 语言中,指针会和长度与容量一起被合成一个 slice
结构返回到 makeslice
的调用方,但是从 020a18c5 这个 commit 开始,构建结构体 SliceHeader
的工作就都由上层在类型检查期间完成了:
func typecheck1(n *Node, top int) (res *Node) {
switch n.Op {
// ...
case OSLICEHEADER:
switch
t := n.Type
n.Left = typecheck(n.Left, ctxExpr)
l := typecheck(n.List.First(), ctxExpr)
c := typecheck(n.List.Second(), ctxExpr)
l = defaultlit(l, types.Types[TINT])
c = defaultlit(c, types.Types[TINT])
n.List.SetFirst(l)
n.List.SetSecond(c)
// ...
}
}
OSLICEHEADER
操作会创建一个如下所示的结构体,其中包含数组指针、切片长度和容量,它是切片在运行时的表示:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
正是因为大多数对切片类型的操作并不需要直接操作原 slice
结构体,所以 SliceHeader
的引入能够减少切片初始化时的开销,这个改动能够减少 0.2% 的 Go 语言包大小并且能够减少 92 个 panicindex
的调用。
访问
对切片常见的操作就是获取它的长度或者容量,这两个不同的函数 len
和 cap
其实被 Go 语言的编译器看成是两种特殊的操作 OLEN
和 OCAP
,它们会在 SSA 生成阶段 被转换成 OpSliceLen
和 OpSliceCap
操作:
func (s *state) expr(n *Node) *ssa.Value {
switch n.Op {
case OLEN, OCAP:
switch {
case n.Left.Type.IsSlice():
op := ssa.OpSliceLen
if n.Op == OCAP {
op = ssa.OpSliceCap
}
return s.newValue1(op, types.Types[TINT], s.expr(n.Left))
// ...
}
// ...
}
}
除了获取切片的长度和容量之外,访问切片中元素使用的 OINDEX
操作也都在 SSA 中间代码生成期间就转换成对地址的获取操作:
func (s *state) expr(n *Node) *ssa.Value {
switch n.Op {
case OINDEX:
switch {
case n.Left.Type.IsSlice():
p := s.addr(n, false)
return s.load(n.Left.Type.Elem(), p)
// ...
}
// ...