💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 2.4 主 Goroutine 的生与死 上一节中我们已经知道`schedinit`完成初始化工作后并不会立即执行`runtime.main`(即主 Goroutine 运行的地方)。相反,会在后续的`mstart`调用中被调度器调度执行。 这个过程中,只会将`runtime.main`的入口地址压栈,进而将其传递给`newproc`进行使用, 而后`newproc`完成 G 的创建保存到 G 的运行现场中,因此真正执行会等到`mstart`后才会被调度执行。 我们在调度器一章中详细讨论调度器的调度过程,现在我们先将目光聚焦在`runtime.main`已经开始执行时的情况。 ## 2.4.1 主 Goroutine 的一生 运行时包的 main 函数`runtime.main`承载了用户代码的 main 函数`main.main`, 并在同一个 Goroutine 上执行: ``` // 主 Goroutine func main() { ... // 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统) if sys.PtrSize == 8 { maxstacksize = 1000000000 } else { maxstacksize = 250000000 } ... // 启动系统后台监控(定期垃圾回收、抢占调度等等) systemstack(func() { newm(sysmon, nil) }) ... // 执行 runtime.init。运行时包中有多个 init 函数,编译器会将他们链接起来。 runtime_init() ... // 启动垃圾回收器后台操作 gcenable() ... // 执行用户 main 包中的 init 函数,因为链接器设定运行时不知道 main 包的地址,处理为非间接调用 fn := main_init fn() ... // 执行用户 main 包中的 main 函数,同理 fn = main_main fn() ... // 退出 exit(0) } ``` 整个执行过程有这样几个关键步骤: 1. `systemstack`会运行`newm(sysmon, nil)`启动后台监控 2. `runtime_init`负责执行运行时的多个初始化函数`runtime.init` 3. `gcenable`启用垃圾回收器 4. `main_init`开始执行用户态`main.init`函数,这意味着所有的`main.init`均在同一个主 Goroutine 中执行 5. `main_main`开始执行用户态`main.main`函数,这意味着`main.main`和`main.init`均在同一个 Goroutine 中执行。 ## 2.4.2`pkg.init`的执行顺序 运行时的`runtime_init`则由编译器将多个`runtime.init`进行链接,我们可以从 函数的声明中看到: ``` //go:linkname runtime_init runtime.init func runtime_init() ``` 运行时存在多个 init 函数,其中较为重要的几个函数包括: 1. 垃圾回收器所需的参数检查并创建强制启动 GC 的监控 Goroutine ``` const ( _WorkbufSize = 2048 workbufAlloc = 32 &lt;&lt; 10 ) func init() { if workbufAlloc%pageSize != 0 || workbufAlloc%_WorkbufSize != 0 { throw("bad workbufAlloc") } } func init() { go forcegchelper() } ``` 2. 确定`defer`的运行时类型: ``` var deferType *_type // _defer 结构的类型 func init() { var x interface{} x = (*_defer)(nil) deferType = (*(**ptrtype)(unsafe.Pointer(&amp;x))).elem } ``` 从这两个`init`函数可以看出,在用户代码正式启动之前,运行时还额外准备了强制 GC 的 监控并确定了 defer 的类型。 本节中我们不对这些方法做详细分析,等到他们各自的章节中再做详谈。那么我们仍然还会有这样 的疑问:包含多个`init`的执行顺序怎样由编译器控制的? 我们可以验证下面这两个不同的程序: ``` // main1.go package main import ( "fmt" _ "net/http" ) func main() { fmt.Printf("hello, %s", "world!") } ``` ``` // main2.go package main import ( _ "net/http" "fmt" ) func main() { fmt.Printf("hello, %s", "world!") } ``` 他们的唯一区别就是导入包的顺序不同,通过`go tool objdump -s "main.init"`可以获得`init`函数的实际汇编代码: ~~~asm TEXT main.init.0(SB) main1.go:8 0x11f0f40 65488b0c2530000000 MOVQ GS:0x30, CX ... main1.go:9 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB) ... TEXT main.init(SB) <autogenerated> ... <autogenerated>:1 0x11f10a8 e8e3b0ebff CALL fmt.init(SB) <autogenerated>:1 0x11f10ad e88e5affff CALL net/http.init(SB) <autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB) ... ~~~ ~~~asm TEXT main.init.0(SB) ... main2.go:10 0x11f0f76 e8a5b8e3ff CALL runtime.printstring(SB) ... TEXT main.init(SB) <autogenerated> <autogenerated>:1 0x11f1060 65488b0c2530000000 MOVQ GS:0x30, CX ... <autogenerated>:1 0x11f10a8 e8935affff CALL net/http.init(SB) <autogenerated>:1 0x11f10ad e81e40ecff CALL fmt.init(SB) <autogenerated>:1 0x11f10b2 e889feffff CALL main.init.0(SB) ... ~~~ 从实际的汇编代码可以看到,init 的顺序由实际包调用顺序给出,所有引入的外部包的 init 均会被 编译器安插在当前包的`main.init.0`之前执行,而外部包的顺序与引入包的顺序有关。 那么某个包内的多个 init 函数是否有顺序可言?我们简单看一看编译器关于 init 函数的实现: ``` // cmd/compile/internal/gc/init.go // 将 init 的名字 pkg.init 重命名为 pkg.init.0 var renameinitgen int func renameinit() *types.Sym { s := lookupN("init.", renameinitgen) renameinitgen++ return s } ``` `renameinit`这个函数中实现了对 init 函数的重命名,并通过`renameinitgen`在全局记录了 init 的索引后缀。`renameinit`会在处理函数声明时被调用: ``` // cmd/compile/internal/gc/noder.go func (p *noder) funcDecl(fun *syntax.FuncDecl) *Node { name := p.name(fun.Name) t := p.signature(fun.Recv, fun.Type) f := p.nod(fun, ODCLFUNC, nil, nil) // 函数没有 reciver if fun.Recv == nil { // 且名字叫做 init if name.Name == "init" { name = renameinit() // 对其进行重命名 ... } ... } ... } ``` 而`funcDecl`则会在 AST 的`noder`结构的方法`decls`中被调用: ``` func (p *noder) decls(decls []syntax.Decl) (l []*Node) { var cs constState for _, decl := range decls { p.lineno(decl) switch decl := decl.(type) { case *syntax.FuncDecl: l = append(l, p.funcDecl(decl)) ... } } return } ``` 一个包内的 init 函数的调用顺序取决于声明的顺序,即从上而下依次调用。 ## 2.4.3 小结 看到这里我们已经结束了整个 Go 程序的执行,但仍有海量的细节还没有被敲定,完全还没有深入 运行时的三大核心组件,运行时各类机制也都还没有接触。总结一下这节讨论中遗留下来的问题: 1. `mstart`会如何将主 Goroutine 调度执行? 2. `sysmon`系统监控做了什么事情,它的工作原理是什么? 3. `runtime.init`的`forcegchelper`是什么?`gcenable`又做了什么? 我们在随后的章节中一一介绍。 ## 进一步阅读的参考文献 1. [Command compile](https://golang.org/cmd/compile/) 2. [`main_init_done`can be implemented more efficiently](https://github.com/golang/go/issues/15943)