首先,我们从GitHub下载Go语言的源代码:
git clone http://github.com/golang/go.git
使用tree命令查看:
cd go/src // 进入Go语言项目src目录
tree . // 查看src目录下的结构布局
然后会出现如下目录:
./src
├── all.bash*
├── clean.bash*
├── cmd/
├── make.bash*
├── Make.dist
├── pkg/
├── race.bash*
├── run.bash*
... ...
└── sudo.bash*
上面的结果来看,src 目录下面的结构有这三个特点
首先,以 all.bash 为代表的代码构建的脚本源文件放在了 src 下面的顶层目录下
第二,src 下的二级目录 cmd 下面存放着 Go 相关可执行文件的相关目录,我们可以深入查看一下 cmd 目录下的结构:
cd cmd
tree .
# 看到如下结果
./cmd
... ...
├── 6a/
├── 6c/
├── 6g/
... ...
├── cc/
├── cgo/
├── dist/
├── fix/
├── gc/
├── go/
├── gofmt/
├── ld/
├── nm/
├── objdump/
├── pack/
└── yacc/
可以看到,这里的每个子目录都是一个 Go 工具链命令或子命令对应的可执行文件。其中,6a、6c、6g 等是早期 Go 版本针对特定平台的汇编器、编译器等的特殊命名方式。
第三个特点,你会看到 src 下的二级目录 pkg 下面存放着运行时实现、标准库包实现,这些包既可以被上面 cmd 下各程序所导入,也可以被 Go 语言项目之外的 Go 程序依赖并导入。下面是我们通过 tree 命令查看 pkg 下面结构的输出结果:
cd pkg
tree .
# 看到如下结果
./pkg
... ...
├── flag/
├── fmt/
├── go/
├── hash/
├── html/
├── image/
├── index/
├── io/
... ...
├── net/
├── os/
├── path/
├── reflect/
├── regexp/
├── runtime/
├── sort/
├── strconv/
├── strings/
├── sync/
├── syscall/
├── testing/
├── text/
├── time/
├── unicode/
└── unsafe/
虽然 Go 语言的创世项目的 src 目录下的布局结构,离现在已经比较久远了,但是但是这样的布局特点依然对后续很多 Go 项目的布局产生了比较大的影响。比如,Go 调试器项目 Delve、开启云原生时代的 Go 项目 Docker,以及云原生时代的“操作系统”项目 Kubernetes 等,它们的项目布局,至今都还保持着与 Go 创世项目早期相同的风格。
当然,现在布局结构也在一直在不断地演化,简单来说可以归纳为下面三个比较重要的演进。
演进一:Go 1.4 版本删除 pkg 这一中间层目录并引入 internal 目录
- 作用: 使Go 项目中 Go 包的分类与用途变得更加清晰
演进二:Go1.6 版本增加 vendor 目录
- 让 Go 项目第一次具有了可重现构建(Reproducible Build)的能力。
- Go 源码的编译可以不在 GOPATH 环境变量下面搜索依赖包的路径,而在 vendor 目录下查找对应的依赖包
演进三:Go 1.13 版本引入 go.mod 和 go.sum
- 还是为了解决 Go 包依赖版本管理的问题
Go 语言项目自身在 Go 1.13 版本引入 go.mod 和 go.sum 以支持 Go Module 构建机制,下面是 Go 1.13 版本的 go.mod 文件内容:
module std
go 1.13
require (
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
golang.org/x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect
golang.org/x/text v0.3.2 // indirect
)
Go 可执行程序项目的典型结构布局
$tree -F exe-layout
exe-layout
├── cmd/
│ ├── app1/
│ │ └── main.go
│ └── app2/
│ └── main.go
├── go.mod
├── go.sum
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
├── pkg2/
│ └── pkg2.go
└── vendor/
这样的一个 Go 项目典型布局就是“脱胎”于 Go 创世项目的最新结构布局,我现在跟你解释一下这里面的几个要点。
我们从上往下按顺序来,先来看 cmd 目录。cmd 目录就是存放项目要编译构建的可执行文件对应的 main 包的源文件。如果你的项目中有多个可执行文件需要构建,每个可执行文件的 main 包单独放在一个子目录中,比如图中的 app1、app2,cmd 目录下的各 app 的 main 包将整个项目的依赖连接在一起。
而且通常来说,main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高级的执行控制对象。另外,也有一些 Go 项目将 cmd 这个名字改为 app 或其他名字,但它的功能其实并没有变。
接着我们来看 pkgN 目录,这是一个存放项目自身要使用、同样也是可执行文件对应 main 包所要依赖的库文件,同时这些目录下的包还可以被外部项目引用。
然后是 go.mod 和 go.sum,它们是 Go 语言包依赖管理使用的配置文件。我们前面说过,Go 1.11 版本引入了 Go Module 构建机制,这里我建议你所有新项目都基于 Go Module 来进行包依赖管理,因为这是目前 Go 官方推荐的标准构建模式。
对于还没有使用 Go Module 进行包依赖管理的遗留项目,比如之前采用 dep、glide 等作为包依赖管理工具的,建议尽快迁移到 Go Module 模式。Go 命令支持直接将 dep 的 Gopkg.toml/Gopkg.lock 或 glide 的 glide.yaml/glide.lock 转换为 go.mod。
最后我们再来看看 vendor 目录。vendor 是 Go 1.5 版本引入的用于在项目本地缓存特定版本依赖包的机制,在 Go Modules 机制引入前,基于 vendor 可以实现可重现构建,保证基于同一源码构建出的可执行程序是等价的。
不过呢,我们这里将 vendor 目录视为一个可选目录。原因在于,Go Module 本身就支持可再现构建,而无需使用 vendor。 当然 Go Module 机制也保留了 vendor 目录(通过 go mod vendor 可以生成 vendor 下的依赖包,通过 go build -mod=vendor 可以实现基于 vendor 的构建)。一般我们仅保留项目根目录下的 vendor 目录,否则会造成不必要的依赖选择的复杂性。
当然了,有些开发者喜欢借助一些第三方的构建工具辅助构建,比如:make、bazel 等。你可以将这类外部辅助构建工具涉及的诸多脚本文件(比如 Makefile)放置在项目的顶层目录下,就像 Go 创世项目中的 all.bash 那样。
另外,这里只要说明一下的是,Go 1.11 引入的 module 是一组同属于一个版本管理单元的包的集合。并且 Go 支持在一个项目 / 仓库中存在多个 module,但这种管理方式可能要比一定比例的代码重复引入更多的复杂性。 因此,如果项目结构中存在版本管理的“分歧”,比如:app1 和 app2 的发布版本并不总是同步的,那么我建议你将项目拆分为多个项目(仓库),每个项目单独作为一个 module 进行单独的版本管理和演进。
当然如果你非要在一个代码仓库中存放多个 module,那么新版 Go 命令也提供了很好的支持。比如下面代码仓库 multi-modules 下面有三个 module:mainmodule、module1 和 module2:
$tree multi-modules
multi-modules
├── go.mod // mainmodule
├── module1
│ └── go.mod // module1
└── module2
└── go.mod // module2
我们可以通过 git tag 名字来区分不同 module 的版本。其中 vX.Y.Z 形式的 tag 名字用于代码仓库下的 mainmodule;而 module1/vX.Y.Z 形式的 tag 名字用于指示 module1 的版本;同理,module2/vX.Y.Z 形式的 tag 名字用于指示 module2 版本。
如果 Go 可执行程序项目有一个且只有一个可执行程序要构建,那就比较好办了,我们可以将上面项目布局进行简化:
$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
你可以看到,我们删除了 cmd 目录,将唯一的可执行程序的 main 包就放置在项目根目录下,而其他布局元素的功用不变。
好了到这里,我们已经了解了 Go 可执行程序项目的典型布局,现在我们再来看看 Go 库项目的典型结构布局是怎样的。
Go 库项目仅对外暴露 Go 包,这类项目的典型布局形式是这样的:
$tree -F lib-layout
lib-layout
├── go.mod
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
└── pkg2/
└── pkg2.go
我们看到,库类型项目相比于 Go 可执行程序项目的布局要简单一些。因为这类项目不需要构建可执行程序,所以去除了 cmd 目录。
而且,在这里,vendor 也不再是可选目录了。对于库类型项目而言,我们并不推荐在项目中放置 vendor 目录去缓存库自身的第三方依赖,库项目仅通过 go.mod 文件明确表述出该项目依赖的 module 或包以及版本要求就可以了。
Go 库项目的初衷是为了对外部(开源或组织内部公开)暴露 API,对于仅限项目内部使用而不想暴露到外部的包,可以放在项目顶层的 internal 目录下面。当然 internal 也可以有多个并存在于项目结构中的任一目录层级中,关键是项目结构设计人员要明确各级 internal 包的应用层次和范围。
对于有一个且仅有一个包的 Go 库项目来说,我们也可以将上面的布局做进一步简化,简化的布局如下所示:
$tree -L 1 -F single-pkg-lib-layout
single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└── internal/
简化后,我们将这唯一包的所有源文件放置在项目的顶层目录下(比如上面的 feature1.go 和 feature2.go),其他布局元素位置和功用不变。
Go项目典型项目结构分为五部分:
- 放在项目顶层的 Go Module 相关文件,包括 go.mod 和 go.sum;
- cmd 目录:存放项目要编译构建的可执行文件所对应的 main 包的源码文件;
- 项目包目录:每个项目下的非 main 包都“平铺”在项目的根目录下,每个目录对应一个 Go 包;
- internal 目录:存放仅项目内部引用的 Go 包,这些包无法被项目之外引用;
- vendor 目录:这是一个可选目录,为了兼容 Go 1.5 引入的 vendor 构建模式而存在的。这个目录下的内容均由 Go 命令自动维护,不需要开发者手工干预。