理解 Go 中的 init 函数

在 Go 中有两个为特殊目的而保留的预定义函数。一个是init(),另一个是main()init()函数通常用于初始化应用程序的特定状态。虽然很有用,但它有时会使代码难以阅读。在这里,我们快速浏览一下init()函数、它的用途以及它背后的概念,以及一些 Golang 编码示例。

在我们开始之前,如果您是 Go 新手或想重温一些 Go 概念,请随时查看我们 Go 系列中的以下一些 Golang 编程教程:

Go 和 Golang 中的 init() 函数

init()函数是保留的,用于特定原因。此函数被定义为不接受任何参数并且不返回任何内容。这个函数应该在任何其他代码之前运行——甚至在main()函数之前,这应该是我们程序开始执行的第一个函数。init()函数用于某些特定目的,例如初始化应用程序的特定状态。例如,假设我们想在执行我们的程序之前找出当前运行的操作系统的类型。我们可以使用以下 Go 代码来做到这一点:

package main

import (
	"fmt"
	"os"
	"runtime"
)

func init() {
	if runtime.GOOS == "linux" {
		fmt.Println("This program will run.")
	} else {
		fmt.Println("Program will exit now.")
		os.Exit(1)
	}

}

func main() {
	fmt.Println("running main function")
}

如果您在代码编辑器或 IDE 中运行它,您将获得以下输出:

This program will run.
running main function

Go 的运行时包提供了GOOS(Go 操作系统)字符串常量来标识程序当前运行的操作系统。此常量的典型值为LinuxWindowsDarwin (Mac OS X) 和Freebsd

如何定义 init()

观察上面示例中的init()函数在运行main()函数之前自动调用并检查运行时。GOOS常数。每当在代码中声明init()函数时,Go 在包中的任何其他内容之前加载并运行它。然而,一个包可以有多个init()函数,并且所有这些函数都在主包的main()函数之前执行。另请注意,init()函数永远不会被显式调用。它由 Go 运行时隐式调用。main()不同,init()函数是可选的,但可以在一个包中出现多次;另一方面,每个程序的主包中都必须有一个main()函数。

在多个init()函数的情况下,它们的声明顺序在代码中很重要,因为它们完全按照该顺序执行。因此,如果您希望一个init()函数首先执行,那么它必须是包中声明的第一个函数。这是一个简单的例子来说明这个想法:

package main

import (
	"fmt"
)

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

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

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

func main() {
	fmt.Println("Running main function")
}

这会产生以下输出:

Veni.
Vidi.
Vici.
Running main function

现在,更改init()函数的顺序并编译和执行相同的代码。运行代码示例以查看输出如何变化。

Go 中的 init() 函数是做什么用的?

init()函数可以有多种用途——特别是当您希望在main()函数执行之前执行或初始化某些内容时。它的典型用途之一是在导入包时。可以在允许使用包之前完成特定的设置任务。下面是一个简单的例子来说明这个想法:

package randgreet

import (
	"math/rand"
	"time"
)

var greet = []string{"Good Morning", "Guten Morgen", "Bonjour", "สวัสดีตอนเช้า", "Chào buổi sáng", "өглөөний мэнд"}

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

func RandGreetLang() string {
	return greet[rand.Intn(len(greet))]
}


package main

import (
	"fmt"

	"example.com/mano/gtk_proj1/randgreet"
)

func main() {
	fmt.Println(randgreet.RandGreetLang())
	fmt.Println(randgreet.RandGreetLang())
	fmt.Println(randgreet.RandGreetLang())
}

在代码编辑器中运行时,这会产生以下输出:

Chào buổi sáng
Guten Morgen
สวัสดีตอนเช้า

在上面的例子中有两个包。主包导入randgreet并调用RandGreetLang。此函数以字符串格式返回问候消息。问候语是从由字符串数组表示的列表中以随机方式选择的。数学包中的随机函数使用当前时间作为其种子值进行初始化。

现在,我们想要的是,每当导入randgreet包时,我们要确保初始化随机函数的种子值。这只能通过使用init()函数来完成,这就是我们在代码中所做的。如果我们在RandGreetLang函数中初始化随机种子,它会在每次调用该函数时执行(观察我们如何在 main 中调用了3 次RandGreetLang函数)。这不是我们想要的。我们只是希望随机函数只初始化一次。init()函数使这成为可能,并且仅在导入包时执行一次。这种类型的场景适合使用init()功能。

另一个可以很好地使用init()函数的示例场景是:假设您的程序处理图片并将图像文件作为输入。有不同类型的图像:JPEG、PNG、GIF 等。每个都必须以不同的方式处理。因此,在处理 main 函数之前,init可以确定图像文件的类型,并指导程序采取适当的行动。

init() 的问题

我们知道,在单个文件中声明的多个init()函数是按照它们的声明顺序执行的。这很好,但是如果在多个文件中声明了init()函数会怎样。那么执行的顺序是什么?根据 Go 语言规范,在包中的多个文件中声明的init()函数按照文件名的字母顺序进行处理。例如,a.go 文件中的init()声明将在b.go文件中声明的init()函数之前处理

这种行为有时可能会成为问题,因为文件的简单重命名可能会改变init()函数的执行顺序,这可能会产生不良影响。消除此问题的方法是在单个文件中声明所有init()函数或确保文件名在同一包中保持词法顺序。

尽管有可能,但不应允许init()函数更改或管理任何影响包状态的全局变量。对整个包可用的变量与包状态有直接关系。来自任何init()函数的任何不良变化都可能破坏程序的可预测性。事实上,全局变量因意外误用而臭名昭著。但它们不能完全丢弃,因为在某些情况下它们有助于跨函数共享值。良好的编程习惯是使用全局变量的频率低于需要它们的频率。变量具有尽可能局部可访问性的安全性。

Golang init()

在 Go 中,init()是一个非常特殊的函数,意在在main()函数之前执行。与main()不同,一个或多个文件中可以有多个init()函数。对于单个文件中的多个init,它们的处理顺序是按照它们的声明顺序,而在多个文件中声明的init是按照字典文件名顺序处理的。init()函数是可选的。编写一个可运行的程序而无需对init()进行任何跟踪是完全可以的功能。它特别用于初始化和设置程序或任何其他认为合适的目的。不过请注意——滥用init()可能会出现问题。