很多人都说init()函数在 Go 语言中是一种神奇的存在,那么它到底神奇在哪里呢?这里就来聊一聊在 Go 语言中 init() 函数的作用以及它的执行顺序。

作用及调用时机

顾名思义,init是英语单词"initialization"的缩写形式,意思是初始化的意思,用来执行一些初始化操作,它在入口函数main()之前执行,并且一个包中甚至一个文件中,可以有多个init()函数,没有参数和返回值。

话不多说,这里直接上代码验证一下:

package main

import "fmt"

var initA = printA()

func init() {
    fmt.Println("run init 1")
}

func init() {
        fmt.Println("run init 2")
}

func printA() bool {
    fmt.Println("run a")
    return true
}

func main() {
    fmt.Println("run main")
}

输出:

run a
run init 1
run init 2
run main

通过这个验证的结果可以看出,init()函数有以下这几个特征:

  • 一个文件或一个包中可以有多个init()
  • init()函数没有参数也没有返回值
  • 不可以主动调用init()函数
  • 在初始化变量之后,main()之前执行

在项目中,init()函数可以用来初始化配置、初始化数据库、注册插件等。

单个包中 init() 的执行顺序

上面说到在一个文件或一个包中可以定义多个init()函数,那么它们的执行顺序是怎样的呢?假设这包中有三个文件:a.go b.go main.go,内容如下:

// a.go
package main

import "fmt"

func init() {
    fmt.Println("run init 1 from a.go")
}

func init() {
    fmt.Println("run init 2 from a.go")
}

// b.go
package main

import "fmt"

func init() {
    fmt.Println("run init 1 from b.go")
}

func init() {
    fmt.Println("run init 2 from b.go")
}

// main.go
package main

import "fmt"

func init() {
    fmt.Println("run init 1 from main.go")
}

func init() {
    fmt.Println("run init 2 from main.go")
}

func main() {
    fmt.Println("run main")
}

执行后,输出内容如下:

run init 1 from a.go
run init 2 from a.go
run init 1 from b.go
run init 2 from b.go
run init 1 from main.go
run init 2 from main.go
run main

经过多次反复试验,输入的结果始终都是上面的顺序。

综上所得结论,同一个包中的不同文件的多个init()函数的执行顺序如下:

  • 包下有多个init()函数,按照源文件名的(ASCII)排序从前往后执行。
  • 同一个文件下多个init()函数,按照出现顺序执行

多个包中多个 init() 函数的执行顺序

接着,我们再来看下多个包中有多个 init() 函数的情况。这里有ab两个包,然后再入口文件main.go中依赖这两个包,目录结构如下:

.
├── a
│   └── a.go
├── b
│   └── b.go
├── go.mod
└── main.go

其中,入口main.go中的内容如下:

package main

import (
    "fmt"

    _ "github.com/project/demo/a"
    _ "github.com/project/demo/b"
)

func init() {
    fmt.Println("run init 1 from main.go")
}

func init() {
    fmt.Println("run init 2 from main.go")
}

func main() {
    fmt.Println("run main")
}

输出结果如下:

run init 1 from a/a.go
run init 2 from a/a.go
run init 1 from b/b.go
run init 2 from b/b.go
run init 1 from main.go
run init 2 from main.go
run main

可以看下,多个包中的多个init()函数的执行顺序是根据被导入的顺序执行,即先导入的先执行。

如果多个包是存在依赖顺序的话,又是怎样的执行顺序呢?这里就不上代码了,直接上结论:多个包存依赖关系的包中的init()函数是按照依赖关系从里到外依次执行的,就是最深层的包中的init()函数先执行。

总结

通过前面的介绍,对init()的作用以及执行顺序有了一定的了解。

  • 包下有多个init()函数,按照源文件名的(ASSIC)排序从前往后执行。
  • 同一个文件下多个init()函数,按照出现顺序执行
  • 多个包存依赖关系的包中的init()函数是按照依赖关系从里到外依次执行的

最后,尽管init()的执行有一定的顺序,但是在日常编码中应该尽量避免去依赖init()的执行顺序。