今天的内容看起来简单,其实深入了解的话还是蛮难的!不过,我们也是GO的初学者的一个身份,我们先从简单的理解开始吧。
包
什么是包?Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。
import "fmt"
上面的代码中,fmt
就是一个包,是通过关键词import导入的。那么,我们怎么制作自己的包呢?一个包的源代码保存在 一个或多个
以.go为文件后缀名的源文件中,通常一个包所在 目录路径
的后缀是包的导入路径。每个源文件都是以包的 声明语句
开始,用来指明 包的名字
。如果你写了一个自己的包,使用 相对路径
来引用,那么这个包一般放在GO的工作目录下的 src
文件夹下,才可以被引入使用。
每个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不同的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是可以导出的。
在每个源文件的包声明前紧跟着的注释是包注释。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的 doc.go
文件中。
导入包
在Go语言程序中,每个包都有一个全局唯一的导入路径。Go语言的规范并没有定义这些字符串的具体含义或包来自哪里,它们是由 构建工具
来解释的。当使用Go语言自带的go工具箱时,一个导入路径代表一个目录中的一个或多个Go源文件。
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定。 按照惯例
,一个包的名字和包的导入路径的最后一个字段相同。
如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理。
包的初始化
包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:
var a = b + c // a 第三个初始化, 为 3
var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1 // c 第一个初始化, 为 1
func f() int { return c + 1 }
如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的 init初始化函数
来简化初始化工作。每个文件都 可以包含多个init初始化函数
func init() { /* ... */ }
这样的init初始化函数除了 不能被调用或引用外
,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化
。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
作用域
作用域我们可以套用PHP的作用域概念来理解,简单概括就是 函数内部声明的变量不能被外部使用,内部声明屏蔽了外部同名的声明
。
不要将作用域和生命周期混为一谈。声明语句的 作用域
对应的是 一个源代码
的文本区域;它是一个编译时的 属性
。一个变量的生命周期是指程序运行时变量存在的 有效时间段
,在此时间区域内它可以被程序的其他部分引用;是一个 运行时的概念
。
声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true等是在 全局作用域
的,因此可以在整个程序中直接使用。任何在在函数外部(也就是包级语法域)声明的名字可以在 同一个包
的任何源文件中访问的。
一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。例如,你可以声明一个局部变量,和包级的变量同名。但是物极必反,如果滥用不同词法域可重名的特性的话,可能导致程序很难阅读。
当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找
。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问
。
隐式和显示
下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的:
func main() {
x := "hello"
for _, x := range x { // 隐式
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
在这个程序中:
if f, err := os.Open(fname); err != nil {
return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f
变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。你可能会收到一个局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。
通常需要在if之前声明变量,这样可以确保后面的语句依然可以访问变量:
f, err := os.Open(fname)
if err != nil {
return err
}
f.ReadByte()
f.Close()
短变量语句作用域
要特别注意短变量声明语句的作用域范围,考虑下面的程序,它的目的是获取当前的工作目录然后保存到一个包级的变量中。这可以本来通过直接调用os.Getwd完成,但是将这个从主逻辑中分离出来可能会更好,特别是在需要处理错误的时候。函数log.Fatalf用于打印日志信息,然后调用os.Exit(1)终止程序。
var cwd string
func init() {
cwd, err := os.Getwd() // compile error: unused: cwd
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
虽然cwd在外部已经声明过,但是:=语句还是将cwd和err 重新声明
为新的局部变量。因为内部声明的cwd将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的cwd变量。
由于当前的编译器会检测到局部声明的cwd并没有本使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部cwd的打印语句,就可能导致这种检测失效。
有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明err变量,来避免使用:=的简短声明方式:
var cwd string
func init() {
var err error
cwd, err = os.Getwd()
if err != nil {
log.Fatalf("os.Getwd failed: %v", err)
}
}
参考
《GO语言圣经》
有疑问加站长微信联系(非本文作者)