一 零值

不管是通过声明还是调用内置函数new来为变量分配存储时;或者通过复合字面量或make函数创建一个新值时,只要没有显式初始化,变量(或者值)就持有默认值。这种变量(或值)的每一个元素都会被设置为它们对应类型的零值。布尔类型是false,数字类型是0,字符串是"",指针、函数、接口、切片、信道、映射是nil。这种初始化是递归完成的,例如,如果没有指定值,则结构数组的每个元素都将其字段置零。

以下声明是等价的:

var i int
var i int = 0

声明以下代码之后:

type T struct { i int; f float64; next *T }
t := new(T)

以下表达式成立:

t.i == 0
t.f == 0.0
t.next == nil

通过以下声明同样也是成立的:

var t T

二 程序的初始化

2.1 包的初始化

在一个包中,包级的变量按声明顺序初始化,但在此之前会首先初始化它们依赖的任何变量。

更准确地说,如果包级变量尚未初始化且没有初始化表达式,或有初始化表达式但其并不依赖其他未初始化的变量,则认为它(包级变量)已准备好进行初始化。初始化进程通过重复这种操作来初始化声明顺序中较早声明的下一个包级变量,直到没有待初始化的变量为止,完成整个初始化过程。

如果初始化过程结束,仍有变量未初始化,且这些变量是一个或多个初始化周期的一部分,则程序无效。

多个源文件中变量的声明顺序由源文件呈现给编译器的顺序决定:第一个源文件中声明的变量在第二个源文件中声明的任何变量之前声明,依此类推。

依赖分析不依赖于变量的实际值,只依赖于源码中对它们的词法引用,且依赖具有传递性。例如,变量x的初始化表达式引用了一个函数,该函数的函数体又引用了变量y,则x依赖于y。特别地:

  • 对变量或函数的引用即表示该变量或函数的标识符。
  • 对方法m的引用是t.m形式的一个方法值或方法表达式,其中t的(静态)类型不是接口类型,且方法mt的方法集中。结果函数值t.m是否被调用并不重要。
  • 对于变量,函数或者方法x,如果x的初始化表达式(对函数和方法来说可能是函数体或方法体)包含对变量y的引用,或包含依赖于y的函数或方法,则x依赖于y

每个包都执行依赖分析,且只考虑引用当前包中声明的变量,函数和方法的引用。例如,给定以下声明:

var (
    a = c + b
    b = f()
    c = f()
    d = 3
)

func f() int {
    d++
    return d
}

初始化顺序为d, b, c, a

2.2 init函数

变量也可以使用包块中声明的init函数进行初始化,该函数不带入参和结果参数:

func init() { … }

即使包中只有单个源文件,每个包也可以定义多个这样的函数。在包块中,init标识符只能用于声明init函数,但标识符本身未声明。因此,无法在程序的其他位置引用init函数

一个没有导入其他依赖的包的初始化过程如下:
先初始化所有包级变量,再按顺序调用当前包中所有init函数,这些待初始化的项可能分布在多个源文件中,按源文件呈现给编译器的顺序进行。

如果包含其他导入的包,则先初始化导入的包;如果多个包导入了同一个包,则这个导入的包只会初始化一次。通过构造包的导入可以保证不存在循环初始化依赖。

即:
当前包中所有导入包先初始化,接着当前包中所有声明的变量进行初始化,最后当前包中所有的init函数初始化。

包的初始化,变量的初始化和init函数的调用,在单个goroutine中按顺序进行,一次一个包。init函数还可以启动其他的goroutine,与初始化代码并发执行。但是,初始化始终会对init函数按顺序调用:在前一个init函数返回后,才会调用下一个。

初始化顺序示例:

package main

import "fmt"

var a int = f()

func main() {
    fmt.Println("调用main()") // 4
}

func f() int {
    fmt.Println("调用f()") // 1
    return 2
}

func init() {
    fmt.Println("调用1 init()") // 2
}

func init() {
    fmt.Println("调用2 init()") // 3
}

将打印出:

调用f()
调用1 init()
调用2 init()
调用main()

除了不能表示为声明的初始化之外,init函数的常见用途是在实际执行开始之前验证或修复程序状态的正确性。例如:

func init() {
    if user == "" {
        log.Fatal("$USER not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath 可能会被命令行中的 --gopath 标识覆盖
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

为了确保可重现初始化行为,建议构建系统将同属一个包的多个文件以词法文件名顺序呈现给编译器。

三 程序的执行

一个完整的程序是通过将一个未导入的main package包和其导入的所有包连在一起创建的。主包必须含有一个名为main的包和一个名为main的无参无返回值函数:

func main() { … }

程序执行从初始化main包开始,然后调用main函数,当该函数调用返回时,程序退出。它不会等待其他非主goroutine完成。

参考:
https://golang.org/ref/spec#Program_initialization_and_execution

文章目录