Go语言工程结构详述 图片看不了?点击切换HTTP 返回上层
更重要的是,Go语言让开发人员很容易通过 go get 命令从各种公共代码库(比如 GitHub)中下载开源代码并使用。这除了得益于 Go语言自带命令的强大之外,还应该归功于 Go 工程结构的严谨和完善。本节中,我们详述 Go语言的工程结构。
工作区
一般情况下,Go语言的源码文件必须放在工作区中。但是对于命令源码文件来说,这不是必需的。工作区其实就是一个对应于特定工程的目录,它应包含 3 个子目录:src 目录、pkg 目录和 bin 目录,下面逐一说明。src 目录
用于以代码包的形式组织并保存 Go 源码文件,这里的代码包与 src 下的子目录一一对应。例如,若一个源码文件被声明属于代码包 log,那么它就应当保存在 src/log 目录中。当然,也可以把 Go 源码文件直接放在 src 目录下,但这样的 Go 源码文件就只能被声明属于 main 代码包了。除非用于临时测试或演示,一般还是建议把 Go 源码文件放入特定的代码包中。
pkg 目录
用于存放通过 go install 命令安装后的代码包的归档文件。前提是代码包中必须包含 Go 库源码文件。归档文件是指那些名称以“.a”结尾的文件。该目录与 GOROOT 目录下的 pkg 目录功能类似。区别在于,工作区中的 pkg 目录专门用来存放用户代码的归档文件。编译和安装用户代码的过程一般会以代码包为单位进行。比如 log 包被编译安装后,将生成一个名为 log.a 的归档文件,并存放在当前工作区的 pkg 目录下的平台相关目录中。
bin 目录
与 pkg 目录类似,在通过 go install 命令完成安装后,保存由 Go 命令源码文件生成的可执行文件。在类 Unix 操作系统下,这个可执行文件一般来说名称与源码文件的主文件名相同。而在 Windows 操作系统下,这个可执行文件的名称则是源码文件主文件名加 .exe 后缀。注意:这里有必要明确一下 Go语言的命令源码文件和库源码文件的区别。所谓命令源码文件,就是声明属于 main 代码包并且包含无参数声明和结果声明的 main 函数的源码文件。这类源码文件是程序的入口,它们可以独立运行(使用 go run 命令),也可以通过 go build 或 go install 命令得到相应的可执行文件。所谓库源码文件,则是指存在于某个代码包中的普通源码文件。
GOPATH
我们需要将工作区的目录路径添加到环境变量 GOPATH 中。否则,即使处于同一工作区,代码之间也无法通过绝对代码包路径调用。在实际开发环境中,工作区可以只有一个,也可以有多个,这些工作区的目录路径都需要添加到 GOPATH 中。与 GOROOT 一样,我们应该确保 GOPATH 一直有效。需要注意一下两点:
- GOPATH 中不要包含 Go语言的根目录,以便将 Go语言本身的工作区同用户工作区严格分开。
- 通过 Go 工具中的代码获取命令 go get,可将指定项目的源码下载到我们在 GOPATH 中设定的第一个工作区中,并在其中完成编译和安装。
源码文件
Go 的源码文件有 3 个种类,即命令源码文件、库源码文件和测试源码文件,下面详细说明这 3 类源码文件。1) 命令源码文件
如果一个源码文件被声明属于 main 代码包,且该文件代码中包含无参数声明和结果声明的 main 函数,则它就是命令源码文件。命令源码文件可通过 go run 命令直接启动运行。同一个代码包中的所有源码文件,其所属代码包的名称必须一致。如果命令源码文件和库源码文件处于同一个代码包中,那么在该包中就无法正确执行 go build 和 go install 命令。换句话说,这些源码文件将无法通过常规方法编译和安装。
因此,命令源码文件通常会单独放在一个代码包中。这是合理且必要的,因为通常一个程序模块或软件的启动入口只有一个。
同一个代码包中可以有多个命令源码文件,可通过 go run 命令分别运行,但这会使 go build 和 go install 命令无法编译和安装该代码包。所以,我们也不应该把多个命令源码文件放在同一个代码包中。
当代码包中有且仅有一个命令源码文件时,在文件所在目录中执行 go build 命令,即可在该目录下生成一个与目录同名的可执行文件;而若使用 go install 则可在当前工作区的 bin 目录下生成相应的可执行文件。例如,代码包 gopcp.v2/helper/ds 中只有一个源码文件 showds.go,且它是命令源码文件,则相关操作和结果如下:
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ Is
showds.go
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ go build
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/ds$ Is
ds showds.go
hc@ubt: ~/golang/example.v2/src/gopcp.v2/helper/ds$ go install
hc@ubt:~/golang/example.v2/src/gopcp・v2/helper/ds$ Is ../../../../bin
ds
2) 库源码文件
通常,库源码文件声明的包名会与它直接所属的代码包(目录)名一致,且库源码文件中不包含无参数声明和无结果声明的 main 函数。下面来安装(其中包含编译)gopcp.v2/helper/log 包,其中含有若干库源码文件:
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls
base logger.go logger_test.go logrus
hc@ubt: ~/golang/example.v2/src/gopcp.v2/helper/log$ go install
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls ../../../pkg
linux_amd64
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls ../../../pkg/linux_amd64/gopcp.v2/helper
log log.a
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ ls ../../../../pkg/linux_amd64/gopcp.v2/helper/log
base.a field.a logrus.a
安装库源码文件时所生成的归档文件会被存放到当前工作区的 pkg 目录中。example.v2 项目的 gopcp.v2/helper/log 包所属工作区的根目录是 ~/golang/example.v2。因此,上面所说的 pkg 目录即 ~/golang/example.v2/pkg。
根据被编译时的目标计算环境,归档文件会被放在该 pkg 目录下的平台相关目录中。例如,我是在 64 位的 Linux 计算环境下安装的,对应的平台相关目录就是 linux_amd64,那么归档文件一定会被存放到 ~/golang/example.v2/pkg/linux_amd64 目录中的某个地方。
存放归档文件的目录的相对路径与被安装的代码包的上一级代码包的相对路径一致。第一个相对路径是相对于工作区的 pkg 目录下的平台相关目录而言的,而第二个相对路径是相对于工作区的 src 目录而言的。如果被安装的代码包没有上一级代码包(也就是说,它的父目录就是工作区的 src 目录),那么它的归档文件就会被直接存放到当前工作区的 pkg 目录的平台相关目录下。
例如,gopcp.v2/helper/log 包的归档文件 log.a 一定会被存放到 ~/golang/example.v2/pkg/linux_amd64/gopcp.v2/helper 这个目录下。而它的子代码包 gopcp.v2/helper/log/base 的归档文件 base.a,则一定会被存放到 ~/golang/example.v2/pkg/linux_amd64/gopcp.v2/helper/log 目录下。
3) 测试源码文件
测试源码文件是一种特殊的库文件,可以通过执行 go test 命令运行当前代码包下的所有测试源码文件。成为测试源码文件的充分条件有两个,如下。- 文件名需要以"_test.go"结尾。
- 文件中需要至少包含一个名称以 Test 开头或 Benchmark 开头,且拥有一个类型为 *testing.T 或 *testing.B 的参数的函数。testing.T 和 testing.B 是两个结构体类型。而 *testing.T 和 *testing.B 则分别为前两者的指针类型。它们分别是功能测试和基准测试所需的。
当在一个代码包中执行 go test 命令时,该代码包中的所有测试源码文件会被找到并运行。我们依然以 gopcp.v2/helper/log 包为例:
hc@ubt:~/golang/example.v2/src/gopcp.v2/helper/log$ go test
PASS
ok gopcp.v2/helper/log 0.008s
最后插一句,Go 代码的文本文件需要以 UTF-8 编码存储。如果源码文件中出现了非 UTF-8 编码的字符,那么在运行、编译或安装的时候,Go 命令会抛出 illegal UTF-8 sequence 错误。
代码包
在 Go 中,代码包是代码编译和安装的基本单元,也是非常直观的代码组织形式。1) 包声明
细心的读者可能已经发现,在 example.v2 项目的代码包中,多数源码文件名称看似都与包名没什么关系。实际上,在 Go语言中,代码包中的源码文件可以任意命名。另外,这些任意名称的源码文件都必须以包声明语句作为文件中代码的第一行。比如,gopcp.v2/helper/log/base 包中的所有源码文件都要先声明自己属于某一个代码包:package "base"
其中 package 是 Go 中用于包声明语句的关键字。Go 规定包声明中的包名是代码包路径的最后一个元素。比如,gopcp.v2/helper/log/base 包的源码文件包声明中的包名是 base。但是,不论命令源码文件存放在哪个代码包中,它都必须声明属于 main 包。2) 包导入
代码包 gopcp.v2/helper/log 中的 logger.go 需要依赖 base 子包和 logrus 子包,因此需要在源码文件中使用代码包导入语句,如下所示:
import "gopcp.v2/helper/log/base"
import "gopcp.v2/helper/log/logrus"
当导入多个代码包时,可以用圆括号括起它们,且每个代码包名独占一行。在使用被导入代码包中公开的程序实体时,需要使用包路径的最后一个元素加的方式指定代码所在的包。
因此,上述语句可以写成:
import (
"gopcp.v2/helper/log/base"
"gopcp.v2/helper/log/logrus"
)
import (
"github.com/Sirupsen/logrus"
mylogrus "gopcp.v2/helper/log/logrus"
)
import (
. "gopcp.v2/helper/log/logrus"
)
var logger = NewLogger("gopcp") // NewLogger 是 gopcp.v2/helper/log/logrus 包中的函数
这里强调一下,Go 中的变量、常量、函数和类型声明可统称为程序实体,而它们的名称统称为标识符。标识符可以是 Unicode 字符集中任意能表示自然语言文字的字符、数字以及下划线 (_)。标识符不能以数字或下划线开头。实际上,标识符的首字符的大小写控制着对应程序实体的访问权限。如果标识符的首字符是大写形式,那么它所对应的程序实体就可以被本代码包之外的代码访问到,也称为可导出的或公开的;否则,对应的程序实体就只能被本包内的代码访问,也称为不可导岀的或包级私有的。要想成为可导出的程序实体,还需要额外满足以下两个条件。
- 程序实体必须是非局部的。局部的程序实体是指:它被定义在了函数或结构体的内部。
- 代码包所属目录必须包含在 GOPATH 中定义的工作区目录中。
代码包导入还有另外一种情况:如果只想初始化某个代码包,而不需要在当前源码文件中使用那个代码包中的任何程序实体,就可以用“_”来代替别名:
import (
_ "github.com/Simpsen/logrus"
)
3) 包初始化
在 Go语言中,可以有专门的函数负责代码包初始化,称为代码包初始化函数。这个函数需要无任何参数声明和结果声明,且名称必须为 init,如下所示:
func init() {
fmt.Println("Initialize...")
}
下面的代码展示了全局赋值语句、代码包初始化函数以及主函数的执行顺序。其中,双斜杠及其右边的内容为代码注释,Go 编译器在编译的时候会将其忽略。
package main //命令源码文件必须在这里声明自己属于main包 import ( //导入标准库代码包fmt和runtime "fmt" "runtime" ) func init() { //代码包初始化函数 fmt.Printf("Map: %v\n", m) // 格式化的打印 //通过调用runtime包的代码获取当前机器的操作系统和计算架构. //而后通过fmt包的Sprintf方法进行格式化字符串生成并赋值给变量info info = fmt.Sprintf("OS: %s, Arch: %s", runtime.GOOS, runtime.GOARCH) } //非局部变量,map类型,且已初始化 var m = map[int]string{l: "A", 2: "B", 3: "C"} //非局部变量,string类型,未被初始化 var info string func main() { //命令源码文件必须有的入口函数,也称主函数 fmt.Println(info) // 打印变量 info }运行这个文件:
hc@ubt:~/golang/example.v2/src/gopcp.v2/chapter1/pkginit$ go run pkg_init.go
Map: map[l:A 2:B 3:C]
OS: linux, Arch: amd64
第一行是对变量 m 的值格式化后的结果。可以看到,在函数 init 的第一条语句执行时,变量 m 已经被初始化并赋值了。这验证了:当前代码包中所有全局变量的初始化会在代码包初始化函数执行前完成。
第二行是对变量 info 的值格式化后的结果。变量 info 被定义时并没有显式赋值,因此它被赋予类型 string 的零值——""(空字符串)。之后,变量 info 在代码包初始化函数 init 中被赋值,并在入口函数 main 中输出。这验证了:所有的代码包初始化函数都会在 main 函数执行前执行完毕。
同一个代码包中可以存在多个代码包初始化函数,甚至代码包内的每一个源码文件都可以定义多个代码包初始化函数。Go 不会保证同一个代码包中多个代码包初始化函数的执行顺序。此外,被导入的代码包的初始化函数总是会先执行。在上例中,fmt 和 runtime 包中的 init 函数(如果有的话)会先执行,然后当前文件中的 init 函数才会执行。