1. Go 项目的项目结构

  1. # tree -LF 1 ~/go/src/github.com/golang/go
  2. ./go
  3. ├── api/
  4. ├── AUTHORS
  5. ├── CONTRIBUTING.md
  6. ├── CONTRIBUTORS
  7. ├── doc/
  8. ├── favicon.ico
  9. ├── lib/
  10. ├── LICENSE
  11. ├── misc/
  12. ├── PATENTS
  13. ├── README.md
  14. ├── robots.txt
  15. ├── src/
  16. └── test/
  17. 6 directories, 8 files

关于 src 下面的结构,我们总结三个特点:

  • 代码构建的脚本源文件放在 src 下面的顶层目录下;
  • src 下的二级目录 cmd 下面存放着 go 相关可执行文件的相关目录以及 main 包
  • src 下的二级目录 pkg 下面存放着上面 cmd 下各程序依赖的包、go 运行时以及 go 标准库的源文件

2. Go 语言典型项目结构(构建二进制可执行文件类型)

基于上述参考项目结构,Go 社区在多年的 Go 语言实践积累后逐渐形成了一种典型项目结构,如下图所示:

image.png

cmd 目录:存放项目要编译构建的可执行文件对应的 main 包的源文件。如果有多个可执行文件需要构建,每个可执行文件的 main 包单独放在一个子目录中,比如图中的 app1、app2;cmd 目录下的各 app 的 main 包将整个项目的依赖连接在一起;并且通常来说,main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高级的执行控制对象;也有一些 go 项目将 cmd 这个名字改为 app,但其功用并没有变;

pkg 目录:存放项目自身要使用、同样也是可执行文件对应 main 包所要依赖的库文件;同时该目录下的包还可以被外部项目引用;也有些项目将 pkg 这个名字改为 lib,但目录用途不变;

Makefile:这里的 Makefile 是项目构建工具所用脚本的“代表”。Go 并没有内置如 make、bazel 等级别的项目构建工具,对于一些规模稍大的项目而言,项目构建工具似乎还不可缺少。在 Go 典型项目中,项目构建工具的脚本一般放在项目顶层目录下,比如这里的 Makefile;对于构建脚本较多的项目,也可以建立 build 目录,并将构建脚本的规则属性文件、子构建脚本放入其中;

go.mod 和 go.sum:Go 语言包依赖管理使用的配置文件。Go 1.11 版本引入 go modules 机制,因此新项目建议基于 go modules 进行包依赖管理;对于没有使用 go modules 进行包管理的项目,这里可以换为 dep 的 Gopkg.toml 和 Gopkg.lock 或者 glide 的 glide.yaml 和 glide.lock 等;

vendor 目录(可选):vendor 是 Go 1.5 版本引入的用于在项目本地缓存特定版本依赖包的机制,在 go modules 机制引入前,基于 vendor 可以实现可再现构建(reproducible build),保证基于同一源码构建出的可执行程序是等价的,这个机制是对中国大陆地区的 gopher 们尤为实用。go modules 本身就可以实现可再现构建,而无需 vendor,因此这里将 vendor 目录视为一个可选目录。一般我们仅保留项目根目录下的 vendor 目录,否则会造成不必要的依赖选择的复杂性。

3. Go 语言典型项目结构(构建库类型)

Go 1.4 发布时,Go 语言项目自身去掉了 src 下的 pkg 这一层目录,这个结构上的改变对那些只编译为库的 Go 语言库类型项目结构有着一定的影响。我们来看一个典型的 Go 语言库类型项目的结构布局:

image.png

去除了 cmd 和 pkg 两个子目录;

vendor 也不再是可选目录:对于库类型项目而言,我们不推荐在项目中放置 vendor 目录去缓存库自身的第三方依赖,库项目仅通过 go.mod(或其他包依赖管理工具的 manifest 文件)明确表述出该项目依赖的模块或包以及版本要求即可。

Go 库项目的初衷是为了对外部(开源或组织内部公开)暴露 API,对于仅限项目内部使用的包,在项目结构上可以通过 Go 1.4 版本中引入的 internal 包机制来实现。对库项目而言,最简单的方式就是在顶层加入一个 internal 目录,将不想暴露到外部的包都放在该目录下,比如下面项目结构中的 ilib1、ilib2:

  1. // internalGo库项目结构
  2. GoLibProj
  3. ├── LICENSE
  4. ├── Makefile
  5. ├── README.md
  6. ├── go.mod
  7. ├── internal/
  8. ├── ilib1/
  9. └── ilib2/
  10. ├── lib.go
  11. ├── lib1/
  12. └── lib1.go
  13. └── lib2/
  14. └── lib2.go

这样,根据 go internal 机制的作用原理,internal 目录下的 ilib1、ilib2 可以被以 GoLibProj 目录为根目录的其他目录下的代码(比如 lib.go、lib1/lib1.go 等)所导入和使用,但是却不可以为 GoLibProj 目录以外的代码所使用,从而实现选择性的暴露 API 包。当然 internal 也可以放在项目结构中的任一目录层级中,关键是项目结构设计人员明确哪些要暴露到外层代码,哪些仅用于同级目录或子目录中。