Skip to main content

Command Palette

Search for a command to run...

Project Layout in Go

Updated
2 min read

剛入門任何一門程式語言開發的人, 應該大多都是參考各路大神們的專案或者公司的專案在學習模仿。 一開始印入眼簾的應該就是 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檔案。

演化路程如下圖

如果是純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比較是慣例命名。 這目錄中所有的檔案其package 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。這時就出現 internal 資料夾了。

Internal package

Go在1.4時加入了internal package的機制。 internal package除了不能讓外面的用戶直接import之外,代表著對其他module來說,該internal package對它們來說是不可見的,所以能做一層隔離,internal 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的internal只有package1這層以下的能導入,package2的internal也是,連main都無法導入到這兩個internal package。 但是main可以導入根目錄下的internal package。 package1能夠導入根目錄下的internal 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的認識後,就能將internal 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/modname
github.com/someuser/modname/auth
github.com/someuser/modname/hash

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

github.com/someuser/modname/auth/token

外部專案就是不能導入

github.com/someuser/modname/internal/trace

在這專案結構設計上,剛好internal 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/modname
github.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 项目目录该怎么组织?官方终于出指南了!

427 views

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read162
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read72
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read441
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts