阅读 676

[Golang]初始化流程分析

前言

借助gdb来查看go的底层汇编,借此梳理和分析go程序的初始化流程,看看在初始化阶段go都做了哪些工作,对于理解go的工作机制很有帮助。目前是基于go 1.16.4进行的。

gdb调试

搭建gdb调试go程序 中已经探究并介绍了gdb的环境搭建、基本使用以及如何利用gdb来调试断点查看函数调用次序。

流程调试

在这里插入图片描述
如上图,是go程序初始化流程的整理,由于整个流程调用方法非常多,这里挑选较为核心部分进行梳理和分析,这里仅梳理初始化过程的调用流程和次序,用来熟悉初始化的工作机制,不涉及原理性分析,结合这张图的执行次序来说明,如下:

阶段次序语言环境执行文件执行函数核心逻辑
11.1汇编(.s)$GOROOT/src/runtime/rt0_darwin_amd64.s_rt0_amd64_darwin汇编引导
11.2汇编(.s)$GOROOT/src/runtime/asm_amd64.s_rt0_amd64汇编引导
11.3汇编(.s)$GOROOT/src/runtime/asm_amd64.srt0_go汇编引导
22.1golang(.go)$GOROOT/src/runtime/runtime1.goargs整理命令行参数
22.2golang(.go)$GOROOT/src/runtime/os_darwin.goosinit确定CPU数量
22.3golang(.go)$GOROOT/src/runtime/proc.goschedinit初始化、参数、环境处理
22.4golang(.go)$GOROOT/src/runtime/proc.gonewproc创建主goroutine即runtime.main对应的g
22.5golang(.go)$GOROOT/src/runtime/proc.gomstart启动调度循环
22.6golang(.go)$GOROOT/src/runtime/proc.gomain调用main goroutine运行,但不是用户函数入口的main.main
32.3 => 2.3.1golang(.go)$GOROOT/src/runtime/proc.golockInit(xxx)各类Lock的初始化
32.3 => 2.3.2golang(.go)$GOROOT/src/runtime/proc.gosched.maxmcount = 10000最大线程数
32.3 => 2.3.3golang(.go)$GOROOT/src/runtime/proc.gostackinit()栈初始化
32.3 => 2.3.4golang(.go)$GOROOT/src/runtime/proc.gomallocinit()内存管理器初始化
32.3 => 2.3.5golang(.go)$GOROOT/src/runtime/proc.gomcommoninit()调度器初始化
32.3 => 2.3.6golang(.go)$GOROOT/src/runtime/proc.gogoargs()命令行参数处理
32.3 => 2.3.7golang(.go)$GOROOT/src/runtime/proc.gogoenvs()环境变量处理
32.3 => 2.3.8golang(.go)$GOROOT/src/runtime/proc.goparsedebugvars()调试相关参数处理
32.3 => 2.3.9golang(.go)$GOROOT/src/runtime/proc.gogcinit()垃圾回收器初始化
32.6 => 2.6.1golang(.go)$GOROOT/src/runtime/proc.go(64bit-1G 32bit-250M)Stack栈的最大限制
32.6 => 2.6.2golang(.go)$GOROOT/src/runtime/proc.gosystemstack()启动系统后台监控(垃圾回收,并发调度相关)
32.6 => 2.6.3golang(.go)$GOROOT/src/runtime/proc.goruntime_initruntime包内所有init函数初始化
32.6 => 2.6.4golang(.go)$GOROOT/src/runtime/proc.gogcenable()启动垃圾回收
32.6 => 2.6.5golang(.go)$GOROOT/src/runtime/proc.gomain_init用户包内所有init函数初始化
32.6 => 2.6.6golang(.go)$GOROOT/src/runtime/proc.gomain_main调用用户程序入口执行,由编译器动态生成
32.6 => 2.6.7golang(.go)$GOROOT/src/runtime/proc.goexit(0)执行结束

案例分析

我们创建一个简单的项目,在项目main函数中import一些依赖,查看下用户main_main的方法由编译器动态生成了什么内容。

项目目录及文件结构,大致如下:

% tree
.
├── funcs
│   └── func.go
├── main.go
复制代码

funcs/func.go文件

package funcs

