main.main 函数:Go 应用的入口函数
main 包中的 main 函数,也就是 main.main,它是所有Go 可执行程序的用户层执行逻辑的入口函数
可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错。
main 函数返回就意味着整个 Go 程序的终结,而且你也不用管这个时候是否还有其他子 Goroutine 正在执行
除了 main 包外,其他包也可以拥有自己的名为 main 的函数或方法。但按照 Go 的可见性规则(小写字母卡头的标识符为非导出标识符),非 main包中自定义的 main 函数仅限于包内使用
不过对于 main 包的main 函数来说,你还需要明确一点,就是它虽然是用户层逻辑的入口函数,但它却不一定是用户层第一个被执行的函数。
init 函数:Go 包的初始化函数
如果 main 包依赖的包中定义了 init 函数,或者是 main 包自身定义了 init 函数,那么 Go 程序在这个包初始化的时候,就会自动调用它的 init 函数,因此这些 init 函数的执行就都会发生在 main 函数之前
Go 程序中我们不能手工显式地调用 init,否则就会收到编译错误,
package main
import "fmt"
func init() {
fmt.Println("init invoked")
}
func main()
{
init()
}
在构建并运行上面这些示例代码之后,Go 编译器会报下面这个错误:
$go run call_init.go
./call_init.go:10:2: undefined: init
Go 包可以拥有不止一个 init 函数,每个组成 Go 包的 Go 源文件中,也可以定义
多个 init 函数。
所以说,在初始化 Go 包时,Go 会按照一定的次序,逐一、顺序地调用这个包的 init 函
数。一般来说,先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文
件中的多个 init 函数,会按声明顺序依次执行。
Go 包的初始化次序
依赖包按“深度优先”的次序进行初始化;
每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
包内的多个 init 函数按出现次序进行自动调用
init 函数的用途
重置包级变量值
2.对包级变量的复杂初始化。
var (
http2VerboseLogs bool // 初始化时默认值为false
http2logFrameWrites bool // 初始化时默认值为false
http2logFrameReads bool // 初始化时默认值为false
http2inTests bool // 初始化时默认值为false
)
func init() {
e := os.Getenv("GODEBUG")
if strings.Contains(e, "http2debug=1") {
http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
}
if strings.Contains(e, "http2debug=2") {
http2VerboseLogs = true // 在init中对http2VerboseLogs的值进行重置
http2logFrameWrites = true // 在init中对http2logFrameWrites的值进行重置
http2logFrameReads = true // 在init中对http2logFrameReads的值进行重置
}
}
我们可以看到,标准库 http 包定义了一系列布尔类型的特性开关变量,它们默认处于关闭
状态(即值为 false),但我们可以通过 GODEBUG 环境变量的值,开启相关特性开关。
可是这样一来,简单地将这些变量初始化为类型零值,就不能满足要求了,所以 http 包在
init 函数中,就根据环境变量 GODEBUG 的值,对这些包级开关变量进行了复杂的初始
化,从而保证了这些开关变量在 http 包完成初始化后,可以处于合理状态。
3.在 init 函数中实现“注册模式”。
import (
"database/sql"
_ "github.com/lib/pq"
)
func main() {
db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=ver
if err != nil {
log.Fatal(err)
}
age := 21
rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
...
}
你可以看到示例代码是以空导入的方式导入 lib/pq 包的,main 函数中没有使用 pq 包的任何变量、函数或方法,这样就实现了对 PostgreSQL数据库的访问。而这一切的奥秘,全在 pq 包的 init 函数中:
func init() {
sql.Register("postgres", &Driver{})
}
这个奥秘就在,我们其实是利用了用空导入的方式导入 lib/pq 包时产生的一个“副作
用”,也就是 lib/pq 包作为 main 包的依赖包,它的 init 函数会在 pq 包初始化的时候得
以执行。
从上面代码中,我们可以看到在 pq 包的 init 函数中,pq 包将自己实现的 sql 驱动注册到
了 sql 包中。这样只要应用层代码在 Open 数据库的时候,传入驱动的名字(这里
是“postgres”),那么通过 sql.Open 函数,返回的数据库实例句柄对数据库进行的操
作,实际上调用的都是 pq 包中相应的驱动实现。
实际上,这种通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接
暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。
另外,从标准库 database/sql 包的角度来看,这种“注册模式”实质是一种工厂设计模式
的实现,sql.Open 函数就是这个模式中的工厂方法,它根据外部传入的驱动名称“生产”出不同类别的数据库实例句柄。
这种“注册模式”在标准库的其他包中也有广泛应用,比如说,使用标准库 image 包获取
各种格式图片的宽和高:

上面这个示例程序支持 png、jpeg、gif 三种格式的图片,而达成这一目标
的原因,正是 image/png、image/jpeg 和 image/gif 包都在各自的 init 函数中,将自
己“注册”到 image 的支持格式列表中了,你可以看看下面这个代码:
