You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 lines
19 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 05标准先行Go项目的布局标准是什么
你好我是Tony Bai。
在前面的讲解中我们编写的Go程序都是简单程序一般由一个或几个Go源码文件组成而且所有源码文件都在同一个目录中。但是生产环境中运行的实用程序可不会这么简单通常它们都有着复杂的项目结构布局。弄清楚一个实用Go项目的项目布局标准是Go开发者走向编写复杂Go程序的第一步也是必经的一步。
但Go官方到目前为止也没有给出一个关于Go项目布局标准的正式定义。那在这样的情况下Go社区是否有我们可以遵循的参考布局或者事实标准呢我可以肯定的告诉你有的。在这一节课里我就来告诉你Go社区广泛采用的Go项目布局是什么样子的。
要想了解Go项目的结构布局以及演化历史全世界第一个Go语言项目是一个最好的切入点。所以我们就先来看一下Go语言“创世项目”的结构布局是什么样的。
### Go语言“创世项目”结构是怎样的
什么是“**Go语言的创世项目**”呢其实就是Go语言项目自身它是全世界第一个Go语言项目。但这么说也不够精确因为Go语言项目从项目伊始就混杂着多种语言而且以C和Go代码为主Go语言的早期版本C代码的比例还不小。
我们先用[loccount工具](https://gitlab.com/esr/loccount)对Go语言发布的第一个[Go 1.0版本](https://github.com/golang/go/releases/tag/go1)分析看看:
```plain
$loccount .
all SLOC=460992 (100.00%) LLOC=193045 in 2746 files
Go SLOC=256321 (55.60%) LLOC=109763 in 1983 files
C SLOC=148001 (32.10%) LLOC=73458 in 368 files
HTML SLOC=25080 (5.44%) LLOC=0 in 57 files
asm SLOC=10109 (2.19%) LLOC=0 in 133 files
... ...
```
你会发现在1.0版本中Go代码行数占据一半以上比例但是C语言代码行数也占据了32.10%的份额。而且在后续Go版本演进过程中Go语言代码行数占比还在逐步提升直到Go 1.5版本实现自举后Go语言代码行数占比将近90%C语言比例下降为不到1%,这一比例一直延续至今。
虽然C代码比例下降Go代码比例上升但Go语言项目的布局结构却整体保留了下来十多年间虽然也有一些小范围变动但整体没有本质变化。作为Go语言的“创世项目”它的结构布局对后续Go社区的项目具有重要的参考价值尤其是Go项目早期src目录下面的结构。
为了方便查看我们首先下载Go语言创世项目源码
```plain
$git clone https://github.com/golang/go.git
```
进入Go语言项目根目录后我们使用tree命令来查看一下Go语言项目自身的最初源码结构布局以Go 1.3版本为例,结果是这样的:
```plain
$cd go // 进入Go语言项目根目录
$git checkout go1.3 // 切换到go 1.3版本
$tree -LF 1 ./src // 查看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目录下的结构
```plain
$ tree -LF 1 ./cmd
./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下面结构的输出结果
```plain
# tree -LF 1 ./pkg
./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语言早期采纳者建立的Go项目。比如Go调试器项目Delve、开启云原生时代的Go项目Docker以及云原生时代的“操作系统”项目Kubernetes等它们的项目布局至今都还保持着与Go创世项目早期相同的风格。
当然了,这些早期的布局结构一直在不断地演化,简单来说可以归纳为下面三个比较重要的演进。
**演进一Go 1.4版本删除pkg这一中间层目录并引入internal目录**
出于简化源码树层次的原因Go语言项目的Go 1.4版本对它原来的src目录下的布局做了两处调整。第一处是删除了Go源码树中“src/pkg/xxx”中pkg这一层级目录而直接使用src/xxx。这样一来Go语言项目的源码树深度减少一层更便于Go开发者阅读和探索Go项目源码。
另外一处就是Go 1.4引入internal包机制增加了internal目录。这个internal机制其实是所有Go项目都可以用的Go语言项目自身也是自Go 1.4版本起就使用internal机制了。根据internal机制的定义一个Go项目里的internal目录下的Go包只可以被本项目内部的包导入。项目外部是无法导入这个internal目录下面的包的。可以说internal目录的引入让一个Go项目中Go包的分类与用途变得更加清晰。
**演进二Go1.6版本增加vendor目录**
第二次的演进其实是为了解决Go包依赖版本管理的问题Go核心团队在Go 1.5版本中做了第一次改进。增加了vendor构建机制也就是Go源码的编译可以不在GOPATH环境变量下面搜索依赖包的路径而在vendor目录下查找对应的依赖包。
Go语言项目自身也在Go 1.6版本中增加了vendor目录以支持vendor构建但vendor目录并没有实质性缓存任何第三方包。直到Go 1.7版本Go才真正在vendor下缓存了其依赖的外部包。这些依赖包主要是golang.org/x下面的包这些包同样是由Go核心团队维护的并且其更新速度不受Go版本发布周期的影响。
vendor机制与目录的引入让Go项目第一次具有了可重现构建Reproducible Build的能力。
**演进三Go 1.13版本引入go.mod和go.sum**
第三次演进还是为了解决Go包依赖版本管理的问题。在Go 1.11版本中Go核心团队做出了第二次改进尝试引入了Go Module构建机制也就是在项目引入go.mod以及在go.mod中明确项目所依赖的第三方包和版本项目的构建就将摆脱GOPATH的束缚实现精准的可重现构建。
Go语言项目自身在Go 1.13版本引入go.mod和go.sum以支持Go Module构建机制下面是Go 1.13版本的go.mod文件内容
```plain
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语言项目自身所依赖的包在go.mod中都有对应的信息而原本这些依赖包是缓存在vendor目录下的。
总的来说这三次演进主要体现在简化结构布局以及优化包依赖管理方面起到了改善Go开发体验的作用。可以说Go创世项目的源码布局以及演化对Go社区项目的布局具有重要的启发意义以至于在多年的Go社区实践后Go社区逐渐形成了公认的Go项目的典型结构布局。
### 现在的Go项目的典型结构布局是怎样的
一个Go项目通常分为可执行程序项目和库项目现在我们就来分析一下这两类Go项目的典型结构布局分别是怎样的。
**首先我们先来看Go可执行程序项目的典型结构布局。**
可执行程序项目是以构建可执行程序为目的的项目Go社区针对这类Go项目所形成的典型结构布局是这样的
```plain
$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、app2cmd目录下的各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下面有三个modulemainmodule、module1和module2
```plain
$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可执行程序项目有一个且只有一个可执行程序要构建那就比较好办了我们可以将上面项目布局进行简化
```plain
$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
```
你可以看到我们删除了cmd目录将唯一的可执行程序的main包就放置在项目根目录下而其他布局元素的功用不变。
**好了到这里我们已经了解了Go可执行程序项目的典型布局现在我们再来看看Go库项目的典型结构布局是怎样的。**
Go库项目仅对外暴露Go包这类项目的典型布局形式是这样的
```plain
$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库项目来说我们也可以将上面的布局做进一步简化简化的布局如下所示
```plain
$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可执行程序项目的经典布局这个又有所不同。
### 注意早期Go可执行程序项目的典型布局
很多早期接纳Go语言的开发者所建立的Go可执行程序项目深受Go创世项目1.4版本之前的布局影响这些项目将所有可暴露到外面的Go包聚合在pkg目录下就像前面Go 1.3版本中的布局那样,它们的典型布局结构是这样的:
```plain
$tree -L 3 -F early-project-layout
early-project-layout
└── exe-layout/
├── cmd/
│   ├── app1/
│   └── app2/
├── go.mod
├── internal/
│   ├── pkga/
│   └── pkgb/
├── pkg/
│   ├── pkg1/
│   └── pkg2/
└── vendor/
```
我们看到原本放在项目顶层目录下的pkg1和pkg2公共包被统一聚合到pkg目录下了。而且这种早期Go可执行程序项目的典型布局在Go社区内部也不乏受众很多新建的Go项目依然采用这样的项目布局。
所以当你看到这样的布局也不要奇怪并且在我的讲解后你应该就明确在这样的布局下pkg目录所起到的“聚类”的作用了。不过在这里还是建议你在创建新的Go项目时优先采用前面的标准项目布局。
### 小结
到这里我们今天这门课就结束了。在这一节课里我们学习了Go创世项目也就是Go语言项目自身的项目源码布局以及演进情况。在Go创世项目的启发下Go社区在多年实践中形成了典型的Go项目结构布局形式。
我们将Go项目分为可执行程序项目和Go库项目两类进行了详细的项目典型布局讲解这里简单回顾一下。
首先对于以生产可执行程序为目的的Go项目它的典型项目结构分为五部分
* 放在项目顶层的Go Module相关文件包括go.mod和go.sum
* cmd目录存放项目要编译构建的可执行文件所对应的main包的源码文件
* 项目包目录每个项目下的非main包都“平铺”在项目的根目录下每个目录对应一个Go包
* internal目录存放仅项目内部引用的Go包这些包无法被项目之外引用
* vendor目录这是一个可选目录为了兼容Go 1.5引入的vendor构建模式而存在的。这个目录下的内容均由Go命令自动维护不需要开发者手工干预。
第二对于以生产可复用库为目的的Go项目它的典型结构则要简单许多我们可以直接理解为在Go可执行程序项目的基础上去掉cmd目录和vendor目录。
最后早期接纳Go语言的开发者所建立的项目的布局深受Go创世项目1.4版本之前布局的影响将可导出的公共包放入单独的pkg目录下我们了解这种情况即可。对于新建Go项目我依旧建议你采用前面介绍的标准布局形式。
现在如果你要再面对一个要用于生产环境的Go应用项目的布局问题是不是胸有成竹了呢
### 思考题
如果非要你考虑Go项目结构的最小标准布局那么你觉得这个布局中都应该包含哪些东西呢欢迎在留言区留下你的答案。
感谢你和我一起学习也欢迎你把这节课分享给更多对Go项目布局感兴趣的朋友。我是Tony Bai我们下节课见。