🎯 什麼情境該想到我

分工:這張是「函式該多大、該做幾件事」的判準。怎麼偵測過長函式、Go 裡哪三種長函式不要拆 → 過長函式;拆的機械步驟 → 提煉函式;參數太多的 Go 解法(option struct / variadic options)→ 工具-Go函式參數太多怎麼辦。

當一個函式很長、要捲很久、或你發現它「先…再…然後…」做很多事時。

⚙️ 怎麼用

  1. 只做一件事:函式內的敘述應在同一抽象層次;「還能再被有意義地拆出一個函式」就代表它不只做一件事。
  2. 越短越好:理想幾行;巢狀縮排別太深。
  3. 參數越少越好:0–2 個最佳,3 個以上考慮包成物件;避免旗標參數(傳 boolean 通常代表函式做了兩件事)。
  4. 無副作用:函式名說做什麼,就別偷偷做別的。

★ 「一件事」的可操作判準:單一抽象層次

「只做一件事」太模糊,實際能用的判準是:函式裡的每一行,都在同一個抽象層次上。

// ✗ 混了三層:業務流程、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() 並回傳新值。

🔗 相關工具