Go的Project布局浅谈

图文好读版连结

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 项目目录该怎么组织?官方终于出指南了!


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章