在Go1.14之后,init函数进行了改动,每个package会按需生成一个package..inittask
的符号
该符号有三个字段
- state 本package init的状态,0:未初始化,1:正在初始化中,2:初始化完成
- ndeps 本package init所依赖的package个数
- nfns 本package中需要动态初始化的变量(var s = make([]int,10))和显式写的func init的个数
- 后续是按照内存偏移,将具体所依赖的package..inittask、和init函数指针,写入到结构体后面
type initTask struct {
// TODO: pack the first 3 fields more tightly?
state uintptr // 0 = uninitialized, 1 = in progress, 2 = done
ndeps uintptr
nfns uintptr
// followed by ndeps instances of an *initTask, one per package depended on
// followed by nfns pcs, one per init function to run
}
一个小例子
执行时机
在用户main函数执行之前,按照深度优先的方式进行初始化
// $GOROOT/src/runtime/proc.go
// 这个是`runtime.main`,也是运行的第一个goroutine,
// 会先进行GC初始化等操作,运行`runtime..inittask`,
// 运行`main..inittask`,再调用用户的 `func main`
// The main goroutine.
func main() {
g := getg()
// Allow newproc to start new Ms.
mainStarted = true
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil, -1)
})
}
// 先执行runtime的init函数
doInit(&runtime_inittask) // must be before defer
main_init_done = make(chan bool)
// Note: 执行用户main包的init函数
doInit(&main_inittask)
close(main_init_done)
needUnlock = false
unlockOSThread()
fn := main_main
fn() // 调用 用户的 func main
// ……
// 设置程序退出码为0,go的 main 函数没有返回值。
exit(0)
}
func doInit(t *initTask) {
switch t.state {
case 2: // 该package已初始化完成,直接返回,避免重复初始化
return
case 1: // 如果调用doInit时,状态为1,说明在初始话某个package的过程中,又调用到了自身(类似于循环引用),是一种不应该发生的情况,直接报错
throw("recursive call during initialization - linker skew")
default: // 默认为0
t.state = 1 // 先将设置为1,正在初始化中
for i := uintptr(0); i < t.ndeps; i++ {
// 3是 state+ndeps+nfns 这三个字段的长度
// 按照偏移去找到具体的dep,强转为*initTask,并递归调用doInit
p := add(unsafe.Pointer(t), (3+i)*sys.PtrSize)
t2 := *(**initTask)(p)
doInit(t2)
}
// 执行到这里时,本package所有依赖的package的init函数已执行完成,下面开始执行本package中需要动态初始化和用户显示写的func init函数
for i := uintptr(0); i < t.nfns; i++ {
p := add(unsafe.Pointer(t), (3+t.ndeps+i)*sys.PtrSize)
f := *(*func())(unsafe.Pointer(&p))
f()
}
t.state = 2 // 设置初始化状态为已完成
}
}
从源码可以看出:
-
通过
state
是可以保证每个package
只会被执行一次的 -
执行顺序是按照深度优先的方式进行初始化,所以可以保证其所依赖的package的初始化一定在当package初始化之前
-
同一个package中的初始化函数实际上是没有层级关系的,所以不应该依赖同一个package中初始函数的执行顺序
比如说如下的例子
package main
import "fmt"
var global map[int64]string
func init() {
global = make(map[int64]string, 100)
fmt.Println("init 1")
}
func init() {
global[1] = "1"
fmt.Println("init 2")
}
func main() {
fmt.Println("Hello World")
}
// Output:
// init 1
// init 2
// Hello World
虽然程序可以正常运行,且输出是稳定的,但是这依赖于编译器的实现,先扫描到哪个func init
,再极端一点,如果再同一个package中的两个文件中,那又如何保证顺序呢?
从源码来说,这两个init 是没有层级关系的,Go对于同一个package
中的init
函数的执行顺序也没有任何说明,不建议使用这种未明确的“特性”。
initTask是如何生成的?
实际上是在编译过程中写入到程序二进制中的,在程序启动时被加载到内存中,所以后续的处理都是使用unsafe.Pointer和uintptr进行内存偏移的方式
fninit
的粒度是package
,如注释所述每个package
的init
函数需要做三件事
- 初始化 当前
package
所依赖的package
- 初始化 当前
package
中所有需要初始化的变量 - 执行 用户所写的
func init()
func fninit(n []*Node) {
nf := initOrder(n)
var deps []*obj.LSym // initTask records for packages the current package depends on
var fns []*obj.LSym // functions to call for package initialization
// 将当前Package所import的package的信息加入到 deps 中
for _, s := range types.InitSyms {
deps = append(deps, s.Linksym())
}
// 对于所有需要初始化的变量,封装到package.init函数中,并加入到fns中
if len(nf) > 0 {
lineno = nf[0].Pos // prolog/epilog gets line number of first init stmt
initializers := lookup("init")
disableExport(initializers)
fn := dclfunc(initializers, nod(OTFUNC, nil, nil))
for _, dcl := range dummyInitFn.Func.Dcl {
dcl.Name.Curfn = fn
}
fn.Func.Dcl = append(fn.Func.Dcl, dummyInitFn.Func.Dcl...)
dummyInitFn.Func.Dcl = nil
……
fns = append(fns, initializers.Linksym())
}
// 对于用户所写的 func init 也加入到fns中
// 用户所写的 func init 在之前的流程中已经将Name重置为 package.init.X 从0按顺序进行生成
for i := 0; i < renameinitgen; i++ {
s := lookupN("init.", i)
fn := asNode(s.Def).Name.Defn
if fn.Nbody.Len() == 0 {
continue
}
fns = append(fns, s.Linksym())
}
// 如果没有需要进行初始化的操作,则直接返回(按需生成packege..inittask)
// 对于 main 和 runtime 包会继续向下走
if len(deps) == 0 && len(fns) == 0 && localpkg.Name != "main" && localpkg.Name != "runtime" {
return // nothing to initialize
}
// 生成 packege..inittask 符号
sym := lookup(".inittask")
// 写入state字段,占用一个uintptr长度
ot = duintptr(lsym, ot, 0) // state: not initialized yet
// 写入deps 的长度字段,占用一个uintptr长度
ot = duintptr(lsym, ot, uint64(len(deps)))
// 写入fns 的长度字段,占用一个uintptr长度
ot = duintptr(lsym, ot, uint64(len(fns)))
// 写入deps字段详细信息,每条信息为一个 *inittask
for _, d := range deps {
ot = dsymptr(lsym, ot, d, 0)
}
// 写入fns字段详细信息,每条信息为一个 *func()
for _, f := range fns {
ot = dsymptr(lsym, ot, f, 0)
}
ggloblsym(lsym, int32(ot), obj.NOPTR)
}
如何调试initTask
Go 1.16中提供了GODEBUG=inittrace
环境变量来进行initTask的调试
> GODEBUG=inittrace=1 ./gin-vue-admin
init internal/bytealg @0.011 ms, 0 ms clock, 0 bytes, 0 allocs
init runtime @0.42 ms, 0.72 ms clock, 0 bytes, 0 allocs
init errors @1.7 ms, 0.18 ms clock, 0 bytes, 0 allocs
init math @1.9 ms, 0.13 ms clock, 0 bytes, 0 allocs
……
init os @4.5 ms, 0.38 ms clock, 4456 bytes, 20 allocs
init fmt @4.9 ms, 0.17 ms clock, 32 bytes, 2 allocs
init context @5.1 ms, 0.005 ms clock, 128 bytes, 4 allocs
init path/filepath @5.1 ms, 0.16 ms clock, 16 bytes, 1 allocs
……
init go.uber.org/zap @14 ms, 0.33 ms clock, 888 bytes, 15 allocs
init github.com/go-redis/redis/internal/pool @14 ms, 0.15 ms clock, 32 bytes, 2 allocs
init github.com/go-redis/redis @15 ms, 0.16 ms clock, 192 bytes, 6 allocs
init encoding/csv @15 ms, 0.15 ms clock, 80 bytes, 5 allocs
init github.com/spf13/pflag @15 ms, 0.014 ms clock, 272 bytes, 2 allocs
init github.com/spf13/afero/mem @16 ms, 0 ms clock, 48 bytes, 3 allocs
init regexp/syntax @16 ms, 0.19 ms clock, 7648 bytes, 9 allocs
可以看到被初始化的package
、初始化开始的时间、初始化过程的耗时、初始化过程中的内存分配大小、初始化过程中的内存分配次数的信息。
也可以看出初始化的顺序大致为 runtime -> 标准库 -> 第三方库
End
这篇文章没有魔改定制版,额外说个冷知识
initTask
在什么时候会被调用?
上方已经说了,会在runtime.main
goroutine中,用户main
函数执行之前,会手动调用执行runtime和main的初始化函数,并进行递归初始化。
后续还有没有其他地方会调用呢?
答案是 有,加载plugin
的时候如果有需要执行的inittask
,也会调用doInit
来递归执行。
$GOROOT/src/plugin/plugin_dlopen.go
func open(name string) (*Plugin, error) {
filepath := C.GoString((*C.char)(unsafe.Pointer(&cPath[0])))
var cErr *C.char
h := C.pluginOpen((*C.char)(unsafe.Pointer(&cPath[0])), &cErr)
if h == 0 {
pluginsMu.Unlock()
return nil, errors.New(`plugin.Open("` + name + `"): ` + C.GoString(cErr))
}
initStr := make([]byte, len(pluginpath)+len("..inittask")+1) // +1 for terminating NUL
copy(initStr, pluginpath)
copy(initStr[len(pluginpath):], "..inittask")
initTask := C.pluginLookup(h, (*C.char)(unsafe.Pointer(&initStr[0])), &cErr)
if initTask != nil {
// Note
doInit(initTask)
}
……
return p, nil
}
// doInit is defined in package runtime
//go:linkname doInit runtime.doInit
func doInit(t unsafe.Pointer) // t should be a *runtime.initTask
这里先埋两个坑,plugin
、//go:linkname
,等后续再填吧。
简单来说
plugin
是一种可动态加载的插件机制,可以将Go的main
package
编译生成一个so动态库,提供了plugin.Open
和Lookup
方法,用来加载动态库和查找符号,对应的底层方法是dlopen
和dlsym
。
//go:linkname
可以用来引用Go Runtime的一些私有方法,或者其他package的私有方法,本质是会将对应的符号进行重定向操作。
本文由 LeonardWang 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Sep 26,2022