🎯 什麼情境該想到我
分工:這張是「函式該多大、該做幾件事」的判準。怎麼偵測過長函式、Go 裡哪三種長函式不要拆 → 過長函式;拆的機械步驟 → 提煉函式;參數太多的 Go 解法(option struct / variadic options)→ 工具-Go函式參數太多怎麼辦。
當一個函式很長、要捲很久、或你發現它「先…再…然後…」做很多事時。
⚙️ 怎麼用
- 只做一件事:函式內的敘述應在同一抽象層次;「還能再被有意義地拆出一個函式」就代表它不只做一件事。
- 越短越好:理想幾行;巢狀縮排別太深。
- 參數越少越好:0–2 個最佳,3 個以上考慮包成物件;避免旗標參數(傳 boolean 通常代表函式做了兩件事)。
- 無副作用:函式名說做什麼,就別偷偷做別的。
★ 「一件事」的可操作判準:單一抽象層次
「只做一件事」太模糊,實際能用的判準是:函式裡的每一行,都在同一個抽象層次上。
// ✗ 混了三層:業務流程、SQL 細節、字串格式
func CloseMonth(db *sql.DB, month time.Time) error {
rows, err := db.Query(`SELECT id, amount FROM invoices WHERE month = $1 AND status = 'open'`, month)
if err != nil { return err }
defer rows.Close()
var total int64
for rows.Next() { /* … scan、累加 … */ }
subject := fmt.Sprintf("[%s] 月結 %d 元", month.Format("2006-01"), total)
return sendMail("finance@corp", subject)
}
// ✓ 讀起來是一份「步驟清單」,每一步的細節在下一層
func CloseMonth(ctx context.Context, repo InvoiceRepo, n Notifier, month time.Time) error {
invoices, err := repo.OpenInvoices(ctx, month)
if err != nil {
return fmt.Errorf("load open invoices: %w", err)
}
return n.MonthClosed(ctx, month, sumAmount(invoices))
}自我檢查:把函式唸出來,如果是「先 A、再 B、然後 C」,而 A/B/C 是同一層級的業務步驟 → 這是一件事(編排);如果中間夾著「然後開一個 for 迴圈把每列掃進 struct」→ 層次混了。
這也叫降層規則(step-down rule):程式從上往下讀,每個函式後面緊接著它呼叫的下一層函式,像讀文章的大綱→段落→細節。
★ 旗標參數:一個函式做兩件事的最明顯訊號
// ✗ 呼叫端:Render(doc, true) —— true 是什麼?
func Render(doc Doc, draft bool) string { if draft { … } else { … } }
// ✓ 兩件事就是兩個函式;共用部分抽成第三個
func RenderDraft(doc Doc) string { … }
func RenderFinal(doc Doc) string { … }不能拆(兩條路徑 90% 相同)時,至少換成具名型別:Render(doc, ModeDraft),呼叫端讀得懂。
★ 命令與查詢分離(CQS)
一個函式要嘛回答問題(查詢,無副作用),要嘛改變狀態(命令),不要兩者都做。
func (c *Cart) Total() int如果順便把過期商品移掉 → 呼叫Total()兩次結果不同,測試、除錯、快取全亂。- Go 的慣例例外:
m, ok := <-ch、v, loaded := m.LoadOrStore(k, v)這類「原子地查+改」是為了併發正確性刻意合一,因為拆開就有 race。
★ 兩派的真實分歧(別當教條)
| 《程式碼整潔之道》 | Go 社群常見的做法 | |
|---|---|---|
| 長度 | 「越短越好」,幾行為佳 | 線性、無分支的 50 行 handler 可以接受 |
| 理由 | 函式名就是文件 | 跳來跳去讀 10 個 3 行小函式,比從上讀到下更累 |
| 共識 | 都同意:巢狀深、抽象層混雜、旗標參數是問題 |
真正的判準是讀者要同時記住多少東西(見 工具-管理複雜度),不是行數。拆出來的函式如果名字說不清、或只被呼叫一次且讀者一定得跳進去看,那就是拆過頭。
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
-
拆函式讓抽象層次一致,不是為短而短;配合 工具-提煉函式 手法安全地拆。
-
「無副作用」在 Go 最常見的違規:名為
Validate/Check/Get的函式偷偷寫入 log 以外的狀態(補預設值、改欄位)。要補預設值就叫WithDefaults()並回傳新值。
🔗 相關工具
- 工具-有意義的命名 —— 配套:函式拆小之後,名字是否說得清它做什麼就是驗收標準
- 工具-提煉函式 —— 執行手法,這張講「該多小」,它講「怎麼拆出去」
- 工具-一次只做一件事 —— 同一原則的段落層說法,可用來判斷函式內部還該不該再切