写这篇文章的动力是一个关于gopher Slack的问题。一位开发人员想知道在哪里可以找到更多关于len的信息。
我想知道len func是如何调用的。
人们很快就回答了一个正确的答案.
len是一个编译器魔法,并不是一个实际的函数调用。
……所有len工作的类型都有相同的头格式,编译器只是把对象当作一个头,并返回代表元素长度的整数。
虽然这些答案在技术上是正确的,但我认为用一个简明的解释来展开构成这个 “魔法 “的层次是很好的 这也是一个很好的小练习,可以让我们更深入地了解Go编译器的内部运作。
顺便说一下,这篇文章中的所有链接都指向即将发布的Go 1.17分支。
一个小插曲
一些背景信息可能有助于理解本帖的其余部分。
Go编译器由四个主要阶段组成。你可以从这里开始阅读它们。前两个阶段一般被称为编译器的 “前端”,而后两个阶段也被称为编译器的 “后端”。
- 解析;对源文件进行标记、解析,并为每个源文件构建一个语法树。
- AST转换和类型检查;语法树被转换为编译器的AST表示,AST树被进行类型检查。
- 通用SSA;AST树被转换为静态单一赋值(SSA)形式,这是一种较低级别的中间表示,可以实现优化。
- 生成机器码;SSA经过另一个针对机器的优化过程,之后被传递给汇编器,翻译成机器码并写入最终的二进制文件。
让我们重新开始吧!
进入点
Go 编译器的入口是(不出所料)compile/internal/gc 包中的 main() 函数。
正如文档串所提示的,这个函数负责解析 Go 源文件、对解析后的 Go 包进行类型检查、将所有内容编译为机器码并编写编译后的包定义。
早期发生的事情之一是tynecheck.InitUniverse(),它定义了基本类型、内置函数和操作数。
在那里,我们看到所有内置函数是如何与 “操作 “相匹配的,我们可以使用ir.OLEN来追踪调用len的步骤。
1 | var builtinFuncs = [...]struct { |
在InitUniverse的后面,我们可以看到okfor数组的初始化,它定义了各种操作数的有效类型;例如,哪些类型应该被允许用于+操作。
1 | if types.IsInt[et] || et == types.TIDEAL { |
以同样的方式,我们可以看到所有的类型将成为len()的有效输入。
1 | okforlen[types.TARRAY] = true |
编译器的 “前台”
进入到编译过程的下一个主要步骤,我们到达了从noder.LoadPackage(flag.Args())开始对输入进行解析和类型检查的地方。
再深入几层,我们可以看到每个文件都被单独解析,然后在五个不同的阶段进行类型检查。
1 | Phase 1: const, type, and names and types of funcs. |
一旦在最后的类型检查阶段遇到len语句,它就会被转换为UnaryExpr,因为它实际上最终不会成为一个函数调用。
编译器隐含地获取参数的地址,并使用okforlen数组来验证参数的合法性或发出相关的错误信息。
1 | // typecheck1 should ONLY be called from typecheck. |
回到编译器的主流程,在所有东西都经过类型检查后,所有的函数都被排队等待编译。
在compileFunctions()中,队列中的每个元素都会通过ssagen.Compile。
1 | compile = func(fns []*ir.Func) { |
在buildssa和genssa之后,再深入几层,我们终于可以把AST树中的len表达式转换为SSA。
在这一点上,我们很容易看到每一个可用的类型是如何处理的!
1 | // expr converts the expression n to ssa, adds it to s and returns the ssa result. |
数组
对于数组,我们只是根据输入数组的NumElem()方法返回一个常数,该方法只是访问输入数组的Bound域。
1 | // Array contains Type fields specific to array types. |
Slices, Strings
对于切片和字符串,我们必须看一看ssa.OpSliceLen和ssa.OpStringLen是如何处理的。
当这些调用在Late Expansion阶段和rewriteSelect方法中被降低时,切片和字符串被递归行走,使用指针算术找出它们的大小,如offset+x.ptrSize
1 | func (x *expandState) rewriteSelect(leaf *Value, selector *Value, offset int64, regOffset Abi1RO) []*LocalSlot { |
Maps, Channels
最后,对于Maps, Channels,我们使用 referenceTypeBuiltin 辅助工具。它的内部工作原理有点神奇,但它最终做的是获取map/chan参数的地址,并以零偏移量引用其结构布局,就像unsafe.Pointer(uintptr(unsafe.Pointer(s))一样,最终返回第一个结构域的值。
1 | // referenceTypeBuiltin generates code for the len/cap builtins for maps and channels. |
hmap和hchan结构的定义表明,它们的第一个字段确实包含我们所需要的len,即分别是活地图单元和通道队列数据。
1 | type hmap struct { |
临别赠言
就这样吧! 这篇文章没有我想象的那么长,我只希望它对你来说也是有趣的。
我对Go编译器的内部工作没有什么经验,所以有些东西可能是不对的。另外,很多事情在不久的将来都会发生变化,特别是泛型和新的类型系统会在接下来的几个Go版本中出现,但至少我希望我提供了一个方法,让你可以用来开始自己的挖掘。
在任何情况下,如果您有任何意见、建议、关于新文章的想法,或者只是简单地谈论Go,请不要犹豫。
直到下一次,再见!