main.main

main 包中的 main 函数 - 所有可执行程序用户层执行逻辑的入口函数

1
2
3
4
5
6
package main

// 无参数 + 无返回值
func main() {
// 用户层执行逻辑
}

可执行程序的 main 包必须定义 main 函数,否则会编译报错
function main is undeclared in the main package

main.go
1
package main
1
2
3
$ go build main.go
# command-line-arguments
runtime.main_main·f: function main is undeclared in the main package
  1. 在启动了多个 goroutine 的 Go 应用中,main.main 将在 Go 应用的主 goroutine 中执行
  2. main.main 函数返回意味着整个 Go 程序结束
  3. 除了 main 包外,其它包也可以拥有自己的 main 函数
    • 但依据 Go 的可见性规则,非 main 包中的 main 函数仅限于包内使用
1
2
3
4
5
6
7
8
9
10
11
12
package pkg1

import "fmt"

func Main() {
main()
}

// 仅在包内可见
func main() {
fmt.Println("main func for pkg1")
}

init

Go 包的初始化函数

1
2
3
4
// 无参数 + 无返回值
func init() {
// 包初始化逻辑
}
  1. 如果 main 包依赖的包中定义了 init 函数,或者 main 包自身定义了 init 函数
    • Go 程序在包初始化的时候,会先自动调用 init 函数,都发生在 main 函数执行前
  2. 不能显式调用 init 函数,否则会编译报错
main.go
1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

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

func main() {
init()
}
1
2
3
$ go build main.go
# command-line-arguments
./main.go:10:2: undefined: init
  1. Go 包可以拥有多个 init 函数,每个组成 Go 包的 Go 源文件中,也可以定义多个 init 函数
  2. 在初始化包时,Go 会按照一定的次序,串行调用这个包的 init 函数
    • 先传递给 Go 编译器的源文件中的 init 函数,会先被执行
    • 同一个源文件中的多个 init 函数,会按声明顺序依次执行

初始化顺序

  1. Go 包程序封装的基本单元
    • 每个 Go 包可以理解为是一个自治封装良好,对外部暴露有限接口的基本单元
  2. 一个 Go 程序由一组 Go 包组成,程序的初始化 = Go 包的初始化

DFS

go-pkg-init

1
2
3
4
5
6
7
8
9
10
$ tree go101
go101
├── go.mod
├── main.go
├── pkg1
│   └── pkg1.go
├── pkg2
│   └── pkg2.go
└── pkg3
└── pkg3.go
  1. main 包依赖 pkg1 包和 pkg2 包
  2. pkg1 包和 pkg2 包依赖 pkg3 包(只会被初始化 1 次)

空导入的方式可以触发包的初始化

main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"fmt"
_ "github.com/zhongmingmao/go101/pkg1"
_ "github.com/zhongmingmao/go101/pkg2"
)

var (
_ = constInitCheck()
v1 = variableInit("v1")
v2 = variableInit("v2")
)

const (
c1 = "c1"
c2 = "c2"
)

func constInitCheck() string {
if c1 != "" {
fmt.Println("main: const c1 has been initialized")
}
if c2 != "" {
fmt.Println("main: const c2 has been initialized")
}
return ""
}

func variableInit(name string) string {
fmt.Printf("main: var %s has been initialized\n", name)
return name
}

func init() {
fmt.Println("main: first init func invoked")
}

func init() {
fmt.Println("main: second init func invoked")
}

func main() {
fmt.Println("main: main func invoked")
}

一个被多个包依赖的包仅会初始化 1 次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ go build

$ ./go101
pkg3: const c1 has been initialized
pkg3: const c2 has been initialized
pkg3: var v1 has been initialized
pkg3: var v2 has been initialized
pkg3: first init func invoked
pkg3: second init func invoked
pkg1: const c1 has been initialized
pkg1: const c2 has been initialized
pkg1: var v1 has been initialized
pkg1: var v2 has been initialized
pkg1: first init func invoked
pkg1: second init func invoked
pkg2: const c1 has been initialized
pkg2: const c2 has been initialized
pkg2: var v1 has been initialized
pkg2: var v2 has been initialized
pkg2: first init func invoked
pkg2: second init func invoked
main: const c1 has been initialized
main: const c2 has been initialized
main: var v1 has been initialized
main: var v2 has been initialized
main: first init func invoked
main: second init func invoked
main: main func invoked

Go 包初始化顺序

  1. 依赖包之间按照深度优先的次序进行初始化
  2. 依赖包之内按照常量变量init 函数的顺序进行初始化
  3. 依赖包之内多个 init 函数按照声明次序进行自动调用

用途

重置包级变量值

负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查

flag 包定定义了一个 Exported 包级变量 CommandLine
flag 包初始化时,包级变量 CommandLine 在 init 函数之前就被初始化了

flag.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package flag

var CommandLine = NewFlagSet(os.Args[0], ExitOnError)

// CommandLine.Usage 被赋值了 defaultUsage
func NewFlagSet(name string, errorHandling ErrorHandling) *FlagSet {
f := &FlagSet{
name: name,
errorHandling: errorHandling,
}
f.Usage = f.defaultUsage
return f
}

func (f *FlagSet) defaultUsage() {
if f.name == "" {
fmt.Fprintf(f.Output(), "Usage:\n")
} else {
fmt.Fprintf(f.Output(), "Usage of %s:\n", f.name)
}
f.PrintDefaults()
}

flag 包的 init 函数提供了用户可以重置 CommandLine 的 Usage 字段的能力

1
2
3
4
5
6
7
8
9
10
11
12
func init() {
CommandLine.Usage = commandLineUsage
}

func commandLineUsage() {
Usage()
}

var Usage = func() {
fmt.Fprintf(CommandLine.Output(), "Usage of %s:\n", os.Args[0])
PrintDefaults()
}

包级变量的复杂初始化

  1. 有些包级变量需要一个比较复杂的初始化过程
  2. 包级变量的类型零值(每个 Go 类型都有具有一个零值定义)或者简单初始化表达式不能满足需求

http 包在 init 函数中,根据环境变量 GODEBUG,动态调整包级变量的初始化值

h2_bundle.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var (
http2VerboseLogs bool
http2logFrameWrites bool
http2logFrameReads bool
http2inTests bool
)

func init() {
e := os.Getenv("GODEBUG")
if strings.Contains(e, "http2debug=1") {
http2VerboseLogs = true
}
if strings.Contains(e, "http2debug=2") {
http2VerboseLogs = true
http2logFrameWrites = true
http2logFrameReads = true
}
}

注册模式

空导入的方式导入 github.com/lib/pq,main 函数中没有使用 pq 包的内容,但却可以访问 pq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"database/sql"
_ "github.com/lib/pq"
)

func main() {
db, err := sql.Open("postgres", "user=postgres password=postgres dbname=postgres sslmode=disable")
if err != nil {
return
}

_, err = db.Query("SELECT * FROM users")
if err != nil {
return
}
}

原因在于 pq 包的 init 函数中

conn.go
1
2
3
func init() {
sql.Register("postgres", &Driver{})
}
  1. 空导入会触发依赖包的初始化(会执行 init 函数)
  2. 通过在 init 函数中注册自身实现的模式,有效地降低了 Go 包对外的直接暴露
    • 尤其是包级变量的暴露,避免了外部通过包级变量对包状态的改动
  3. 从 database/sql 包来看,注册模式实际上是工厂模式的实现,sql.Open 对应为工厂方法