图文好读版连结
Project Layout in Go
刚入门任何一门程式语言开发的人, 应该大多都是参考各路大神们的专案或者公司的专案在学习模仿。 一开始印入眼帘的应该就是 Project布局的各种长相, Project布局关心的是我们怎样组织 Go project。 这里针对的是资料夹跟档案的布局,官方Blog Organizing a Go module这篇文章有给我们建议跟说明。让我们一起读这篇吧!
在阅读这篇之前能先稍微理解Go Module以及Go install的用法。
官方Blog Organizing a Go module这篇文章内把Go专案的内容大致分成Package和Command或者两个的组合。 Package
就是我们熟知的library, Command
则是executable能编译出来执行的go档案。
演化路程如下图, 因为Gif在这无法支援请至图文好读版连结
如果是纯library专案给人家import到别人的go project内使用的, 那package这条路线就是我们能参考。 像这个go-linq,该专案都是library。go-redis也是这类型的专案。
如果是需要安装到主机上变成执行档的,那command这条路线能参考。 像protoc-gen-go。
Basic package
下面的目录结构长相是go module和package code都在专案的根目录中。
project-root-directory/ go.mod modname.go modname_test.go
上面的 modname.go
是package code,modname_test.go
则是与modname.go相关的测试程式。
如果把这包程式码上传到Github repo上,例如 github.com/someuser/modname
,那么 go.mod
里指定该module的路径也会是 github.com/someuser/modname
,这点之后有机会在介绍go module的细节。
module github.com/someuser/modname
然后因为package code在根目录,所以没意外这package code一开始的package name会是 modname
package modname
这样别人就能透过 go get
github.com/someuser/modname
来取得这公开的package。
也可以该package的程式码能切分开来在多个档案中,只要这些档案都在同一层的目录中。
project-root-directory/ go.mod modname.go modname_test.go auth.go auth_test.go hash.go hash_test.go
这些档案除了在同一层目录中,同时它们的package name也都是modname。
Basic command
如果是要可以执行的程式,那最基本的专案结构会长的像
project-root-directory/ go.mod auth.go auth_test.go client.go main.go
其中 main.go
会包含 func main
,这应该没什么问题,虽然档名也能叫 modname.go
,只是main.go比较是惯例命名。 这目录中所有的档案其pakcage name也会是 main
,毕竟go同一层目录的package name必须一样否则编译会出错。
同上该专案被上传到 github.com/someuser/modname
module github.com/someuser/modname
那么我们就能够透过 go install
来下载并安装
go install github.com/someuser/modname@latest
只是通常这种可执行的专案没那么单纯,我们会有一些程式码不希望发布上git后被别人的专案给go get来使用那些package。这时就出现 interanl
资料夹了。
Internal package
Go在1.4时加入了internal package的机制。 internal package除了不能让外面的用户直接import之外,代表着对其他module来说,该internal package对它们来说是不可见
的,所以能做一层隔离,interanl package内程式的修改对外部是没影响的
同时也只能被同一个阶层以下
的所有package来存取,所以绝大部分internal package都被放在根目录,就是希望让根目录中所有package都能去导入 internal package。
project-root-directory/ go.mod main.go package1/ internal/ interal.go package1.go package2/ internal/ interal.go package2.go interanl/ version/ version.go internal.go
这样的定义下,package1的interanl只有package1这层以下的能导入,package2的interanl也是,连main都无法导入到这两个interanl package。 但是main可以导入根目录下的internal package。 package1能够导入根目录下的interanl package。
图片请至图文好读版连结
如果硬要import,则编译时会出现编译错误 use of internal package xxxx/internal not allowed
至于如果package1刚好要导入root internal package, 都要给导入internal package一个别名, 才能正常使用;除非引用的是internal package的下一层package。 如导入上面目录结构的 internal/version
, 就没问题。 如果是刚好想要导入internal package的internal.go的部份就要给上alias别名。
Package or command with supporting packages
有了internal package的认识后,就能将interanl package理解成supporting package,就是我们内部会引用到,但不希望被外部专案给直接引用的package。这时候Go官方就会建议我们放到 internal
目录中。
project-root-directory/ internal/ auth/ auth.go auth_test.go hash/ hash.go hash_test.go go.mod modname.go modname_test.go
在这样目的下,我们就能设计modname.go,能导入auth package和hash package。 modname.go能这样导入
import "github.com/someuser/modname/internal/auth"
但外部只能导入modname package来使用。
Multiple packages
一个专案可以有多个能被导入的package,且每个package都会有自己的目录结构来进行组织。
project-root-directory/ go.mod modname.go modname_test.go auth/ auth.go auth_test.go token/ token.go token_test.go hash/ hash.go internal/ trace/ trace.go
这样子的结构意味着,别人可以导入
github.com/someuser/modnamegithub.com/someuser/modname/authgithub.com/someuser/modname/hash
也能导入auth package的下一层token package
github.com/someuser/modname/auth/token
外部专案就是不能导入
github.com/someuser/modname/internal/trace
在这专案结构设计上,刚好interanl package放在根目录中,就是为了能让专案内的package给导入使用。 例如专案内的package就能导入trace package使用
github.com/someuser/modname/internal/trace
Multiple commands
一个专案能包含多个可执行的程序。像是例子中的prog1/main.go和prog2/main.go
project-root-directory/ go.mod internal/ ... shared internal packages prog1/ main.go prog2/ main.go
所以使用者可以透过go install来直接安装使用
$ go install github.com/someuser/modname/prog1@latest$ go install github.com/someuser/modname/prog2@latest
但是为了好区别可执行command的目录名称,通常会建议放入一个名为 cmd
的目录内,这样子会很好区分出哪些是可以被导入的package哪些则是可以被执行的command。这些都是建议,并非必要。
Packages and commands in the same repository
如果一个专案同时提供可以被导入的package和能够被安装执行的command,通常会像这样的结构。
project-root-directory/ go.mod modname.go modname_test.go auth/ auth.go auth_test.go internal/ ... internal packages cmd/ prog1/ main.go prog2/ main.go
这样子的结构意味着,别人可以导入
github.com/someuser/modnamegithub.com/someuser/modname/auth
能透过go install来安装cmd/prog1和cmd/prog2
$ go install github.com/someuser/modname/cmd/prog1@latest$ go install github.com/someuser/modname/cmd/prog2@latest
以Prometheus这专案为例子
github.com/prometheus/prometheus/ cmd/ prometheus/ main.go promtool/ main.go internal/ model/ plugins/ rules/ scrape/ scripts/ storage/ interface.go
Prometheus提供了prometheus能安装启动,还提供了promtool这工具,能让我们安装来对prometheus进行检查还有检查各种prometheus设定文件等作用。
又因为prometheus本身只提供local storage功能, 所以在stoage package内有定义了interace来给其他storage service来导入开发用。 像是Grafana Lab旗下的Mimir,内部就导入很多Prometheus专案内的package。
但你不会看到Mimir去导入prometheus/internal底下的package,因为这些只能被prometheus专案内部所导入。
总结
官方这篇文章主要针对专案根目录,internal目录做说明,cmd目录则是建议。 如果我们的专案需要提供一堆package供别专案导入时,有人也是会建议集中放到 pkg
目录中,让根目录乾净点,但也只是建议。
对我来说目录结构反应的也是设计时的一个边界,怎样探索出边界,不是按照这篇Project布局来被强迫设计就这样塞入,该repo的issue也在讨论这份也不是标準Go专案布局。
更多的我想还是回归场景与需求的设计,像是DDD中提到的subdomain与bounded context, 就是基于业务层面的设计布局,在基于这样业务布局进行专案结构的布局设计,这样能让专案设计贴近于业务设计,减低认知与转化负担。
令一个层面是关于release,如果该专案内有些package对于其他专案项目有用处,为了方便管理发布与版本控制,官方是建议拆分成独立的专案进行管理。只有跟我们这包系统有紧密生命週期相关的package和command才集中在一个专案内,方便开发和部署。
参考资料
Go doc Organizing a Go module
Go doc go1.4 Internal packages
Go 项目目录该怎么组织?官方终于出指南了!