理解Go中的init

来自菜鸟教程
(重定向自理解Go中的init
跳转至:导航、​搜索

介绍

在 Go 中,预定义的 init() 函数会触发一段代码,以便在包的任何其他部分之前运行。 此代码将在 包被导入 后立即执行,并且可以在您需要应用程序在特定状态下初始化时使用,例如当您有特定配置或一组资源用于应用程序时需要开始。 它也用于 导入副作用,这是一种通过导入特定包来设置程序状态的技术。 这通常用于 register 一个包与另一个包,以确保程序正在考虑任务的正确代码。

尽管 init() 是一个有用的工具,但它有时会使代码难以阅读,因为难以找到的 init() 实例会极大地影响代码运行的顺序。 因此,对于刚接触 Go 的开发人员来说,了解这个函数的各个方面非常重要,这样他们在编写代码时可以确保以清晰的方式使用 init()

在本教程中,您将了解 init() 如何用于设置和初始化特定包变量、一次计算以及注册一个包以与另一个包一起使用。

先决条件

对于本文中的一些示例,您将需要:

.
├── bin 
│ 
└── src
    └── github.com
        └── gopherguides

声明 init()

任何时候你声明一个 init() 函数,Go 都会在该包中的任何其他内容之前加载并运行它。 为了演示这一点,本节将介绍如何定义 init() 函数并展示对包运行方式的影响。

我们先以下面的代码为例,不带init()函数:

main.go

package main

import "fmt"

var weekday string

func main() {
    fmt.Printf("Today is %s", weekday)
}

在这个程序中,我们声明了一个名为 weekday 的全局 变量。 默认情况下,weekday 的值为空字符串。

让我们运行这段代码:

go run main.go

因为weekday的值为空,所以当我们运行程序时,会得到如下输出:

OutputToday is

我们可以通过引入一个将weekday的值初始化为当天的init()函数来填充空白变量。 将以下突出显示的行添加到 main.go

main.go

package main

import (
    "fmt"
    "time"
)

var weekday string

func init() {
    weekday = time.Now().Weekday().String()
}

func main() {
    fmt.Printf("Today is %s", weekday)
}

在这段代码中,我们导入并使用了 time 包来获取当前星期几(Now().Weekday().String()),然后使用 init() 来初始化 weekday那个值。

现在当我们运行程序时,它会打印出当前的工作日:

OutputToday is Monday

虽然这说明了 init() 的工作原理,但 init() 更典型的用例是在导入包时使用它。 当您需要先在包中执行特定的设置任务才能使用该包时,这会很有用。 为了演示这一点,让我们创建一个程序,该程序需要特定的初始化才能使包按预期工作。

导入时初始化包

首先,我们将编写一些代码,从 切片 中选择一个随机生物并将其打印出来。 但是,我们不会在初始程序中使用 init()。 这将更好地展示我们遇到的问题,以及 init() 将如何解决我们的问题。

src/github.com/gopherguides/ 目录中,使用以下命令创建一个名为 creature 的文件夹:

mkdir creature

creature 文件夹中,创建一个名为 creature.go 的文件:

nano creature/creature.go

在此文件中,添加以下内容:

生物.go

package creature

import (
    "math/rand"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func Random() string {
    i := rand.Intn(len(creatures))
    return creatures[i]
}

该文件定义了一个名为 creatures 的变量,该变量具有一组初始化为值的海洋生物。 它还有一个 exported Random 函数,该函数将从 creatures 变量中返回一个随机值。

保存并退出此文件。

接下来,让我们创建一个 cmd 包,我们将使用它来编写我们的 main() 函数并调用 creature 包。

在我们创建 creature 文件夹的同一文件级别,使用以下命令创建一个 cmd 文件夹:

mkdir cmd

cmd 文件夹中,创建一个名为 main.go 的文件:

nano cmd/main.go

将以下内容添加到文件中:

cmd/main.go

package main

import (
    "fmt"

    "github.com/gopherguides/creature"
)

func main() {
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
}

这里我们导入了 creature 包,然后在 main() 函数中,使用 creature.Random() 函数检索一个随机生物并打印四次。

保存并退出 main.go

我们现在已经编写了整个程序。 然而,在我们可以运行这个程序之前,我们还需要创建几个配置文件以使我们的代码正常工作。 Go 使用 Go Modules 来配置导入资源的包依赖。 这些模块是放置在包目录中的配置文件,告诉编译器从哪里导入包。 虽然学习模块超出了本文的范围,但我们只需编写几行配置即可使此示例在本地工作。

cmd目录下,创建一个名为go.mod的文件:

nano cmd/go.mod

打开文件后,放入以下内容:

cmd/go.mod

module github.com/gopherguides/cmd
 replace github.com/gopherguides/creature => ../creature

该文件的第一行告诉编译器,我们创建的 cmd 包实际上是 github.com/gopherguides/cmd。 第二行告诉编译器可以在本地磁盘上的 ../creature 目录中找到 github.com/gopherguides/creature

保存并关闭文件。 接下来,在creature目录下创建一个go.mod文件:

nano creature/go.mod

将以下代码行添加到文件中:

生物/go.mod

 module github.com/gopherguides/creature

这告诉编译器我们创建的 creature 包实际上是 github.com/gopherguides/creature 包。 没有这个,cmd 包将不知道从哪里导入这个包。

保存并退出文件。

您现在应该具有以下目录结构和文件布局:

├── cmd
│   ├── go.mod
│   └── main.go
└── creature
    ├── go.mod
    └── creature.go

现在我们已经完成了所有配置,我们可以使用以下命令运行 main 程序:

go run cmd/main.go

这将给出:

Outputjellyfish
squid
squid
dolphin

当我们运行这个程序时,我们收到了四个值并将它们打印出来。 如果我们多次运行程序,我们会注意到我们 总是 得到相同的输出,而不是预期的随机结果。 这是因为 rand 包会创建伪随机数,这些伪随机数将始终为单个初始状态生成相同的输出。 为了获得更多的随机数,我们可以seed包,或者设置一个变化的源,这样每次运行程序的初始状态都不同。 在 Go 中,通常使用当前时间来播种 rand 包。

由于我们希望 creature 包处理随机功能,因此打开此文件:

nano creature/creature.go

将以下突出显示的行添加到 creature.go 文件中:

生物/生物.go

package creature

import (
    "math/rand"
    "time"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func Random() string {
    rand.Seed(time.Now().UnixNano())
    i := rand.Intn(len(creatures))
    return creatures[i]
}

在这段代码中,我们导入了 time 包并使用 Seed() 来播种当前时间。 保存并退出文件。

现在,当我们运行程序时,我们会得到一个随机结果:

go run cmd/main.go
Outputjellyfish
octopus
shark
jellyfish

如果您继续一遍又一遍地运行程序,您将继续获得随机结果。 但是,这还不是我们代码的理想实现,因为每次调用 creature.Random() 时,它还会通过再次调用 rand.Seed(time.Now().UnixNano()) 来重新播种 rand 包。 如果内部时钟未更改,重新播种将增加使用相同初始值播种的机会,这将导致随机模式的可能重复,或者通过让您的程序等待时钟更改来增加 CPU 处理时间。

为了解决这个问题,我们可以使用 init() 函数。 让我们更新 creature.go 文件:

nano creature/creature.go

添加以下代码行:

生物/生物.go

package creature

import (
    "math/rand"
    "time"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func init() {
    rand.Seed(time.Now().UnixNano())
}

func Random() string {
    i := rand.Intn(len(creatures))
    return creatures[i]
}

添加 init() 函数告诉编译器在导入 creature 包时,应该运行一次 init() 函数,为随机数生成提供单个种子。 这确保了我们不会运行过多的代码。 现在如果我们运行程序,我们将继续得到随机结果:

go run cmd/main.go
Outputdolphin
squid
dolphin
octopus

在本节中,我们看到了如何使用 init() 确保在使用包之前执行适当的计算或初始化。 接下来,我们将看到如何在一个包中使用多个 init() 语句。

init() 的多个实例

与只能声明一次的 main() 函数不同,init() 函数可以在整个包中声明多次。 但是,多个 init() 会让人很难知道哪个优先级高于其他。 在本节中,我们将展示如何保持对多个 init() 语句的控制。

在大多数情况下,init() 函数将按照您遇到它们的顺序执行。 我们以下面的代码为例:

main.go

package main

import "fmt"

func init() {
    fmt.Println("First init")
}

func init() {
    fmt.Println("Second init")
}

func init() {
    fmt.Println("Third init")
}

func init() {
    fmt.Println("Fourth init")
}

func main() {}

如果我们使用以下命令运行程序:

go run main.go

我们将收到以下输出:

OutputFirst init
Second init
Third init
Fourth init

请注意,每个 init() 都按照编译器遇到它的顺序运行。 但是,确定调用 init() 函数的顺序可能并不总是那么容易。

让我们看一个更复杂的包结构,其中我们有多个文件,每个文件都声明了自己的 init() 函数。 为了说明这一点,我们将创建一个共享名为 message 的变量并将其打印出来的程序。

删除前面章节中的creaturecmd目录及其内容,替换为如下目录和文件结构:

├── cmd
│   ├── a.go
│   ├── b.go
│   └── main.go
└── message
    └── message.go

现在让我们添加每个文件的内容。 在 a.go 中,添加以下行:

cmd/a.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    fmt.Println("a ->", message.Message)
}

该文件包含一个 init() 函数,该函数从 message 包中打印出 message.Message 的值。

接下来,将以下内容添加到b.go

cmd/b.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    message.Message = "Hello"
    fmt.Println("b ->", message.Message)
}

b.go 中,我们有一个 init() 函数,它将 message.Message 的值设置为 Hello 并打印出来。

接下来,创建 main.go 如下所示:

cmd/main.go

package main

func main() {}

该文件什么都不做,但为程序运行提供了一个入口点。

最后,创建您的 message.go 文件,如下所示:

消息/message.go

package message

var Message string

我们的 message 包声明了导出的 Message 变量。

要运行程序,请从 cmd 目录执行以下命令:

go run *.go

因为我们在 cmd 文件夹中有多个 Go 文件组成 main 包,我们需要告诉编译器 cmd 中的所有 .go 文件] 文件夹应该被编译。 使用 *.go 告诉编译器加载 cmd 文件夹中以 .go 结尾的所有文件。 如果我们发出 go run main.go 命令,程序将无法编译,因为它看不到 a.gob.go 文件中的代码。

这将给出以下输出:

Outputa ->
b -> Hello

根据 Package Initialization 的 Go 语言规范,当在一个包中遇到多个文件时,它们会按字母顺序进行处理。 正因为如此,我们第一次从 a.go 打印出 message.Message 时,值是空白的。 直到 b.go 中的 init() 函数运行后,该值才被初始化。

如果我们将 a.go 的文件名更改为 c.go,我们会得到不同的结果:

Outputb -> Hello
a -> Hello

现在编译器首先遇到 b.go,因此,当 init() 函数在 [X154X ] 遇到。

这种行为可能会在您的代码中产生问题。 更改文件名在软件开发中很常见,并且由于处理init()的方式,更改文件名可能会更改处理init()的顺序。 这可能会产生改变程序输出的不良影响。 为了确保可重现的初始化行为,鼓励构建系统将属于同一包的多个文件按照词法文件名顺序呈现给编译器。 确保按顺序加载所有 init() 函数的一种方法是将它们全部声明在一个文件中。 即使更改了文件名,这也将防止顺序更改。

除了确保 init() 函数的顺序不会改变之外,您还应该尽量避免使用 全局变量 来管理包中的状态,即可以从任何地方访问的变量包裹。 在前面的程序中,message.Message 变量可用于整个包并维护程序的状态。 由于这种访问,init() 语句能够更改变量并破坏程序的可预测性。 为避免这种情况,请尝试在受控空间中使用变量,这些空间尽可能少地访问,同时仍允许程序运行。

我们已经看到您可以在一个包中包含多个 init() 声明。 但是,这样做可能会产生不良影响并使您的程序难以阅读或预测。 避免使用多个 init() 语句或将它们全部保存在一个文件中将确保在移动文件或更改名称时程序的行为不会改变。

接下来,我们将研究如何使用 init() 进行带副作用的导入。

使用 init() 获得副作用

在 Go 中,有时需要导入包不是因为它的内容,而是因为导入包时出现的副作用。 这通常意味着在导入的代码中有一个 init() 语句在任何其他代码之前执行,允许开发人员操纵他们的程序启动的状态。 这种技术称为 导入以产生副作用

导入副作用的一个常见用例是在您的代码中 register 功能,这让包知道您的程序需要使用代码的哪一部分。 例如,在 图像包 中,image.Decode 函数需要知道它试图解码的图像格式(jpgpng、[ X149X] 等)才能执行。 您可以通过首先导入具有 init() 语句副作用的特定程序来完成此操作。

假设您尝试在 .png 文件上使用 image.Decode 并使用以下代码片段:

样本解码片段

. . .
func decode(reader io.Reader) image.Rectangle {
    m, _, err := image.Decode(reader)
    if err != nil {
        log.Fatal(err)
    }
    return m.Bounds()
}
. . .

使用此代码的程序仍然可以编译,但每当我们尝试解码 png 图像时,都会出现错误。

为了解决这个问题,我们需要首先为 image.Decode 注册一个图像格式。 幸运的是,image/png 包包含以下 init() 语句:

图片/png/reader.go

func init() {
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

因此,如果我们将 image/png 导入到我们的解码片段中,那么 image/png 中的 image.RegisterFormat() 函数将在我们的任何代码之前运行:

样本解码片段

. . .
import _ "image/png"
. . .

func decode(reader io.Reader) image.Rectangle {
    m, _, err := image.Decode(reader)
    if err != nil {
        log.Fatal(err)
    }
    return m.Bounds()
}

这将设置我们需要 image.Decode()png 版本的状态和寄存器。 此注册将作为导入 image/png 的副作用发生。

您可能已经注意到 "image/png" 之前的 空白标识符 (_)。 这是必需的,因为 Go 不允许您导入整个程序中未使用的包。 通过包含空白标识符,导入本身的值被丢弃,因此只有导入的副作用通过。 这意味着,即使我们从未在代码中调用 image/png 包,我们仍然可以导入它以产生副作用。

了解何时需要导入包以获取其副作用非常重要。 如果没有正确注册,您的程序可能会编译,但在运行时可能无法正常工作。 标准库中的包将在其文档中声明需要这种类型的导入。 如果您编写的包需要导入以产生副作用,您还应该确保您使用的 init() 语句已记录在案,以便导入您的包的用户能够正确使用它。

结论

在本教程中,我们了解到 init() 函数在加载包中的其余代码之前加载,并且它可以为包执行特定任务,例如初始化所需状态。 我们还了解到,编译器执行多个 init() 语句的顺序取决于编译器加载源文件的顺序。 如果您想了解更多关于 init() 的信息,请查看 Golang 官方文档,或阅读 Go 社区关于函数的讨论

您可以阅读我们的 如何在 Go 中定义和调用函数一文了解更多关于函数的信息,或探索 整个如何在 Go 中编码系列