Init函数执行流程
in Go with 0 comment

Init函数执行流程

in Go with 0 comment

在Go1.14之后,init函数进行了改动,每个package会按需生成一个package..inittask的符号

该符号有三个字段

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
}

一个小例子

image-20210516001425942

执行时机

在用户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 // 设置初始化状态为已完成
	}
}

从源码可以看出:

比如说如下的例子

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,如注释所述每个packageinit函数需要做三件事

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.OpenLookup方法,用来加载动态库和查找符号,对应的底层方法是dlopendlsym

//go:linkname可以用来引用Go Runtime的一些私有方法,或者其他package的私有方法,本质是会将对应的符号进行重定向操作。