原创分享 一文告诉你神奇的 Go 内建函数源码在哪里

bigwhite-github · 2020年12月21日 · 最后由 bigwhite-github 回复于 2021年01月05日 · 720 次阅读
本帖已被设为精华帖!

img{512x368}

Go 内建函数源码,我好像在哪里见过你。 - 佚名

1. 何为 Go 内建函数

众所周知,Go 是最简单的主流编程语言之一,截至Go 1.15 版本,Go 语言的关键字的规模依旧保持在 25 个:

img{512x368}

很多刚入门的 gopher 可能会问:像 bool、byte、error、true、iota甚至 int 都难道都不是关键字?没错!和其他语言不同,这些标识符并不是关键字,在 Go 中它们被称为预定义标识符。这些标识符拥有universe block 作用域(关于 go 代码块作用域的详细解析,可参考我的技术专栏:“改善 Go 语⾔编程质量的 50 个有效实践”),可以在任何源码位置使用。

img{512x368}

从上图我们看到:所谓的Go 内建函数也包含在这个预定义标识符集合中,只是这些标识符被用作函数名称标识符罢了。

2. 预定义标识符可被 override

Go 语言的关键字是保留的,我们无法将其用于规范之外的其他场合,比如作为变量的标识符。但是预定义标识符不是关键字,我们可以 override 它们。下面就是一个对默认表示整型类型的预定义标识符int进行 override 的例子:

package main

import (
        "fmt"
        "unsafe"
)

type int int8

func main() {
        var a int = 5
        fmt.Printf("%T\n", a) // main.int,而不是int
        fmt.Println(unsafe.Sizeof(a)) // 1,而不是8
}

在上述这个源文件中,预定义标识符 int 被 override 为一个自定义类型 int,该类型的 underlying type 为 int8,于是当我们输出该类型变量 (代码中的变量 a) 的类型和长度时,我们得到的是 main.int 和 1,而不是 int 和 8。

3. 预定义标识符的声明源码在哪里

Go 是开源的编程语言,这些预定义标识符想必也都有自己的 “归宿” 吧,的确是这样的。Go 的每个发行版都带有一份源码,而预定义标识符就在这份源码中。

Go 1.14为例,我们可以在下面路径中找到预定义标识符的源码:

$GOROOT/src/builtin/builtin.go

以 string、int、uint 这几个代表原生类型的预定义标识符为例,它们的声明代码如下:

// $GOROOT/src/builtin/builtin.go

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
type int int

// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint

同时,我们利用go doc builtin.int也可以查看预定义标识符 int 的文档:

$go doc builtin.int
package builtin // import "builtin"

type int int
    int is a signed integer type that is at least 32 bits in size. It is a
    distinct type, however, and not an alias for, say, int32.

func cap(v Type) int
func copy(dst, src []Type) int
func len(v Type) int

4. 内建函数的源码在哪里?

作为预声明标识符子集的内建函数们在 builtin.go 中也都有自己的位置,比如:以 append 这个内建函数为例,我们可以在 Go 安装包的 builtin.go 中找到它的原型 (Go 1.14):

// 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 函数的实现。那么 append 内建函数实现的源码究竟在哪里呢?本质上讲 append 函数,包括其他内建函数其实并没有自己的实现源码。

内建函数仅仅是一个标识符,在 Go 源码编译期间,Go 编译器遇到内建函数标识符时会将其替换为若干 runtime 的调用,我们还以 append 函数为例,我们输出下面代码的汇编代码 (Go 1.14):

// append.go
package main

import "fmt"

func main() {
 var s = []int{5, 6}
 s = append(s, 7, 8)
 fmt.Println(s)
}

$go tool compile -S append.go > append.s

汇编节选如下 (append.s):

