了解 Go 中的 init

介绍

在 Go 中,预定义的 init() 函数会启动代码,在您的包的任何其他部分之前运行。 此代码将在 包被导入后执行,并且可以在您需要您的应用程序在特定状态中进行初始化时使用,例如当您有一个特定的配置或资源组合,您的应用程序需要启动时使用。

虽然init() 是一个有用的工具,但它有时会使代码难以读取,因为很难找到的init() 实例会极大地影响代码运行顺序。

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

前提条件

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

1.
2├── bin 
34└── src
5    └── github.com
6        └── gopherguides

点击点击INIT()

每当你声明一个init()函数时,Go 都会在该包中的任何其他内容之前加载并运行它. 为了证明这一点,本节将介绍如何定义一个init()函数,并显示对该包如何运行的效果。

首先,让我们把以下作为一个没有init()函数的代码的例子:

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6var weekday string
 7
 8func main() {
 9    fmt.Printf("Today is %s", weekday)
10}

在此程序中,我们声明了一个名为周日的全球 变量

让我们运行这个代码:

1go run main.go

由于周日的值是空的,当我们运行程序时,我们会得到以下输出:

1[secondary_label Output]
2Today is

我们可以通过引入一个init()函数来填写空变,该函数将周日的值初始化为当前日。

 1[label main.go]
 2package main
 3
 4import (
 5    "fmt"
 6    "time"
 7)
 8
 9var weekday string
10
11func init() {
12    weekday = time.Now().Weekday().String()
13}
14
15func main() {
16    fmt.Printf("Today is %s", weekday)
17}

在此代码中,我们导入并使用时间包来获取当前的周日(Now().Weekday().String()),然后使用init()以与该值初始化周日

现在,当我们运行该程序时,它将打印当前的周日:

1[secondary_label Output]
2Today is Monday

虽然这说明了init()是如何工作的,但对于init()来说,一个更为典型的用例是在导入包时使用它,这在您想要使用包之前需要在包中执行特定设置任务时很有用。

进口包的初始化

首先,我们会写一些代码,从 Slice中选择一个随机的生物,并打印出它,但是,我们不会在我们的初始程序中使用init()。

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

1mkdir creature

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

1nano creature/creature.go

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

 1[label creature.go]
 2package creature
 3
 4import (
 5    "math/rand"
 6)
 7
 8var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
 9
10func Random() string {
11    i := rand.Intn(len(creatures))
12    return creatures[i]
13}

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

保存并删除此文件。

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

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

1mkdir cmd

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

1nano cmd/main.go

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

 1[label cmd/main.go]
 2package main
 3
 4import (
 5    "fmt"
 6
 7    "github.com/gopherguides/creature"
 8)
 9
10func main() {
11    fmt.Println(creature.Random())
12    fmt.Println(creature.Random())
13    fmt.Println(creature.Random())
14    fmt.Println(creature.Random())
15}

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

保存并停止main.go

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

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

1nano cmd/go.mod

一旦文件被打开,放置在以下内容:

1[label cmd/go.mod]
2module github.com/gopherguides/cmd
3 replace github.com/gopherguides/creature => ../creature

该文件的第一行告诉编译器我们创建的cmd包实际上是github.com/gopherguides/cmd

保存并关闭文件 下,在生物目录中创建一个go.mod文件:

1nano creature/go.mod

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

1[label creature/go.mod]
2 module github.com/gopherguides/creature

这告诉编译器,我们创建的生物包实际上是github.com/gopherguides/creature包。

保存并删除文件。

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

1├── cmd
2│   ├── go.mod
3│   └── main.go
4└── creature
5    ├── go.mod
6    └── creature.go

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

1go run cmd/main.go

这将给出:

1[secondary_label Output]
2jellyfish
3squid
4squid
5dolphin

当我们运行这个程序时,我们收到了四个值,并打印了它们。如果我们运行该程序多次,我们会注意到我们 总是得到相同的输出,而不是预期的随机结果。这是因为rand包创建了假例数,这将持续地为单个初始状态生成相同的输出。

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

1nano creature/creature.go

在「creature.go」文件中添加以下突出的行:

 1[label creature/creature.go]
 2package creature
 3
 4import (
 5    "math/rand"
 6    "time"
 7)
 8
 9var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
10
11func Random() string {
12    rand.Seed(time.Now().UnixNano())
13    i := rand.Intn(len(creatures))
14    return creatures[i]
15}

在这个代码中,我们导入了时间包,并使用Seed()来播种当前时间。

现在,当我们运行该程序时,我们将获得随机结果:

1go run cmd/main.go
1[secondary_label Output]
2jellyfish
3octopus
4shark
5jellyfish

然而,这还不是我们代码的理想实现,因为每次被调用creature.Random(),它也会通过再次调用rand.Seed(time.Now().UnixNano())来重新播种rand包,从而重新播种rand.Seed(time.Now().UnixNano())

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

1nano creature/creature.go

添加以下代码行:

 1[label creature/creature.go]
 2package creature
 3
 4import (
 5    "math/rand"
 6    "time"
 7)
 8
 9var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}
10
11func init() {
12    rand.Seed(time.Now().UnixNano())
13}
14
15func Random() string {
16    i := rand.Intn(len(creatures))
17    return creatures[i]
18}

添加init()函数告诉编译器,当生物包被导入时,它应该运行init()函数一次,为随机数字生成提供一个单个种子。

1go run cmd/main.go
1[secondary_label Output]
2dolphin
3squid
4dolphin
5octopus

