2.2 包
在很多编程语言中,为了封装和隔离代码,同时也为了代码复用,都有包或者命名空间的语法要素,例如C#语言中的namespace以及Java语言中的package。Go语言中包的作用和其他语言中的库或者模块作用类似。
在Go语言里,包是一个非常重要的概念。它的设计理念是使用包来封装和隔离不同的功能。这样能够更好地复用代码,并对每个包内数据和方法的使用有更好的控制。
当包进行命名时,建议按如下规范进行命名:
· 包名应该全部小写(大写也不会报错),不建议包含大写或下划线来命名。
· 包名应该简短且简洁,且能代表该包的主要功能。
· 包名不用复数形式,例如net/url,而不是net/urls。
· 包名尽量不要和标准库中的包名重名,防止导入包的时候需要重命名。
Go语言中包的命名规范中有关于包名全部小写的规定,可以更快地进行输入,而不用进行大小写切换。包是Go程序的基本单位,在Go语言中的声明语法如下:
package 包名
包名告诉Go编译器,当前文件属于哪个包。一般来说,Go语言包的源代码存放在一个根目录中,其中包含一个或者多个.go文件。这些.go文件按照目录进行分组并构建出上下级的层级结构。每组.go文件被称为包。Go语言的每一个.go文件的package声明前面需要用一段文字对该文件的作用进行描述。
所有的.go文件除了包的注释和空行外,第一行都应该对包进行声明。每个包都在一个单独的目录中,但不能将多个包放在同一个目录中,也不能将同一个包中的文件分散到不同的目录中。换句话说,同一个目录中的所有.go文件必须属于同一个包名,否则报错,如图2.1所示。
图2.1 同一目录下包含2个不同的包名
如果执行go build .命令,则会抛出无法加载包的错误(can't load package),并提示在不同的文件中找到多个包名,即在main.go文件中找到main包,而在test.go文件中找到demo2包。
如果当前编写的Go程序要作为一个可执行的程序,那么必须包含一个main包(main包具有特殊意义,会作为可执行程序的入口)和一个main函数。声明main包的文件在编译时会使用所在目录的目录名作为生成文件的文件名。
注意
go build或者go install命令生成的文件,对于可执行文件,在Windows系统上是文件夹名.exe;对于UNIX、Linux或者Mac OS X系统,则为文件夹名;对于非可执行文件,可生成库文件,为文件夹名.a。
为了更加直观地理解Go语言包的编译过程,我们给出一个不包含main包和main函数的文件,如示例程序2-1所示。
示例程序2-1 Go库文件的用法:chapter02\code01\code01.go
在示例程序2-1中,注意该文件的文件名为test.go、目录名为code01,包名和目录名一致(为code01)。Go语言并不关心文件名,而是关心目录名和包名。第05行的func关键词声明了一个Hello函数,注意这个函数的首字母是大写的,这样对包外是可见的,可以直接调用。
打开“命令提示符”窗口(在目录go.introduce/chapter02/code01中),执行如下命令:
go install .\code01.go
或者
go install go.introduce/chapter02/code01
如果正确执行,就会在%GOPATH%\pkg\windows_amd64\go.introduce\chapter02目录(注意,具体目录会根据不同的环境有所差异)中生成一个code01.a文件,如图2.2所示。
图2.2 code01库文件生成
注意
go install命令可能需要配置GOBIN环境变量,如C:\GoWork\bin,否则报错。
2.2.1 包的导入
Go语言中的包和实际的代码目录结构一致。Go中的包在外部使用时,首先需要导入。导入包时用的是包的导入路径。Go语言的包名和包的导入路径是不一样的。
在Go程序中,每一个包通过唯一的字符串(例如go.introduce/chapter02/code01)作为标识符,这个标识符就是包的导入路径。一个包的导入路径本质是一个目录,该目录中包含了构成包的一个或者多个Go源代码文件。
按照约定,包名匹配导入路径的最后一个目录名。例如,包的导入路径为go.introduce/chapter02/code01,包名就为code01。导入的包名需要使用双引号括起来。包名的路径是相对路径,是从%GOPATH%/src/后开始计算的,使用符号“/”作为路径中的分隔符。
注意
Go程序中如果导入了一个后续没有使用的包,那么编译会报错。另外,如果觉得导入的包名太长,那么可以给导入的包名另起一个短名字,从而便于书写。
对于Go语言中的包,使用关键字import导入,其语法为:
import 包路径
(1)导入多个包时,如果发现包名重名了,那么需要对导入的包进行重命名,其语法为:
import 别名 包路径
(2)导入多个包时,可以用括号一次性导入多个包,其语法如下:
import ( 包1路径 包2路径 ... )
(3)有时候会采用如下方式导入包,别名是点(.)或者下划线(_):
import ( . 包路径 _ 包路径 )
这个点(.)符号的含义是,在调用包的函数时,可以省略包名;下划线(_)只用于在导入包时执行初始化操作,它并不需要使用包内的其他函数、常量等资源,而是调用了该包里面的init函数。
上面包导入的几种模式分别代表正常模式、别名模式和简便模式。Go编译器会根据import语句中包的路径,结合Go配置的环境变量GOROOT和GOPATH来查找物理磁盘上的包。例如,用import "web/api"导入一个包,编译器就会按照如下顺序进行搜索:
%GOROOT%/src/web/api %GOPATH%/src/web/api
下面给出一个导入包的示例程序2-2。
示例程序2-2 Go导入包文件:chapter02\code02\code02.go
第01行声明了一个main包,同时第06行还有一个main函数,表明编译器会将它编译为一个可执行文件。第04行导入了示例程序2-1创建的包文件。由于在Go语言中包的导入路径末尾的目录名一般和包名一致(也可以不一致),因此可以推断包名为code01。第08行调用包code01对外导出的函数Hello。
打开“命令提示符”窗口(在目录go.introduce/chapter02/code02中),执行如下命令:
go run .\code02.go
输出结果如图2.3所示,可见程序可以成功地调用code01包中的函数Hello。
图2.3 code02.go运行结果
在Windows系统下,如果调用go install,就会在%GOPATH%/bin下生成code02.exe文件,如图2.4所示。
图2.4 code02.exe生成结果
前面提到,一般Go语言中的包名和目录名一致。其实,也可以不一致。包名和目录名不一致时,需要注意:目录名使用在文件层面,例如库的安装路径名、库文件名以及被导入时的路径;包名使用在代码层面,例如调用包的函数时。
为了更好地理解上面的这段话,参考下面包名和目录名不一致的示例程序2-3。
示例程序2-3 Go包名和目录名不一致的用法示例:chapter02\code03\code03.go
可以看出,code03.go文件位于code03目录中,但是其package名称为demo,因此二者不一致。在这种情况下,go install生成的文件名为code03.a,但是包名仍然为demo,当我们在外部进行调用时需要用demo作为前缀。为了验证,下面给出示例程序2-4。
示例程序2-4 Go调用demo包的用法示例:chapter02\code04\code04.go
第04行导入了示例程序2-3创建的包,由于包名为demo、目录名为code03,因此在第08行调用Hello函数时用的是包名作为前缀的调用方式demo.Hello,而不是目录名作为前缀的调用方式code03.Hello。
2.2.2 包的嵌套
包往往和源代码目录对应,目录具有上下级关系,包也不例外。在Go语言中,包可以互相嵌套。因为,对于Go语言来说,包本质上对应一个目录,包的嵌套就类似于在目录中创建子目录。用Visual Studio Code打开目录go.introduce\chapter02\code05,并创建如图2.5所示的目录结构。
图2.5 code05目录结构
其中,code05.go定义了main包,通过import "go.introduce/chapter02/code05/app"导入了app包(app.go),而app包中通过import "go.introduce/chapter02/code05/app/config"导入了config包(config.go)。从目录上看,config包定义在app包内部,说明一个包可以定义在另一个包的内部,实现包的嵌套。
正是由于Go语言包可以实现嵌套,即使包名在不同的路径中是同名的,也可以通过包的路径来区分不同包。config.go脚本如示例程序2-5所示。
示例程序2-5 config.go脚本:chapter02\code05\app\config\config.go
第03行用关键词var声明了一个变量Cver。注意,这里的变量名首字母是大写的,表示该变量在外部是可见的。第02行是在Cver变量声明语句上一条语句,通过双斜线“//”的注释方式对变量Cver进行说明(关于变量和注释会在本章后续章节进行详细说明,这里只要了解即可)。
注意
Go的代码规范检测可能会用到go-lint工具,它要求对外导出的变量名写注释,且注释的格式为变量名+空格+变量注释说明。在示例程序2-5中,第02行注释中的Cver后跟着一个空格。
变量Cver是包级别的变量,当外部导入该包时,可以直接用Cver存取该变量。app.go文件内容如示例程序2-6所示。
示例程序2-6 app.go文件内容:chapter02\code05\app\app.go
在示例程序2-6中,第03行通过import导入了config包,由于Go语言中的包采用相对路径(相对于%GOPATH%/src/),因此config包路径为go.introduce/chapter02/code05/app/config,而不是app/config。第06行用关键词func声明了一个GetVer函数,返回string类型的数据。第07行直接返回config包中Cver变量的值。
2.2.3 特殊的init函数
Go包中有一种特殊的init函数,是Go编译器自动可识别的,用于一些初始化工作。函数init和main在定义时不能有任何参数和返回值。该函数只能由Go程序自动调用,不可以被外部引用。
注意
init函数可以在任意包中定义,并且可以重复定义多个。main函数只能用于main包中,且只能定义一个。如果同一个.go文件中定义多个init函数,那么调用的顺序为从上到下依次执行。对于同一个包中的不同文件,会按照.go文件名从小到大的顺序调用各文件中的init函数。例如,某个目录中包含a.go和b.go,那么会先执行a.go中的init函数,再执行b.go中的init函数。
对于不同的包而言,如果不相互依赖,就会按照main包中import的顺序调用其包中的init函数。如果包之间存在依赖关系,调用顺序按照导入包顺序的反序进行初始化。因此,Go语言包中的init调用顺序示意图如图2.6所示。
图2.6 init调用顺序示意图
从图2.6可以看出,包导入的顺序为main → app → config,所以初始化init函数的顺序为config→ app → main。为了验证Go语言是否按照上面的规则执行init函数,我们在Visual Studio Code中构建如图2.7所示的目录结构。
图2.7 验证init调用顺序的code06目录结构
为了更加方便地查看执行init函数的顺序,下面将这些.go文件组合到一块,如示例程序2-7所示。
示例程序2-7 验证init函数的调用顺序:chapter02\code06\main.go
示例程序2-7中是将4个.go文件合并到一起的,注意第05行和第17行导入包的时候用了空白标识符,这样就可以调用包中的init()函数。在目录go.introduce\chapter02\code06中打开“命令提示符”窗口,执行命令go run .\main.go,则会出现如图2.8所示的输出结果。
图2.8 init()函数调用顺序的运行结果
注意
Go项目中不允许出现循环导入包,即使一个包被其他多个包导入,也只会初始化一次。