"".main STEXT size=277 args=0x0 locals=0x58
 0x0000 00000 (xxx.go:5) TEXT "".main(SB), ABIInternal, $88-0
 0x0000 00000 (xxx.go:5) MOVQ (TLS), CX
 0x0009 00009 (xxx.go:5) CMPQ SP, 16(CX)
 0x000d 00013 (xxx.go:5) PCDATA $0, $-2
 0x000d 00013 (xxx.go:5) JLS  267
 0x0013 00019 (xxx.go:5) PCDATA $0, $-1
 0x0013 00019 (xxx.go:5) SUBQ $88, SP
 0x0017 00023 (xxx.go:5) MOVQ BP, 80(SP)
 0x001c 00028 (xxx.go:5) LEAQ 80(SP), BP
 0x0021 00033 (xxx.go:5) PCDATA $0, $-2
 0x0021 00033 (xxx.go:5) PCDATA $1, $-2
 0x0021 00033 (xxx.go:5) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $1, gclocals·568470801006e5c0dc3947ea998fe279(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $2, gclocals·bfec7e55b3f043d1941c093912808913(SB)
 0x0021 00033 (xxx.go:5) FUNCDATA $3, "".main.stkobj(SB)
 0x0021 00033 (xxx.go:6) PCDATA $0, $1
 0x0021 00033 (xxx.go:6) PCDATA $1, $0
 0x0021 00033 (xxx.go:6) LEAQ type.[2]int(SB), AX
 0x0028 00040 (xxx.go:6) PCDATA $0, $0
 0x0028 00040 (xxx.go:6) MOVQ AX, (SP)
 0x002c 00044 (xxx.go:6) CALL runtime.newobject(SB)
 0x0031 00049 (xxx.go:6) PCDATA $0, $1
 0x0031 00049 (xxx.go:6) MOVQ 8(SP), AX
 0x0036 00054 (xxx.go:6) MOVQ $5, (AX)
 0x003d 00061 (xxx.go:6) MOVQ $6, 8(AX)
 0x0045 00069 (xxx.go:7) PCDATA $0, $2
 0x0045 00069 (xxx.go:7) LEAQ type.int(SB), CX
 0x004c 00076 (xxx.go:7) PCDATA $0, $1
 0x004c 00076 (xxx.go:7) MOVQ CX, (SP)
 0x0050 00080 (xxx.go:7) PCDATA $0, $0
 0x0050 00080 (xxx.go:7) MOVQ AX, 8(SP)
 0x0055 00085 (xxx.go:7) MOVQ $2, 16(SP)
 0x005e 00094 (xxx.go:7) MOVQ $2, 24(SP)
 0x0067 00103 (xxx.go:7) MOVQ $4, 32(SP)
 0x0070 00112 (xxx.go:7) CALL runtime.growslice(SB)
 0x0075 00117 (xxx.go:7) PCDATA $0, $1
 0x0075 00117 (xxx.go:7) MOVQ 40(SP), AX
 0x007a 00122 (xxx.go:7) MOVQ 48(SP), CX
 0x007f 00127 (xxx.go:7) MOVQ 56(SP), DX
 0x0084 00132 (xxx.go:7) MOVQ $7, 16(AX)
 0x008c 00140 (xxx.go:7) MOVQ $8, 24(AX)
 0x0094 00148 (xxx.go:8) PCDATA $0, $0
 0x0094 00148 (xxx.go:8) MOVQ AX, (SP)
 0x0098 00152 (xxx.go:7) LEAQ 2(CX), AX
 0x009c 00156 (xxx.go:8) MOVQ AX, 8(SP)
 0x00a1 00161 (xxx.go:8) MOVQ DX, 16(SP)
 0x00a6 00166 (xxx.go:8) CALL runtime.convTslice(SB)
 ... ...

我们可以看到:append 并没有以独立的身份出现在 CALL 汇编指令的后面,而是被换成:runtime.growslice、runtime.convTslice 以及相关汇编指令了。


“Gopher 部落” 知识星球开球了!高品质首发 Go 技术文章,“三天” 首发阅读权,每年两期 Go 语言发展现状分析,每天提前 1 小时阅读到新鲜的 Gopher 日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于 Go 语言生态的所有需求!星球首开,福利自然是少不了的!2020 年年底之前,8.8 折 (很吉利吧^_^) 加入星球,下方图片扫起来吧!

我的 Go 技术专栏:“改善 Go 语⾔编程质量的 50 个有效实践” 上线了,欢迎大家订阅学习!

img{512x368}

我的网课 “Kubernetes 实战:高可用集群搭建、配置、运维与应用” 在慕课网热卖中,欢迎小伙伴们订阅学习!

Gopher Daily(Gopher 每日新闻) 归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 12月21日 02:18

就这?两位大佬是不是对精华的标准降太低了?

这个其实就是入门文章,算不上精华。astaxie 太给面子了。

mahuaibo GoCN 每日新闻 (2021-01-01) 中提及了此贴 01月01日 11:41

Go1.5 就已经实现了自举,既然可以自己编译自己,那么就意味着这些 Go 的内建函数肯定有 Go 的逻辑实现。就算 Go 没有实现自举是 C 的实现,那么这些内建函数一定在 C 里面有所实现,不可能毫无征兆的就替换为了 runtime 调用。一般 go 的内建函数实现源码在 src/cmd/compile/internal/gc 下, 其中 append 的具体实现是在 https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/ssa.go#L2841

不辨不明啊,感谢提醒。不过 ssa.go#L2841 已经算是编译器视角下的将 append 换成 ssa 的实现了。并非从 go 开发人员的视角出发的很直观的 “append 实现”。相信 99% 的不了解 compiler 底层机制的 go 程序员看不懂 ssa.go 中的代码。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册