import "fmt"

func init(){
	fmt.Println(" funcs init")
}

func Add(a int, b int) int  {
	fmt.Println("Add method called.")
	return a + b
}
复制代码

main.go文件

package main

import (
	//这里导入项目包
	"program/funcs"
	"fmt"
	//这里导入外部包
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

func init()  {
	fmt.Println("main init",funcs.Add(4,5))
}

func main() {
	a, b := 1, 2
	fmt.Println("result => ", funcs.Add(a, b))
}
复制代码

通过go tool objdump -s “\.init\.0\b” [program]反编译查看该项目来查看编译情况,这里信息比较多,删减保留主要信息如下:

//===== 系统内部初始化开始 =====
TEXT internal/bytealg.init.0(SB) /usr/local/go/src/internal/bytealg/index_amd64.go
//省略...                           

TEXT runtime.init.0(SB) /usr/local/go/src/runtime/cpuflags_amd64.go
//省略...                                                           

TEXT os.init.0(SB) /usr/local/go/src/os/proc.go
//省略...                           
  proc.go:18            0x10eb852               eb8c                    JMP os.init.0(SB)                       
//省略...                              

//===== 项目包内初始化开始 =====

TEXT program/funcs.init.0(SB) /Users/guanjian/workspace/go/program/funcs/func.go
//省略...                   
  func.go:6             0x10facda               e8617fffff              CALL fmt.Println(SB)                    
//省略...     
  func.go:5             0x10facee               e96dffffff              JMP program/funcs.init.0(SB) 
//省略...                                                               

//===== 外部包初始化开始 =====
TEXT github.com/go-sql-driver/mysql.init.0(SB) /Users/guanjian/go/pkg/mod/github.com/go-sql-driver/mysql@v1.5.0/driver.go
//省略...                                                               
  driver.go:83          0x12707e7               eb97                    JMP github.com/go-sql-driver/mysql.init.0(SB)                                                   
//省略...                                                                                        

TEXT main.init.0(SB) /Users/guanjian/workspace/go/program/main.go
 //省略...                    
  main.go:10            0x128cec0               e83bdee6ff              CALL program/funcs.Add(SB)   
 //省略...                   
  main.go:10            0x128cf75               e8c65ce6ff              CALL fmt.Println(SB)                    
//省略...         
  main.go:9             0x128cf96               e9e5feffff              JMP main.init.0(SB)                     
//省略...                    
//省略...  
复制代码

下面是将以上编译文件主要信息进行了整理,梳理了初始化顺序、包依赖关系、编译文件映射
在这里插入图片描述

初始化顺序

通过编译文件可以梳理得知,go程序的初始化大致可以分为两个部分,分别是Go环境初始化用户环境初始化,Go环境初始化指的是SDK内部执行流程的OS环境识别读取、参数初始化、相关底层支持函数的初始化准备等;用户环境初始化指的是项目中编写的代码逻辑,这里可以再细分为当前项目代码和外部导入包代码。加载顺序是先初始化Go环境,再根据用户代码逻辑从main.main作为入口按序进行初始化。

包依赖关系

包依赖关系的次序与真正的初始化顺序是相反的,在整个依赖链条上最被依赖的包及其init方法是最先被执行初始化的,相反,依赖下游的程序入口main.main的相关初始化操作是最靠后的。

编译文件映射

总结

  • 通过编译文件和gdb的调试可以得知,所有init函数都在同⼀个goroutine 内执⾏的,感兴趣可以参考 github.com/golang/go/b…doInit的实现
  • 所有init函数结束后才会执⾏main.main函数,也就是说先完成初始化再进入程序入口,这点可以帮助我们更好地理解初始化与程序执行入口两者的次序关系,在编码时避免出现问题
  • import会产生对包的依赖,如果依赖包有init函数则会先执行,而init函数中的内容以及依赖也会有此效果,因此不控制使用init会产生隐藏地、不易察觉的依赖链并产生初始化一系列操作,所以慎用该功能,推荐的是只做当前局部作用域的初始化工作,以免带来不必要的问题隐患

参考

《Go语言学习笔记》 雨痕
搭建gdb调试go程序
golang底层 引导、初始化

文章分类
后端
文章标签