包
go也使用包来管理代码,在使用一个包中的可导出标识符时(对于包外而言,只有可导出标识符是可见的),需要先引入包。
导出标识符和非导出标识符
一个由Unicode大写字母开头的标识符称为导出标识符。 这里导出可以被理解为公开(public)。 其它(即非Unicode大写字母开头的)标识符称为非导出标识符。 非导出可以被理解为私有(private)。 截至目前(Go 1.18),东方字符都被视为非导出字符。 非导出有时候也被称为未导出。例如:
代码语言:javascript复制package main
import "fmt"
func main() {
fmt.Println(123) // 注意Println函数的第一个字面是大写的。
}
使用包的导出标识符时,前面需要加上包名做限定。下面再来看一个例子:
代码语言:javascript复制package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(123)
fmt.Println(rand.Uint32())
}
rand是math标准库包中的一个子包,用来产生伪随机数。如果我们希望上面的程序每次运行的时候输出一个不同的随机数,我们需要在程序启动的时候使用调用rand.Seed函数来设置一个不同的随机数种子。
Go不支持循环引用(依赖)。 如果一个代码包a依赖于代码包b,同时代码包b依赖于代码包c,则代码包c中的源文件不能引入代码包a和代码包b,代码包b中的源文件也不能引入代码包a。
和包依赖类似,一个模块也可能依赖于一些其它模块。 此模块的直接依赖模块和这些依赖模块的版本在此模块中的go.mod文件中指定。 模块循环依赖是允许的。
我们称一个程序中含有main入口函数的名称为main的代码包为程序代码包(或者命令代码包),称其它代码包为库代码包。 程序代码包不能被其它代码包引入。一个程序只能有一个程序代码包。
代码包目录的名称并不要求一定要和其对应的代码包的名称相同。 但是,库代码包目录的名称最好设为和其对应的代码包的名称相同。 因为一个代码包的引入路径中包含的是此包的目录名,但是此包的默认引入名为此包的名称。 如果两者不一致,会使人感到困惑。
另一方面,最好给每个程序代码包目录指定一个有意义的名字,而不是它的包名main。
init函数
在一个代码包中,甚至一个源文件中,可以声明若干名为init的函数。 这些init函数必须不带任何输入参数和返回结果。
注意:我们不能声明名为init的包级变量、常量或者类型。
在程序运行时刻,在进入main入口函数之前,每个init函数在此包加载的时候将被(串行)执行并且只执行一遍。
一个程序中所涉及到的所有的在运行时刻要用到的代码包的加载是串行执行的。 在一个程序启动时,每个包中总是在它所有依赖的包都加载完成之后才开始加载。 程序代码包总是最后一个被加载的代码包。每个被用到的包会被而且仅会被加载一次。
在加载一个代码包的过程中,所有的声明在此包中的init函数将被串行调用并且仅调用执行一次。 一个代码包中声明的init函数的调用肯定晚于此代码包所依赖的代码包中声明的init函数。 所有的init函数都将在调用main入口函数之前被调用执行。
在同一个源文件中声明的init函数将按从上到下的顺序被调用执行。 对于声明在同一个包中的两个不同源文件中的两个init函数,Go语言白皮书推荐(但不强求)按照它们所处于的源文件的名称的词典序列(对英文来说,即字母顺序)来调用。 所以最好不要让声明在同一个包中的两个不同源文件中的两个init函数存在依赖关系。
在加载一个代码包的时候,此代码包中声明的所有包级变量都将在此包中的任何一个init函数执行之前初始化完毕。
在同一个包内,包级变量将尽量按照它们在代码中的出现顺序被初始化,但是一个包级变量的初始化肯定晚于它所依赖的其它包级变量。
完整的引入声明语句形式
代码语言:javascript复制import importname "path/to/package"
其中引入名importname是可选的,它的默认值为被引入的包的包名(不是目录名)。 引入声明语句的完整形式在日常编程中使用的频率不是很高。 但是在某些情况下,完整形式必须被使用。 比如,如果一个源文件引入的两个代码包的包名一样,为了防止使编译器产生困惑,我们至少需要用完整形式为其中一个包指定一个不同的引入名以区分这两个包。 如果一个包引入声明中的importname没有省略,则限定标识符使用的前缀必须为importname,而不是被引入的包的名称。例如:
代码语言:javascript复制package main
import (
format "fmt"
random "math/rand"
"time"
)
func main() {
random.Seed(time.Now().UnixNano())
format.Print("一个随机数:", random.Uint32(), "n")
// 下面这两行编译不通过,因为rand不可识别。
/*
rand.Seed(time.Now().UnixNano())
fmt.Print("一个随机数:", rand.Uint32(), "n")
*/
}
一个完整引入声明语句形式的引入名importname可以是一个句点(.)。 这样的引入称为句点引入。使用被句点引入的包中的导出代码要素时,限定标识符的前缀必须省略。例如:
代码语言:javascript复制package main
import (
. "fmt"
. "time"
)
func main() {
Println("Current time:", Now())
}
在上面这个例子中,Println和Now函数调用不需要带任何前缀。
一个完整引入声明语句形式的引入名importname可以是一个空标识符(_)。 这样的引入称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的代码要素得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。
除了匿名引入,其它引入必须在代码中被使用一次。
参考资料
https://gfw.go101.org/article/packages-and-imports.html