切片是引用,所以不需要额外的空间
切片组成元素:
初始化方式
使用 make
关键字创建切片时,很多工作都需要运行时的参与;调用方必须在 make
函数中传入一个切片的大小以及可选的容量,cmd/compile/internal/gc.typecheck1
会对参数进行校验:
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
}
...
}
}
上述函数不仅会检查 len
是否传入,还会保证传入的容量 cap
一定大于或者等于 len
,除了校验参数之外,当前函数会将 OMAKE
节点转换成 OMAKESLICE
,随后的中间代码生成阶段在 cmd/compile/internal/gc.walkexpr
函数中的 OMAKESLICE
分支依据两个重要条件对这里的 OMAKESLICE
进行转换:
当切片发生逃逸或者非常大时,我们需要 runtime.makeslice
函数在堆上初始化,如果当前的切片不会发生逃逸并且切片非常小的时候,make([]int, 3, 4)
会被直接转换成如下所示的代码:
var arr [4]int
n := arr[:3]
上述代码会初始化数组并且直接通过下标 [:3]
来得到数组的切片,这两部分操作都会在编译阶段完成,编译器会在栈上或者静态存储区创建数组,[:3]
会被转换成上一节提到的 OpSliceMake
操作。
分析了主要由编译器处理的分支之后,我们回到用于创建切片的运行时函数 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 {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
它的主要工作就是计算当前切片占用的内存空间并在堆上申请一片连续的内存,它使用如下的方式计算占用的内存:
内存空间 = 切片中元素大小 x 切片容量
虽然大多的错误都可以在编译期间被检查出来,但是在创建切片的过程中如果发生了以下错误就会直接导致程序触发运行时错误并崩溃:
runtime.makeslice
在最后调用的 runtime.mallocgc
是用于申请内存的函数,这个函数的实现还是比较复杂,如果遇到了比较小的对象会直接初始化在 Go 语言调度器里面的 P 结构中,而大于 32KB 的对象会在堆上初始化
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。