Golang标准库揭秘系列 | 初始化流程分析

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: Golang 初始化流程分析

前言


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


gdb调试


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


流程调试


网络异常,图片无法展示
|

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

阶段

次序

语言环境

执行文件

执行函数

核心逻辑

1

1.1

汇编(.s)

$GOROOT/src/runtime/rt0_darwin_amd64.s

_rt0_amd64_darwin

汇编引导

1

1.2

汇编(.s)

$GOROOT/src/runtime/asm_amd64.s

_rt0_amd64

汇编引导

1

1.3

汇编(.s)

$GOROOT/src/runtime/asm_amd64.s

rt0_go

汇编引导

2

2.1

golang(.go)

$GOROOT/src/runtime/runtime1.go

args

整理命令行参数

2

2.2

golang(.go)

$GOROOT/src/runtime/os_darwin.go

osinit

确定CPU数量

2

2.3

golang(.go)

$GOROOT/src/runtime/proc.go

schedinit

初始化、参数、环境处理

2

2.4

golang(.go)

$GOROOT/src/runtime/proc.go

newproc

创建主goroutine即runtime.main对应的g

2

2.5

golang(.go)

$GOROOT/src/runtime/proc.go

mstart

启动调度循环

2

2.6

golang(.go)

$GOROOT/src/runtime/proc.go

main

调用main goroutine运行,但不是用户函数入口的main.main

3

2.3 => 2.3.1

golang(.go)

$GOROOT/src/runtime/proc.go

lockInit(xxx)

各类Lock的初始化

3

2.3 => 2.3.2

golang(.go)

$GOROOT/src/runtime/proc.go

sched.maxmcount = 10000

最大线程数

3

2.3 => 2.3.3

golang(.go)

$GOROOT/src/runtime/proc.go

stackinit()

栈初始化

3

2.3 => 2.3.4

golang(.go)

$GOROOT/src/runtime/proc.go

mallocinit()

内存管理器初始化

3

2.3 => 2.3.5

golang(.go)

$GOROOT/src/runtime/proc.go

mcommoninit()

调度器初始化

3

2.3 => 2.3.6

golang(.go)

$GOROOT/src/runtime/proc.go

goargs()

命令行参数处理

3

2.3 => 2.3.7

golang(.go)

$GOROOT/src/runtime/proc.go

goenvs()

环境变量处理

3

2.3 => 2.3.8

golang(.go)

$GOROOT/src/runtime/proc.go

parsedebugvars()

调试相关参数处理

3

2.3 => 2.3.9

golang(.go)

$GOROOT/src/runtime/proc.go

gcinit()

垃圾回收器初始化

3

2.6 => 2.6.1

golang(.go)

$GOROOT/src/runtime/proc.go

(64bit-1G 32bit-250M)

Stack栈的最大限制

3

2.6 => 2.6.2

golang(.go)

$GOROOT/src/runtime/proc.go

systemstack()

启动系统后台监控(垃圾回收,并发调度相关)

3

2.6 => 2.6.3

golang(.go)

$GOROOT/src/runtime/proc.go

runtime_init

runtime包内所有init函数初始化

3

2.6 => 2.6.4

golang(.go)

$GOROOT/src/runtime/proc.go

gcenable()

启动垃圾回收

3

2.6 => 2.6.5

golang(.go)

$GOROOT/src/runtime/proc.go

main_init

用户包内所有init函数初始化

3

2.6 => 2.6.6

golang(.go)

$GOROOT/src/runtime/proc.go

main_main

调用用户程序入口执行,由编译器动态生成

3

2.6 => 2.6.7

golang(.go)

$GOROOT/src/runtime/proc.go

exit(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 内执⾏的,感兴趣可以参考 https://github.com/golang/go/blob/master/src/runtime/proc.godoInit的实现
  • 所有init函数结束后才会执⾏main.main函数,也就是说先完成初始化再进入程序入口,这点可以帮助我们更好地理解初始化与程序执行入口两者的次序关系,在编码时避免出现问题
  • import会产生对包的依赖,如果依赖包有init函数则会先执行,而init函数中的内容以及依赖也会有此效果,因此不控制使用init会产生隐藏地、不易察觉的依赖链并产生初始化一系列操作,所以慎用该功能,推荐的是只做当前局部作用域的初始化工作,以免带来不必要的问题隐患


参考


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

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
2月前
|
Go
第五章 Golang程序流程控制
第五章 Golang程序流程控制
31 0
|
3月前
|
测试技术 Go 开发者
go-carbon v2.3.8 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
27 0
|
4月前
|
Go
golang数据结构篇之栈和队列以及简单标准库
golang数据结构篇之栈和队列以及简单标准库
35 0
|
4月前
|
测试技术 Go 开发者
go-carbon v2.3.7 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
19 2
|
1天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1
|
1天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
6 0
|
4天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
16 1
|
7天前
|
Go 开发者
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
【4月更文挑战第21天】本文介绍了Go语言中的流程控制语句,包括`if`、`switch`和`for`循环。`if`语句支持简洁的语法和初始化语句,但需注意比较运算符的使用。`switch`语句提供多分支匹配,可省略`break`,同时支持不带表达式的形式。`for`循环有多种形式,如基本循环和`for-range`遍历,遍历时修改原集合可能导致未定义行为。理解并避免易错点能提高代码质量和稳定性。通过实践代码示例,可以更好地掌握Go语言的流程控制。
13 3
Golang深入浅出之-Go语言流程控制:if、switch、for循环详解
|
7天前
|
存储 编译器 Go
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
【4月更文挑战第21天】Go语言中的`map`提供快速的键值对操作,包括初始化、增删查改和遍历。初始化时,推荐使用`make()`函数,如`make(map[string]int)`。插入和查询键值对直接通过索引访问,更新则重新赋值。删除键值对需用`delete()`函数,确保键存在。遍历map常用`for range`,注意避免在遍历中修改map。了解这些并避免易错点,能提升代码效率和可读性。
15 1
Golang深入浅出之-掌握Go语言Map:初始化、增删查改与遍历
|
3月前
|
存储 缓存 算法
Golang高性能内存缓存库BigCache设计与分析
【2月更文挑战第4天】分析Golang高性能内存缓存库BigCache设计
73 0