专栏目录
11 理解包导入路径的含义 44 利其器!Go常用工具大检阅 22 Go 并发模型和常见并发模式 35 告别乱码!GO语言字符集编码方案间转换 12 go语言 init 函数的妙用 41 与时俱进!使用module管理依赖包 1 参考 Go 项目布局设计你的项目结构 16 方法集合决定接口实现 17 go变长参数函数的妙用 7 go语言定义“零值可用”的类型 30 Go 惯例:将测试依赖的外部数据文件放在 testdata 下面 34 一文告诉你如何在 Go 中实现 HTTPS 通信 24 sync 包的正确使用姿势 6 Go“枚举常量”的惯用实现方法 4 变量声明形式尽量保持一致 2 gofmt:Go代码风格的唯一标准 3 Go 标识符的命名惯例 9 深入理解和高效运用切片 10 Go 字符串是原生类型 8 用复合字面值作初值构造器 13 Go 函数是“一等公民”的理解 14 defer 让你的代码更清晰 19 不要在函数参数中使用空接口(interface{}) 23 Go channel 的常见使用模式 18 定义小接口是 Go 的惯例 21 面试必考!掌握 goroutine 的调度原理 15 Go 方法的本质 29 Go 单元测试惯例:表驱动 26 if err != nil 重复太多可以这么办 27 不要让 panic 掺和到正常错误处理中 28 一文告诉你测试包的包名要不要带“\_test”后缀 31 为被测对象建立性能基准 25 别笑!这就是 Go 的错误处理哲学 32 掌握 Go 代码性能剖析神器:pprof 33 掌握 Go 代码调试利器:delve 39 慎用reflect包提供的反射能力 38 小心被kill!不要忽略对系统信号的处理 36 像极!bytes包和strings包的那些相似操作 40 与C互操作不是免费的!疑问了解cgo的使用成本 43 让你的Go包拥有个性化的导入路径 37 time包,你用对了吗 42 小即是美?构建最小Go程序容器镜像 20 要提高代码可测试性,请使用接口 45 未雨绸缪!Go语言常见“坑”大汇 5 无类型常量让代码更简化

1 参考 Go 项目布局设计你的项目结构

九路
• 阅读 1179

除非是像“hello world”这样的简单程序,但凡我们编写一些 non-trivial 的实用程序或库,我们都会遇到采用什么样的项目结构(project structure)的问题(通常一个项目对应一个仓库 repository)。在 Go 语言中,项目结构同样十分重要,因为这决定了项目内部包(package)的布局以及包依赖关系是否合理,同时还会影响到外部项目对该项目中包的依赖。

1. Go 项目的项目结构

我们先来看看世界上第一个 Go 项目- Go 语言自身的项目结构是什么样的。

Go 项目的项目结构从发布 1.0 版本以来一直十分稳定,直到现在 Go 项目的顶层结构基本没有大的改变。截至 go 项目 commit 1e3ffb0c(2019.5.14),go 项目结构如下:

# tree -LF 1 ~/go/src/github.com/golang/go
./go
├── api/
├── AUTHORS
├── CONTRIBUTING.md
├── CONTRIBUTORS
├── doc/
├── favicon.ico
├── lib/
├── LICENSE
├── misc/
├── PATENTS
├── README.md
├── robots.txt
├── src/
└── test/

6 directories, 8 files

作为 Go 语言的“创世项目”,其项目结构对后续的其他 Go 语言项目具有重要的参考意义,尤其是 go 项目早期 src 目录下面的结构,以 Go 1.3 版本为例:

# tree -LF 1 ./src
./src
├── all.bash*
├── all.bat
├── all.rc*
├── clean.bash*
├── clean.bat
├── clean.rc*
├── cmd/
├── lib9/
├── libbio/
├── liblink/
├── make.bash*
├── make.bat
├── Make.dist
├── make.rc*
├── nacltest.bash*
├── pkg/
├── race.bash*
├── race.bat
├── run.bash*
├── run.bat
├── run.rc*
└── sudo.bash*

5 directories, 17 files

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

  • 代码构建的脚本源文件放在 src 下面的顶层目录下;
  • src 下的二级目录 cmd 下面存放着 go 相关可执行文件的相关目录以及 main 包;
# tree -LF 1 ./cmd
./cmd
... ...
├── 6a/
├── 6c/
├── 6g/
... ...
├── cc/
├── cgo/
├── dist/
├── fix/
├── gc/
├── go/
├── gofmt/
├── ld/
├── nm/
├── objdump/
├── pack/
└── yacc/

26 directories, 0 files
  • src 下的二级目录 pkg 下面存放着上面 cmd 下各程序依赖的包、go 运行时以及 go 标准库的源文件
# tree -LF 1 ./pkg
./pkg
... ...
├── flag/
├── fmt/
├── go/
├── hash/
├── html/
├── image/
├── index/
├── io/
├── log/
├── math/
... ...
├── net/
├── os/
├── path/
├── reflect/
├── regexp/
├── runtime/
├── sort/
├── strconv/
├── strings/
├── sync/
├── syscall/
├── testing/
├── text/
├── time/
├── unicode/
└── unsafe/

39 directories, 0 files

虽然 Go 1.4 版本中删除了 Go 源码树中“src/pkg/xxx”中 pkg 这一层级目录而直接使用 src/xxx,但早期 Go 项目 src 目录下的这种结构布局特点依然对后续多数 Go 语言项目的项目结构产生了较大的影响。

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

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

1 参考 Go 项目布局设计你的项目结构

上面就是一个支持构建二进制可执行文件(在 cmd 下)的典型 Go 项目的结构。

  • 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 目录,否则会造成不必要的依赖选择的复杂性。

Go 1.11 引入的 module 是一组同属于一个版本管理单元的包的集合。如果项目结构中存在版本管理的“分歧”,比如:app1 和 app2 的发布版本并不总是同步的,那么建议将项目拆分为多个项目(仓库),每个项目单独承载一个 module 进行单独的版本管理和演进。

Go 支持在一个项目/仓库中存在多个 module,但这种管理方式可能要比一定比例的代码重复引入更多的复杂性。

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

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

我们看到库类型项目相比于构建二进制可执行文件的项目要简单一些:

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

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

// 带internal的Go库项目结构

GoLibProj
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── internal/
│   ├── ilib1/
│   └── ilib2/
├── lib.go
├── lib1/
│   └── lib1.go
└── lib2/
    └── lib2.go

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

4. 小结

以上的两个针对构建二进制可执行文件类型以及库类型的项目参考结构是 Go 社区在多年实践后得到公认且使用较为广泛的项目结构。但它们也不是银弹,在 Go 语言早期,很多项目将所有源文件都放在位于项目根目录下的根包中的作法在一些小规模项目中同样工作得很好,虽然我们现在不推荐这么做了。

对于以构建二进制可执行文件类型为目的的项目来说,受 Go 1.4 项目结构影响,将 pkg 这一层次目录去掉也是很多项目选择的结构布局方式。

上述的参考项目结构与产品设计开发领域的“最小可行产品”(minimum viable product,简称为 mvp)的思路有些异曲同工,开发者可以在这样一个最小的“项目结构核心”的基础上根据实际需要对其进行扩展。

点赞
收藏
评论区
推荐文章

暂无数据