1.5.1
Golang 源码剖析
前⾔
更新
⼀. 准备
⼆. 引导
三. 初始化
四. 内存分配
1. 概述
2. 初始化
3. 分配
4. 回收
5. 释放
6. 其他
五. 垃圾回收
1. 概述
2. 初始化
3. 启动
4. 标记
5. 清理
6. 监控
7. 其他
六. 并发调度
1. 概述
2. 初始化
3. 任务
4. 线程
5. 执⾏
6. 连续栈
7. 系统调⽤
8. 监控
9. 其他
七. 通道
1. 创建
2. 收发
!2
学习笔记 . 预览版
4
5
6
7
10
17
17
21
26
39
42
44
49
49
51
51
57
66
69
71
80
80
81
85
95
104
119
132
137
142
152
152
153
学习笔记 . 预览版
161
169
170
171
172
173
3. 选择
⼋. 延迟
九. 错误
⼗. 析构
⼗⼀. 函数
⼗⼆. 接⼜
!3
前言
我是个安全感匮乏的⼈,对新鲜事物总会保持⼀定的警惕。总想知道为什么会这样,为什
么会那样,渴望将⼀切都看得通透,⽽不仅仅是记住字⾥⾏间的规则条理。
知道 Golang 很早,但观望了相当长时间。究其原因,⽆⾮是⼀门新出的语⾔,⾃⾝和相
关资源都不成熟,不值得⽴即投⼊精⼒。只是后来屡屡出现的 “NextC” 让我终究起了⼀探
究竟的欲望,很想知道这个 goroutine 和 coroutine 究竟有什么区别。正好那段时间我在拆
解 greenlet 和 lua 的源码,算是相互借鉴。
从 R60 到现在,历经好⼏年,⼀直跟着源码去学习。其间有各种故事,倒不值得在此絮叨,
只能说欣喜苦恼掺杂,乐在其中罢了。虽说这是本写 Golang 的书,但我依然庆幸⾃⼰的
C、ASM 底⼦不错,让我多了种学习⼿段,能⽐多数⼈了解得更深⼊些。
尽管这已是本书第五版,但内容⼏乎全部重写,各种错漏在所难免,希望您能及时指正。
全书共分三册:上册《语⾔详解》,中册《标准库》,下册《源码剖析》。
联系⽅式:
微博:weibo.com/qyuhen
开源:github.com/qyuhen/book
邮件:qyuhen@hotmail.com
社区:qyuhen.bearychat.com
雨痕
⼆〇⼀五年,冬
学习笔记 . 预览版
更新
2012-01-11 开始学习 Go。
2012-01-15 第⼀版,基于 R60。
2012-03-29 升级到 1.0。
2012-06-15 升级到 1.0.2。
2013-03-26 升级到 1.1。
2013-12-12 第⼆版,基于 1.2。
2014-05-22 第三版,基于 1.3。
2014-12-20 第四版,基于 1.4。
2015-06-15 第五版,基于 1.5 RC。
2015-11-01 升级到 1.5.1。
!5
一. 准备
内容基于 Golang 1.5.1,测试环境 Linux AMD64,不包含 32 位内容。
学习笔记 . 预览版
我觉得是时候抛弃 32 位平台了。除了学习,⽇常开发和架构都不需要这个东西了。⽽且运⾏时
内部对 32 位的处理看着就别扭。
本书重点剖析 Golang 运⾏时的内部执⾏机制,以便能深⼊了解程序运⾏期状态,这有助
于深⼊理解语⾔规则,写出更好的代码,⽆论是规避 GC 潜在问题,还是为了节约内存,
亦或提升运⾏性能。
为便于阅读,相关代码被删减,如有疑问请对照原始⽂件。如果 Golang 版本不同,⽰例代码⾏
号可能会存在差异,请以您实际测试输出为准。
本书相关环境:
$ go version
go version go1.5.1 linux/amd64
$ lsb_release -d
Description: Ubuntu 14.04.3 LTS
$ gdb --version
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
本书⽰例 go 安装包存放在 /usr/local/go ⽬录,可能与您的有所不同,不影响测试。
!6
学习笔记 . 预览版
二. 引导
事实上,编译好的可执⾏⽂件真正执⾏⼊⼜并⾮我们所写的 main.main 函数,因为编译器
总是会插⼊⼀段引导代码,完成诸如命令⾏参数、运⾏时初始化等⼯作,然后才会进⼊⽤
户逻辑。
要从 src/runtime ⽬录下的⼀堆⽂件中找到真正的⼊⼜,其实很容易。随便准备⼀个编译
好的⽬标⽂件,⽐如 “Hello, World!”。
test.go
package main
func main() {
println("hello, world!")
}
编译,然后⽤ GDB 查看。
建议:尽可能使⽤命令⾏编译,⽽不是某些 IDE 的菜单命令,这有助于我们熟悉各种编译开关
参数的具体功能。其次,调试程序时,建议使⽤ -gcflags "-N -l" 参数关闭编译器代码优化和函数
内联,避免断点和单步执⾏⽆法准确对应源码⾏,避免⼩函数和局部变量被优化掉。
$ go build -gcflags "-N -l" -o test test.go
如果在平台使⽤交叉编译(Cross Compile),需要设置 GOOS 环境变量。
$ gdb test
(gdb) info files
Local exec file:
Entry point: 0x44dd00
(gdb) b *0x44dd00
Breakpoint 1 at 0x44dd00: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
很简单,找到真正的⼊⼜地址,然后利⽤断点命令就可以轻松找到⽬标源⽂件信息。
!7
在 src/runtime ⽬录下有很多不同平台的⼊⼜⽂件,都由汇编实现。
学习笔记 . 预览版
$ ls rt0_*
rt0_android_arm.s rt0_dragonfly_amd64.s rt0_linux_amd64.s ...
rt0_darwin_386.s rt0_freebsd_386.s rt0_linux_arm.s ...
rt0_darwin_amd64.s rt0_freebsd_amd64.s rt0_linux_arm64.s ...
⽤你习惯的代码编辑器打开源⽂件,跳转到指定⾏,查看具体内容。
rt0_linux_amd64.s
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
8(SP), SI // argv
0(SP), DI // argc
$main(SB), AX
AX
LEAQ
MOVQ
MOVQ
JMP
TEXT main(SB),NOSPLIT,$-8
$runtime·rt0_go(SB), AX
AX
MOVQ
JMP
⽤ GDB 设置断点命令看看这个 rt0_go 在哪。
注意:源码⽂件中的 “·” 符号编译后变成正常的 “.”。
(gdb) b runtime.rt0_go
Breakpoint 2 at 0x44a780: file /usr/local/go/src/runtime/asm_amd64.s, line 12.
这段汇编代码就是要找的真正⽬标,正是它完成了初始化和运⾏时启动。
asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT,$0
...
//
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// main goroutine runtime.main
MOVQ $runtime·mainPC(SB), AX
!8