跳到主要内容
  1. Skills/
  2. 后端编程/

Go 语言的初始化函数 init()

·字数 2515·6 分钟
golang

初始化函数,是编程语言、编程框架为程序运行过程中的函数调用提供数据初始设置的手段。比如在大家熟悉的 java、python 中,我们可以为类实例成对象时,在构造函数或者初始化函数中对对象的初始值进行设置。在非面向对象的编程中,我们通过按照某种约定或者人为的,在一个重任务启动之前,调用一个数据设置方法用来对环境数据进行初始化。

golang 不是面向对象的语言,没有构造函数,但同样提供了可以用来实现数据初始化的机制,这是语言级别的原生支持。

下面,有悟带大家来看看在 golang 中可以有哪里初始化的方式。

惯例,创建一个示例工程:

~/Projects/go/examples
➜  code goexamples.code-workspace

~/Projects/go/examples
➜  mkdir goinit && cd goinit

~/Projects/go/examples/goinit
➜  go mod init initdemo
go: creating new go.mod: module initdemo

~/Projects/go/examples/goinit
➜  touch helloinit.go
// helloinit.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("youwu.today say hi...😀")
}

在函数体外的变量初始化 #

我们在函数体定义一个变量,并为其赋初始值,这个变量的作用范围(scope)为包级别(变量在初始化后,可以在包中使用)。

// helloinit.go
package main

import (
    "fmt"
)

var initA string
var initB = "我是 initB"

func main() {
    fmt.Println("youwu.today say hi...😀")

    fmt.Println("* 变量 initA: ", initA)
    fmt.Println("* 变量 initB: ", initB)
}
~/Projects/go/examples/goinit
➜  go run helloinit.go
youwu.today say hi...😀
* 变量 initA:
* 变量 initB:  我是 initB

在有悟文章的 《golang 中的零值问题与判断为空》 中的谈到默认值的问题,所以 initA 是一个 长度为0的字符串 ,有兴趣的同学可以看下。

在 init 函数中初始化 #

golang 为我们提供了 init() 函数,这个函数是隐式调用的,即会在包引入就执行。

// helloinit.go
package main

import (
    "fmt"
)

var initA string
var initB = "我是 initB"
var initC string

func init() {
    initC = "设置为 hello"
}

func main() {
    fmt.Println("youwu.today say hi...😀")

    fmt.Println("* 变量 initA: ", initA)
    fmt.Println("* 变量 initB: ", initB)
    fmt.Println("* 变量 initC: ", initC)
}
~/Projects/go/examples/goinit
➜  go run helloinit.go
youwu.today say hi...😀
* 变量 initA:
* 变量 initB:  我是 initB
* 变量 initC:  设置为 hello

main() 函数中并没有直接调用init(),go在编译的时候,已经确保 init 会在 main执行之前就被调用,这种方式就是隐式。

在包中的init函数 #

上节演示变量在被声明时、init 函数中进行初始化的手段,那么问题来了。如果你的代码比较长,又使用包的方式组织,为了更清楚的表达,你想在包中每个 go 代码中都来一个 init 函数,不知道是否可行?因为我们知道,在一个 go 的包(package)中,全局变量与函数一样,不管在哪个 go 代码文件中声明,名称是全包内唯一,不能重复的。

例如,像以下的样子,该怎么办?init函数应该怎么样组织,又可以有哪些作为?

┌──────────────────────────────────────────┐
│Package initdemo                          │
│                                          │
│     ┌──────────────────────────────────┐ │
│     │   initdemo1.go  init()?          │ │
│     └──────────────────────────────────┘ │
│     ┌──────────────────────────────────┐ │
│     │   initdemo2.go  init()?          │ │
│     └──────────────────────────────────┘ │
└──────────────────────────────────────────┘

init 是 go 中的特殊函数,接受特别的对待。它是 go 中唯一可以在一个包内的不同代码文件中多次定义的函数名称。

即使可以这么做,事实上可能不建议,因为这会让初始化代码看起来很混乱,对维护不友好。你应该根据实际需要来设计代码的组织方式。不过下面我们还是来看看被引用包中的 init 函数。

我们来改进下例子,创建一个名为 demopackage,并新建两个 go 文件,initdemoA.goinitdemo2.go(它们都包含了 init 函数,内容如下),并在 helloinit.go 中引用,看看它们的初始化函数执行顺序:

~/Projects/go/examples/goinit
➜  mkdir demo

~/Projects/go/examples/goinit
➜  touch demo/initdemoA.go demo/initdemo2.go
// initdemoA.go
package demo

import "fmt"

func init() {
    fmt.Println("initdemo1.go 的 init 函数给你打招呼了")
    VarInDemo_Initdemo1 = "VarInDemo_Initdemo1"
}

var VarInDemo_Initdemo1 string
// initdemo2.go
package demo

import "fmt"

func init() {
    fmt.Println("initdemo2.go 的 init 函数给你打招呼了")
    VarInDemo_Initdemo2 = "VarInDemo_Initdemo2"
}

var VarInDemo_Initdemo2 string
// helloinit.go
package main

import (
    "fmt"
    "initdemo/demo"
)

var initA string
var initB = "我是 initB"
var initC string

func init() {
    fmt.Println("main 初始化")
    initC = "设置为 hello"
}

func main() {
    fmt.Println("youwu.today say hi...😀")
    fmt.Println("* 变量 initA: ", initA)
    fmt.Println("* 变量 initB: ", initB)
    fmt.Println("* 变量 initC: ", initC)

    fmt.Println(demo.VarInDemo_Initdemo1)
}
~/Projects/go/examples/goinit
➜  go run helloinit.go
initdemo2.go 的 init 函数给你打招呼了
initdemo1.go 的 init 函数给你打招呼了
main 初始化
youwu.today say hi...😀
* 变量 initA:
* 变量 initB:  我是 initB
* 变量 initC:  设置为 hello
VarInDemo_Initdemo1

helloinit.go 中引入 initdemo/demo,在 main 函数执行之前,先是执行被引入包的 init 函数,再执行当前 main 包的 init 函数。 上面还体现了一个细节,就是包 demo 中的多个 init 函数的执行顺序,是按所在 go 代码文件名称的排序来确定执行顺序的。

┌────────────────────────────────┐    ┌───────────────────────────────┐
│package main                    │    │package demo                   │
│                                │    │   ┌───────────────────────┐   │
│  ┌──────────────────────────┐  │    │   │                       │   │
│  │  import "initdemo/demo"  │◀─┼────│   │init() in initdemoA.go │   │
│  └──────────────────────────┘  │    │   │ [2]                   │   │
│                                │    │   └───────────────────────┘   │
│  ┌──────────────────────────┐  │    │   ┌───────────────────────┐   │
│  │ init() in helloinit.go   │  │    │   │                       │   │
│  │  [3]                     │  │    │   │init() in initdemo2.go │   │
│  │ main() in helloinit.go   │  │    │   │ [1]                   │   │
│  └──────────────────────────┘  │    │   └───────────────────────┘   │
└────────────────────────────────┘    └───────────────────────────────┘ 

再次强调,如果包中有多个需要初始化的地方,把 init() 函数定义在每一个 go 代码文件中可能不是你想要的代码组织方式,尽可能将多个 init 函数的逻辑放到一个文件单独管理起来可能是更好的选择,比如你还可以将该文件命名为 init.go,看到文件名会能猜到它的用途。