Go 语言
函数和方法
init 函数

init 函数

从程序逻辑结构角度来看,包是 Go 程序逻辑封装的基本单元。一个 Go 程序就是由一组包组成。

在 Go 包这一基本单元中分布着常量、包级变量、函数、类型和类型方法、接口等,我们要保证包内部的这些元素在被使用之前出于合理的有效的初始状态,这就需要用到 init 函数。

什么是 init 函数

Go 语言中有两个特殊的函数,一个是 main 函数,它是所有 Go 可执行程序的入口;另一个是 init 函数,它是在程序执行前被 Go 运行时调用的。

init 函数是一个无参数、无返回值的函数,它只能由 Go 程序自动调用,不能被用户代码调用。

如果一个包定义了 init 函数,Go 运行时会负责在该包初始化时调用它的 init 函数。在 Go 程序中我们不能显式调用 init,否则会在编译时引发错误。

一个包可以包含多个 init 函数,每个组成 Go 包的 Go 源文件中可以定义多个 init 函数。

在初始化包时,Go 运行时会按照一定的次序逐一调用该包的 init 函数。Go 运行时不会并发调用 init ,它会等待上一个 init 函数结束后再调用下一个,且每个 init 函数在整个 Go 程序的生命周期中只会被调用一次。

多个文件中的多个 init 函数的执行次序是先被传递到 Go 编译器的源文件中的 init 函数先被执行,同一个源文件中的多个 init 函数的执行次序是按照执行。

但 Go 语言惯例告诉我们:不要依赖 init 函数的执行次序。

程序初始化顺序

Go 程序由一组包组合而成,而程序的初始化就是这些包的初始化。顺序主要概括为如下三个步骤:

  1. 如果包还未被初始化,则初始化该包。
  2. 初始化包级变量。
  3. 如果该包包含 init 函数,则调用该函数。

如图所示:

go-init-order

init 函数的应用场景

init 函数就好比 Go 包真正投入使用之前的质检员,负责对包内部以及暴露到外部的包级数据的初始状态进行检查。

重置包级变量值

我们看看标准库 context 包的 init 函数:

var closedchan = make(chan struct{})
 
func init() {
    close(closedchan)
}

context 包在 cancelCtx 结构体中定义了一个 closed 字段,它是一个只读的只能被关闭的通道。在包初始化时,context 包会调用 init 函数关闭这个通道。

初始化包级变量

标准库 http 包在 init 函数中根据环境变量 GODEBUG 的值对一些包级开关变量进行赋值:

var (
    http2VerboseLogs bool
    http2logFrameWrites bool
    http2logFrameReads bool
    http2inTests bool
)
 
func init() {
    if debug := os.Getenv("GODEBUG"); debug != "" {
        for _, f := range strings.Split(debug, ",") {
            switch f {
            case "http2debug=1":
                http2VerboseLogs = true
            case "http2debug=2":
                http2VerboseLogs = true
                http2logFrameWrites = true
                http2logFrameReads = true
            }
        }
    }
}

注册模式

使用标准库 image 包获取各种图片的宽和高:

import (
    "fmt"
    "image"
    _ "image/gif"
    _ "image/jpeg"
    _ "image/png"
    "os"
)
 
func main() {
    width, height, err := getImageDimension(os.Args[1])
    if err != nil {
        fmt.Println("get image size error", err)
        return
    }
    fmt.Printf("image size: [%d, %d]\n", width, height)
}
 
func getImageDimension(imagePath string) (int, int, error) {
    file, err := os.Open(imagePath)
    if err != nil {
        return 0, 0, err
    }
    defer file.Close()
 
    image, _, err := image.DecodeConfig(file)
    if err != nil {
        return 0, 0, err
    }
 
    b := image.Bounds()
    return b.Max.X, b.Max.Y, nil
}

这个程序支持 PNG、JPEG 和 GIF 三种图片格式,但是我们在代码中并没有显式地引入这三个包,这是因为 image 包的 init 函数中注册了这三个格式的解码器:

// src/image/png/reader.go
func init() {
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
// src/image/jpeg/reader.go
func init() {
    image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
// src/image/gif/reader.go
func init() {
    image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}