在本节中,我们看到了使用init() 如何确保在使用包之前进行适当的计算或初始化。

「init()」的多个实例

与只能宣称一次的主()函数不同,init()函数可以在整个包中宣称多次。

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

 1[label main.go]
 2package main
 3
 4import "fmt"
 5
 6func init() {
 7    fmt.Println("First init")
 8}
 9
10func init() {
11    fmt.Println("Second init")
12}
13
14func init() {
15    fmt.Println("Third init")
16}
17
18func init() {
19    fmt.Println("Fourth init")
20}
21
22func main() {}

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

1go run main.go

我们将获得以下输出:

1[secondary_label Output]
2First init
3Second init
4Third init
5Fourth init

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

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

从上一节中删除生物cmd目录及其内容,并用以下目录和文件结构取代它们:

1├── cmd
2│   ├── a.go
3│   ├── b.go
4│   └── main.go
5└── message
6    └── message.go

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

 1[label cmd/a.go]
 2package main
 3
 4import (
 5    "fmt"
 6
 7    "github.com/gopherguides/message"
 8)
 9
10func init() {
11    fmt.Println("a ->", message.Message)
12}

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

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

 1[label cmd/b.go]
 2package main
 3
 4import (
 5    "fmt"
 6
 7    "github.com/gopherguides/message"
 8)
 9
10func init() {
11    message.Message = "Hello"
12    fmt.Println("b ->", message.Message)
13}

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

接下来,创建main.go,看起来如下:

1[label cmd/main.go]
2package main
3
4func main() {}

此文件没有任何作用,但为程序运行提供了一个入口点。

最后,创建你的message.go文件如下:

1[label message/message.go]
2package message
3
4var Message string

我们的消息包声明了导出的消息变量。

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

1go run *.go

由于我们在构成主要包的cmd文件夹中有多个Go文件,所以我们需要告诉编译器在cmd文件夹中的所有.go文件都应该被编译。

这将产生以下产出:

1[secondary_label Output]
2a ->
3b -> Hello

根据 Go 语言规格的 Package Initialization,当在一个包中遇到多个文件时,它们会以字母形式处理。 因此,我们第一次从 a.go 打印了 message.Message 时,该值是空的。

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

1[secondary_label Output]
2b -> Hello
3a -> Hello

现在编译器首先会遇到b.go,因此在c.go中遇到init()函数时,message.Message的值已经被初始化为Hello

这种行为可能会在您的代码中造成可能的问题。在软件开发中更改文件名是常见的,由于init()是如何处理的,更改文件名可能会改变init()是如何处理的顺序。这可能会产生改变程序的输出的不良影响。为了确保可重复的初始化行为,构建系统被鼓励向编译器呈现属于同一个包的多个文件,以语义的文件名顺序。确保所有init()函数都被加载的方式之一就是在一个文件中宣布它们。

除了确保你的init()函数的顺序不会改变,你还应该尝试通过使用 global variables 来避免在你的包中管理状态,即从包中的任何地方可访问的变量。在上一个程序中,message.Message 变量可用于整个包,并保持了程序的状态。由于这种访问,init() 陈述能够改变变量并破坏你的程序的可预测性。

但是,这样做可能会产生不必要的效果,使您的程序难以读取或预测。避免多个init()声明或将它们全部保存在一个文件中,将确保您的程序的行为不会改变,当文件被移动或名称被更改时。

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

使用init()为副作用

在Go中,有时不应该因为其内容而导入一个包,而是因为导入包时发生的副作用,这通常意味着导入代码中有一个init()陈述,在其他代码之前执行,允许开发人员操纵他们的程序开始状态。

例如,在 image中,image.Decode函数需要知道它试图解码的图像格式(jpg, png, gif,等等)才能执行。

假设你试图在一个.png 文件中使用 image.Decode,并使用以下代码片段:

 1[label Sample Decoding Snippet]
 2. . .
 3func decode(reader io.Reader) image.Rectangle {
 4    m, _, err := image.Decode(reader)
 5    if err != nil {
 6    	log.Fatal(err)
 7    }
 8    return m.Bounds()
 9}
10. . .

具有此代码的程序仍然会编译,但当我们试图解码一个png图像时,我们会收到错误。

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

1[label image/png/reader.go]
2func init() {
3    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
4}

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

 1[label Sample Decoding Snippet]
 2. . .
 3import _ "image/png"
 4. . .
 5
 6func decode(reader io.Reader) image.Rectangle {
 7    m, _, err := image.Decode(reader)
 8    if err != nil {
 9    	log.Fatal(err)
10    }
11    return m.Bounds()
12}

这将设置状态并注册我们需要的png版本的image.Decode()

您可能在图片/png之前注意到了 空标识符 (_) 这是必要的,因为Go不允许您导入在整个程序中未使用的包。 通过包括空标识符,导入本身的值被丢弃,所以只有导入的副作用才会通过。

重要的是要知道何时需要导入一个软件包以达到副作用. 如果没有正确的注册,你的程序可能会编译,但在运行时不会正常工作. 标准库中的软件包将在文档中声明需要这种类型的导入。

结论

在本教程中,我们了解到init()函数在包中的其他代码被加载之前加载,并且它可以为包执行特定任务,例如初始化所需状态。我们还了解到编译器执行多个init()陈述的顺序取决于编译器加载源文件的顺序。

您可以阅读更多关于函数的文章 如何定义和调用函数在Go,或探索 整个How To Code在Go系列

Published At
Categories with 技术
Tagged with
comments powered by Disqus