# Project Layout in Go

剛入門任何一門程式語言開發的人, 應該大多都是參考各路大神們的專案或者公司的專案在學習模仿。 一開始印入眼簾的應該就是 **Project布局**的各種長相， Project布局關心的是我們怎樣**組織** Go project。 這裡針對的是資料夾跟檔案的布局，[官方Blog Organizing a Go module](https://go.dev/doc/modules/layout#multiple-packages)這篇文章有給我們建議跟說明。讓我們一起讀這篇吧！

在閱讀這篇之前能先稍微理解[Go Module](https://go.dev/blog/using-go-modules)以及[Go install](https://go.dev/ref/mod#go-install)的用法。

---

[官方Blog Organizing a Go module](https://go.dev/doc/modules/layout#multiple-packages)這篇文章內把Go專案的內容大致分成Package和Command或者兩個的組合。 `Package` 就是我們熟知的library, `Command` 則是executable能編譯出來執行的go檔案。

演化路程如下圖

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1696833947497/e810dde0-db53-404c-9985-8e3ec5cf277d.gif align="center")

如果是純library專案給人家import到別人的go project內使用的, 那package這條路線就是我們能參考。 像這個[go-linq](https://github.com/ahmetb/go-linq)，該專案都是library。[go-redis](https://github.com/redis/go-redis)也是這類型的專案。

如果是需要安裝到主機上變成執行檔的，那command這條路線能參考。 像[protoc-gen-go](https://github.com/protocolbuffers/protobuf-go)。

## Basic package

下面的目錄結構長相是go module和package code都在專案的根目錄中。

```bash
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的細節。

```arduino
module github.com/someuser/modname
```

然後因為package code在根目錄，所以沒意外這package code一開始的package name會是 `modname`

```bash
package modname
```

這樣別人就能透過 `go get` `github.com/someuser/modname` [來取得這公開的package。](http://github.com/someuser/modname%E4%BE%86%E5%8F%96%E5%BE%97%E9%80%99%E5%85%AC%E9%96%8B%E7%9A%84package%E3%80%82)

也可以該package的程式碼能切分開來在多個檔案中，只要這些檔案都在同一層的目錄中。

```bash
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

如果是要可以執行的程式，那最基本的專案結構會長的像

```bash
project-root-directory/
  go.mod
  auth.go
  auth_test.go
  client.go
  main.go
```

其中 `main.go` 會包含 `func main`，這應該沒什麼問題，雖然檔名也能叫 `modname.go`，只是main.go比較是慣例命名。 這目錄中所有的檔案其package name也會是 `main`，畢竟go同一層目錄的package name必須一樣否則編譯會出錯。

[同上該專案被上傳到](http://xn--github-9o7ia342d47drxg322a2m0bo02g6tf.com/someuser/modname) `github.com/someuser/modname`

```arduino
module github.com/someuser/modname
```

那麼我們就能夠透過 `go install` 來下載並安裝

```ruby
go install github.com/someuser/modname@latest
```

只是通常這種可執行的專案沒那麼單純，我們會有一些程式碼不希望發布上git後被別人的專案給go get來使用那些package。這時就出現 `internal` 資料夾了。

### Internal package

Go在1.4時加入了[internal package](https://go.dev/doc/go1.4#internalpackages)的機制。 internal package除了不能讓外面的用戶直接import之外，代表著對其他module來說，該internal package對它們來說是`不可見`的，所以能做一層隔離，internal package內程式的修改對外部是沒影響的

同時也只能被`同一個階層以下`的所有package來存取，所以絕大部分internal package都被放在根目錄，就是希望讓根目錄中所有package都能去導入 internal package。

```bash
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的internal只有package1這層以下的能導入，package2的internal也是，連main都無法導入到這兩個internal package。 但是main可以導入根目錄下的internal package。 package1能夠導入根目錄下的internal package。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1696836457041/e58ce153-ca4d-422b-ba42-e0cc509dd206.png align="center")

如果硬要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的認識後，就能將internal package理解成supporting package，就是我們內部會引用到，但不希望被外部專案給直接引用的package。這時候Go官方就會建議我們放到 `internal` 目錄中。

```bash
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能這樣導入

```bash
import "github.com/someuser/modname/internal/auth"
```

但外部只能導入modname package來使用。

## Multiple packages

一個專案可以有多個能被導入的package，且每個package都會有自己的目錄結構來進行組織。

```bash
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
```

這樣子的結構意味著，別人可以導入

```bash
github.com/someuser/modname
github.com/someuser/modname/auth
github.com/someuser/modname/hash
```

也能導入auth package的下一層token package

```bash
github.com/someuser/modname/auth/token
```

外部專案就是不能導入

```bash
github.com/someuser/modname/internal/trace
```

在這專案結構設計上，剛好internal package放在根目錄中，就是為了能讓專案內的package給導入使用。 例如專案內的package就能導入trace package使用

```bash
github.com/someuser/modname/internal/trace
```

## Multiple commands

一個專案能包含多個可執行的程序。像是例子中的prog1/main.go和prog2/main.go

```bash
project-root-directory/
  go.mod
  internal/
    ... shared internal packages
  prog1/
    main.go
  prog2/
    main.go
```

所以使用者可以透過go install來直接安裝使用

```ruby
$ 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，通常會像這樣的結構。

```bash
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
```

這樣子的結構意味著，別人可以導入

```bash
github.com/someuser/modname
github.com/someuser/modname/auth
```

能透過go install來安裝cmd/prog1和cmd/prog2

```ruby
$ go install github.com/someuser/modname/cmd/prog1@latest
$ go install github.com/someuser/modname/cmd/prog2@latest
```

以[Prometheus](https://github.com/prometheus/prometheus)這專案為例子

```bash
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](https://github.com/grafana/mimir/tree/80ee3c17ee134541904fcefc7e9b066b6288348d)，內部就導入很多Prometheus專案內的package。

但你不會看到Mimir去導入prometheus/internal底下的package，因為這些只能被prometheus專案內部所導入。

## 總結

官方這篇文章主要針對專案根目錄，internal目錄做說明，cmd目錄則是建議。 如果我們的專案需要提供一堆package供別專案導入時，有人也是會建議集中放到 `pkg` 目錄中，讓根目錄乾淨點，但也只是建議。

對我來說目錄結構反應的也是設計時的一個邊界，怎樣探索出邊界，不是按照這篇[Project布局](https://github.com/golang-standards/project-layout)來被強迫設計就這樣塞入，該repo的[issue](https://github.com/golang-standards/project-layout/issues/117)也在討論這份也不是標準Go專案布局。

更多的我想還是回歸場景與需求的設計，像是DDD中提到的subdomain與bounded context, 就是基於\`\` `業務層面` \`\`的設計布局，在基於這樣業務布局進行專案結構的布局設計，這樣能讓專案設計貼近於業務設計，減低認知與轉化負擔。

令一個層面是關於release，如果該專案內有些package對於其他專案項目有用處，為了方便管理發佈與版本控制，官方是建議拆分成獨立的專案進行管理。只有跟我們這包系統有緊密生命週期相關的package和command才集中在一個專案內，方便開發和部署。

# 參考資料

[Go doc Organizing a Go module](https://go.dev/doc/modules/layout)

[Go doc go1.4 Internal packages](https://go.dev/doc/go1.4#internalpackages)

[Go 项目目录该怎么组织？官方终于出指南了！](https://tonybai.com/2023/10/05/the-official-guide-of-organizing-go-project/)
