Golang - 談談 project layout

今天的主題來談談 Golang 的 project layout 的各種形式。所謂 project layout 指的就是基於 Golang 的專案架構,要知道官方是沒有宣布或是規定怎樣的專案架構。造成社群中大家對 Golang 的 project layout 可能看法都不同,因此我也記錄一下我自己選擇 project layout 的思路。

也會介紹社群中應該最為人所知的:https://github.com/golang-standards/project-layout

Go 本身的 project layout

先來看 Go 本身在 commit version:8d57f4d 的 project layout 是長怎樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tree -LF 1 ~/go/src/github.com/golang/go
./go
├── CONTRIBUTING.md
├── LICENSE
├── PATENTS
├── README.md
├── SECURITY.md
├── api/
├── codereview.cfg
├── doc/
├── lib/
├── misc/
├── src/
└── test/

其中 src 下是 Go 本身的 source code 包含常使用的 library 都在這定義,因此不少的 project layout 也是參考 src 下的目錄結構。

來看看 src 底下的結構:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
./src
├── Make.dist
├── README.vendor
├── all.bash*
├── all.bat
├── all.rc*
├── archive/
├── bootstrap.bash*
├── bufio/
├── buildall.bash*
├── builtin/
├── bytes/
├── clean.bash*
├── clean.bat
├── clean.rc*
├── cmd/
├── cmp.bash
├── compress/
├── container/
├── context/
├── crypto/
├── database/
├── debug/
├── embed/
├── encoding/
├── errors/
├── expvar/
├── flag/
├── fmt/
├── go/
├── go.mod
├── go.sum
├── hash/
├── html/
├── image/
├── index/
├── internal/
├── io/
├── log/
├── make.bash*
├── make.bat
├── make.rc*
├── math/
├── mime/
├── net/
├── os/
├── path/
├── plugin/
├── race.bash*
├── race.bat
├── reflect/
├── regexp/
├── run.bash*
├── run.bat
├── run.rc*
├── runtime/
├── sort/
├── strconv/
├── strings/
├── sync/
├── syscall/
├── testdata/
├── testing/
├── text/
├── time/
├── unicode/
├── unsafe/
└── vendor/
  1. 我們常使用的 fmt 或是 strong package 都在這邊的第一層目錄就定義好了。

  2. internal 目錄是用來存放不需要給外部導入的 package,只能自身使用

  3. vendor 目錄存放了 Go 本身需要參考外部 package 的作用,這是因為之前是透過 vendor 的機制來進行套件版本管理,而現在官方則是變成 go module 的方式來進行套件版本管理。因此在這邊你也可以看到有 go.modgo.sum 這兩個檔案。這些都是因為 Go 本身版本更迭而隨之誕生的檔案。

  4. cmd 目錄放的都是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    ./cmd
    ├── README.vendor
    ├── addr2line/
    ├── api/
    ├── asm/
    ├── buildid/
    ├── cgo/
    ├── compile/
    ├── cover/
    ├── dist/
    ├── doc/
    ├── fix/
    ├── go/
    ├── go.mod
    ├── go.sum
    ├── gofmt/
    ├── internal/
    ├── link/
    ├── nm/
    ├── objdump/
    ├── pack/
    ├── pprof/
    ├── test2json/
    ├── trace/
    ├── vendor/
    └── vet/

    各種 Go tool 相關的實現,例如常見的 gofmtobjdumppprof 等等,在這邊可以直接進行編譯出可執行的檔案。

    光看了這些我們就能知道一些脈絡:

    1. 各種 package 的實現直接放在頂層目錄
    2. cmd 專門放 main func 以便 go build 出可執行檔
    3. internal 專門放不允許讓外部 import 的 pkg

    這三個規則,其實就是現在我們所看到的常見 project layout。

Go 的 project layout 類型

事實上,Go 官方始終沒有給出怎樣的 project layout 才是對的,但是推崇的精神就是:simple,且有說過 package 通常會跟目錄名稱一樣且 package 名稱不要取像是 util or model,這種我以前在寫 Java 常用的 pattern,而是希望 package 要取有意義的名稱不要模稜兩可。

Go 簡單的 project layout

知名的 rsc,就有在社群中的 golang-standard-project-layoutissue 評論說:

The README makes clear that this is not official, but even the claim “it is a set of common historical and emerging project layout patterns in the Go ecosystem” is not accurate.

For example, the vast majority of packages in the Go ecosystem do not put the importable packages in a pkg subdirectory. More generally what is described here is just very complex, and Go repos tend to be much simpler.

It is unfortunate that this is being put forth as “golang-standards” when it really is not. I’m commenting here because I am starting to see people say things like “you are not using the standard Go project layout” and linking to this repo.

其實意思就是說,很多人都推崇這個 repo 的 project layout 並很有可能會視為官方的 project layout, rsc 認為大多數的 go open source 專案其實不會有所謂的 pkg 根目錄來存放可給外部 import 的 package,而是直接像上面我們看 Go 本身的 project layout 那樣:package 的名字直接放在根目錄上。

甚至連 cmd 與 docs 目錄也是不需要的。

我個人的理解是:

  1. 如果是要設計本來就是 for library 的方式且是不會有過多的 package 的小型專案,可以採用只需要定義 package 在根目錄上就可以了
  2. 如果不需要可執行檔,那麼 cmd 目錄當然也不需要
  3. docs 目錄也是根據專案的大小而定,我覺得見仁見智。

所以簡單的 project layout 就誕生了。

Go 複雜的 project layout

看了一下 golang-standard-project-layout,可以知道,就是推崇有:

  1. /cmd
  2. /internal
  3. /pkg
  4. /vendor

當因為有 rsc 提出的 issue,其實在 golang standard project layout 的 readme 也有特別聲明以上四種的使用情境了。

  • cmd 只適合放專案的入口點,main func,但如果你不需要編譯可執行檔案那其實這個目錄當然也不需要

  • internal 只適合放你不想要公開的 package,例如 mock 的檔案或是常見還會有 internal/app/myapp 這樣的方式。

  • pkg 只適合當你有複雜的專案,有太多的 package 了,如果沒有加上 pkg 在根目錄那整個 project 會很長,不好視覺上去看出。另外需要考慮的點,如果本身根目錄還需要放一些不屬於 Go code 所需要的檔案,那麼加上 pkg 就有其必要性。

    事實上 kubernetes 就是採用 pkg 的方式去管理。

    另外一套的說法是,如果你需要 pkg 的話,代表你的 project 很複雜且是 application 的形式不是 for library 的話,需要公開 pkg 嗎?更應該放在 internal 裡面吧,並且將所需要的 package 拆成多個 repo 去引用會更好,當你這樣拆分之後,那麼所剩下真的需要外部可以 impot 的 package 就可以直接定義在根目錄就可以了,這樣的說法可以參考的專案是:mux

  • vendor 基本上有了 Go module 就用不太到了,所以可以捨棄。

另外基於你的專案性質可能還多出類似的目錄:

  • /api
  • /web
  • /configs
  • /init
  • /build

etc…

這些不關於 go code 的目錄或是檔案,我個人認為就比較是個人喜好。當然有個 standard project layout 大家會比較有共識,但是我覺得這也是 Go 官方不給出 project layout 的原因在於沒辦法知道每個人的喜好且專案會是怎樣的性質,所以很難給出統一的 project layout。而在團隊協作中,我比較認為這就是團隊的 guideline 去定義了,只要團隊中每個人都認可且又不違反 Go 官方認定的 package 的方式那我覺得都可以。

總結

所以,我個人認為如果一開始你能夠知道專案的性質及大小,就可以自由去選擇最簡單的 project layout 或是複雜的 project layout,如果一開始不知道呢?那就採用簡單的 project layout 隨著功能變多而去演化吧。在工作上,也是團隊上定義好 guideline 能夠去遵守就可以了。

參考資料:

  1. https://eli.thegreenplace.net/2019/simple-go-project-layout-with-modules/
  2. https://go.dev/blog/organizing-go-code