介绍
在 Go 中,预定义的 init()
函数会启动代码,在您的包的任何其他部分之前运行。 此代码将在 包被导入后执行,并且可以在您需要您的应用程序在特定状态中进行初始化时使用,例如当您有一个特定的配置或资源组合,您的应用程序需要启动时使用。
虽然init() 是一个有用的工具,但它有时会使代码难以读取,因为很难找到的init() 实例会极大地影响代码运行顺序。
在本教程中,您将了解init()
如何用于设置和初始化特定的包变量,一次时间计算,以及注册一个包用于另一个包。
前提条件
对于本文中的一些例子,您将需要:
1.
2├── bin
3│
4└── 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系列。