角色: 你是一位重視簡潔與明確性的 Go 架構師。你只在模式能解決實際問題時才套用它們 — 不會為了展示技術深度而使用 — 並且你會反對過早的抽象。
模式:
- 設計模式 — 建立新的 API、套件或應用程式結構時:在提出模式前,先詢問開發者關於他們的架構偏好;偏好能滿足需求的最小模式。
- 審查模式 — 審查現有程式碼的設計問題時:掃描
init()濫用、無限制資源、缺少逾時和隱含的全域狀態;在建議重構前先回報發現。
社群預設值。 一個明確取代
samber/cc-skills-golang@golang-design-patterns技能的企業技能具有優先權。
Go 設計模式與慣用語
適用於正式環境程式碼的慣用 Go 模式。關於錯誤處理的詳細資訊,請參閱 samber/cc-skills-golang@golang-error-handling 技能;關於 Context 傳播,請參閱 samber/cc-skills-golang@golang-context 技能;關於結構體/介面設計,請參閱 samber/cc-skills-golang@golang-structs-interfaces 技能。
最佳實踐摘要
- 建構器(Constructor)應該使用函數選項(functional options) — 它們在 API 演進時更具擴展性(每個選項一個函數,無破壞性變更)
- 函數選項在驗證可能失敗時必須回傳錯誤 — 在建構時捕獲錯誤配置,而非在執行時期
- 避免使用
init()— 它會隱含執行、無法回傳錯誤、使測試難以預測。應使用明確的建構器 - 列舉(Enum)應該從 1 開始(或將 Unknown 哨兵值設為 0)— Go 的零值會靜默地作為第一個列舉成員傳遞
- 錯誤情況必須優先處理並提早回傳 — 保持快樂路徑平坦
- Panic 是給錯誤(bug)用的,不是給預期的錯誤(error)用的 — 呼叫端可以處理回傳的錯誤;panic 會使程序崩潰
- 開啟後立即
defer Close()— 後續的程式碼變更可能會意外跳過清理 - 使用
runtime.AddCleanup而非runtime.SetFinalizer— finalizer 不可預測且可能復活物件 - 每個外部呼叫都應該有逾時設定 — 一個慢速的上游會無限期地掛起你的 goroutine
- 限制所有東西(池大小、佇列深度、緩衝區)— 無限制的資源會持續成長直到崩潰
- 重試邏輯必須在嘗試之間檢查 context 的取消狀態
- 在迴圈中進行字串串接時使用
strings.Builder→ 參閱samber/cc-skills-golang@golang-code-style - string 與 []byte:對可變操作和 I/O 使用
[]byte,對顯示和鍵(key)使用string— 轉換會產生記憶體分配 - 迭代器(Iterator, Go 1.23+):用於惰性求值 — 避免將所有內容載入記憶體
- 串流傳輸大量資料 — 載入數百萬行會導致 OOM;串流可保持記憶體使用量恆定
- 對靜態資源使用
//go:embed— 在編譯時期嵌入,消除執行時期的檔案 I/O 錯誤 - 對金鑰/令牌使用
crypto/rand—math/rand是可預測的 → 參閱samber/cc-skills-golang@golang-security - 正則表達式必須在套件層級編譯一次 — 編譯是 O(n) 且會分配記憶體
- 編譯時期的介面檢查:
var _ Interface = (*Type)(nil) - 少量重寫程式碼 > 一個大型依賴 — 每個依賴都會增加攻擊面與維護負擔
- 為可測試性而設計 — 接受介面、注入依賴
建構器模式:函數選項 vs 建構器(Builder)
函數選項(建議使用)
type Server struct {
addr string
readTimeout time.Duration
writeTimeout time.Duration
maxConns int
}
type Option func(*Server)
func WithReadTimeout(d time.Duration) Option {
return func(s *Server) { s.readTimeout = d }
}
func WithWriteTimeout(d time.Duration) Option {
return func(s *Server) { s.writeTimeout = d }
}
func WithMaxConns(n int) Option {
return func(s *Server) { s.maxConns = n }
}
func NewServer(addr string, opts ...Option) *Server {
// 預設選項
s := &Server{
addr: addr,
readTimeout: 5 * time.Second,
writeTimeout: 10 * time.Second,
maxConns: 100,
}
for _, opt := range opts {
opt(s)
}
return s
}
// 使用方式
srv := NewServer(":8080",
WithReadTimeout(30*time.Second),
WithMaxConns(500),
)
建構器應該使用函數選項 — 它們在 API 演進時更具擴展性且需要更少的程式碼。只有在不同配置步驟之間需要複雜驗證時,才使用建構器模式。
建構器與初始化
避免 init() 與可變的全域變數
init() 會隱含執行、增加測試難度,並建立隱藏的依賴:
- 多個
init()函數按照宣告順序執行,跨檔案時則按照檔案名稱的字母順序 — 這非常脆弱 - 無法回傳錯誤 — 失敗時必須 panic 或使用
log.Fatal - 在
main()和測試之前執行 — 副作用會使測試難以預測
// 錯誤 — 隱藏的全域狀態
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
}
// 正確 — 明確初始化,可注入
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
列舉:從 1 開始
零值應代表無效/未設定的狀態:
type Status int
const (
StatusUnknown Status = iota // 0 = 無效/未設定
StatusActive // 1
StatusInactive // 2
StatusSuspended // 3
)
編譯正則表達式一次
// 正確 — 在套件層級編譯一次
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func ValidateEmail(email string) bool {
return emailRegex.MatchString(email)
}
對靜態資源使用 //go:embed
import "embed"
//go:embed templates/*
var templateFS embed.FS
//go:embed version.txt
var version string
編譯時期的介面檢查
→ 有關 var _ Interface = (*Type)(nil) 模式,請參閱 samber/cc-skills-golang@golang-structs-interfaces。
錯誤流程模式
錯誤情況必須優先處理並提早回傳 — 將快樂路徑保持在最小的縮排層級。→ 有關完整模式與範例,請參閱 samber/cc-skills-golang@golang-code-style。
何時使用 Panic 與回傳錯誤
- 回傳錯誤:網路失敗、檔案未找到、無效輸入 — 任何呼叫端可以處理的情況
- Panic:在理應不可能的地方出現 nil 指標、違反不變量、在初始化時使用的
Must*建構器 .Close()/Flush()錯誤:唯讀的清理通常可以使用defer f.Close(),但寫入/刷新資源必須在持久性重要時報告關閉或刷新錯誤
資料處理
string vs []byte vs []rune
| 類型 | 預設用於 | 使用時機 |
|---|---|---|
string | 所有情況 | 不可變、安全、UTF-8 |
[]byte | I/O | 寫入 io.Writer、建立字串、可變操作 |
[]rune | Unicode 操作 | len() 必須表示字元數,而非位元組數 |
避免重複轉換 — 每次轉換都會分配記憶體。保持在一個類型中,直到你需要另一種類型。
用於大量資料的迭代器與串流
使用迭代器(Go 1.23+)和串流模式來處理大型資料集,避免將所有內容載入記憶體。對於服務之間的大型傳輸(例如,從資料庫到 HTTP 的 100 萬行),進行串流處理以防止 OOM。
有關程式碼範例,請參閱資料處理模式。
資源管理
開啟後立即 defer Close() — 不要等待、不要忘記:
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 就在這裡,不是 50 行之後
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
有關優雅關閉、資源池和 runtime.AddCleanup,請參閱資源管理。
韌性與限制
為每個外部呼叫設定逾時
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := httpClient.Do(req.WithContext(ctx))
重試與 Context 檢查
重試邏輯必須在嘗試之間檢查 ctx.Err(),並透過 select 搭配 ctx.Done() 使用指數/線性退避。長時間執行的迴圈必須定期檢查 ctx.Err()。→ 參閱 samber/cc-skills-golang@golang-context 技能。
資料庫模式
→ 參閱 samber/cc-skills-golang@golang-database 技能,了解 sqlx/pgx、交易、可為空的欄位、連線池、儲存庫介面、測試等。
架構
詢問開發者他們偏好的架構:整潔架構(Clean Architecture)、六角形架構(Hexagonal)、領域驅動設計(DDD)或扁平佈局。不要在小專案上強加複雜的架構。
無論採用何種架構,核心原則如下:
- 保持領域純淨 — 領域層沒有框架依賴
- 快速失敗 — 在邊界進行驗證,信任內部程式碼
- 使非法狀態無法表示 — 使用型別來強制執行不變量
- 遵守 12-factor app 原則 — → 參閱
samber/cc-skills-golang@golang-project-layout
詳細指南
| 指南 | 範圍 |
|---|---|
| 架構模式 | 高階原則、每種架構適用的時機 |
| 整潔架構 | 使用案例、依賴規則、分層適配器 |
| 六角形架構 | 連接埠與適配器、領域核心隔離 |
| 領域驅動設計 | 聚合、值物件、限界上下文 |
程式碼哲學
- 避免重複程式碼 — 但不要過早抽象
- 最小化依賴 — 少量重寫 > 一個大型依賴
- 為可測試性而設計 — 接受介面、注入依賴、保持函數純淨
交叉引用
- → 參閱
samber/cc-skills-golang@golang-data-structures技能,了解資料結構選擇、內部實作和 container/ 套件 - → 參閱
samber/cc-skills-golang@golang-error-handling技能,了解錯誤包裝、哨兵錯誤和單一處理規則 - → 參閱
samber/cc-skills-golang@golang-structs-interfaces技能,了解介面設計與組合 - → 參閱
samber/cc-skills-golang@golang-concurrency技能,了解 goroutine 生命週期與優雅關閉 - → 參閱
samber/cc-skills-golang@golang-context技能,了解逾時與取消模式 - → 參閱
samber/cc-skills-golang@golang-project-layout技能,了解架構與目錄結構 - → 參閱
samber/cc-skills-golang@golang-refactoring技能,了解如何在現有程式碼庫中安全地分階段遷移到這些模式之一(選項結構、DI、消費者端介面)