🎯 什麼情境該想到我
當你面對「if-else 巢狀好幾層」,得一路縮排讀進去才找得到真正的主要邏輯,而且分不清哪些是特例、哪些是正常流程的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:對每個「特例/異常狀況」在函式開頭就用一句提早 return(衛述句)把它擋掉,讓後面的程式碼專心表達正常流程。衛述句傳達的訊息是「這種情況不是本函式的重點,處理掉就閃人」,把巢狀結構攤平成一連串平行的檢查。
小步安全進行:
- 挑最外層的條件,把它改寫成衛述句:條件成立就直接 return(或丟例外)。
- 執行測試。
- 由外而內,逐一把其他巢狀條件也改成衛述句,每改一個就測一次。
- 全部攤平後,若有多個衛述句回傳相同結果,考慮用合併條件式收斂。
★ before / after(Go)
// before:三層嵌套,真正的邏輯埋在最裡面,而且要一直追「現在在哪個分支」
func payAmount(e Employee) Payment {
var result Payment
if e.IsSeparated {
result = Payment{0, "SEP"}
} else {
if e.IsRetired {
result = Payment{0, "RET"}
} else {
// ★ 真正的邏輯在這裡,第三層
result = computeNormalPay(e)
}
}
return result
}
// after:例外情況一行一個「先走掉」,主線回到最外層
func payAmount(e Employee) Payment {
if e.IsSeparated { return Payment{0, "SEP"} } // ★ 衛述句
if e.IsRetired { return Payment{0, "RET"} } // ★ 衛述句
return computeNormalPay(e) // ★ 主線,零縮排
}★★ 判準:這兩個分支是「對等」的,還是「例外 vs 正常」?
| 用 if/else | ★ 用衛述句 | |
|---|---|---|
| 兩個分支都是正常路徑 | ✅ if isWeekend { … } else { … } | ❌ |
| 一個是例外/提早結束 | ❌ | ✅ 例外先走掉,主線留在最外層 |
★ 這就是 Fowler 說的「if/else 表達的是同等重要的兩條路;衛述句說的是『這不正常,趕快離開』」。
用錯了不是風格問題——它誤導讀者對程式重要性的判斷。
★ Go 天生就是衛述句風格
// ★ Go 的錯誤處理本身就是衛述句,所以這個手法在 Go 是「回到慣用寫法」而不是「引入新風格」
func Load(ctx context.Context, id string) (*User, error) {
if id == "" { return nil, errors.New("empty id") }
row, err := db.QueryRow(ctx, q, id)
if err != nil { return nil, fmt.Errorf("query: %w", err) }
…
}Google 的 Go 風格指南明確要求:把錯誤處理放在 if 裡提早回傳,
讓「正常路徑」保持在最左邊的縮排層級(happy path 靠左)。
原文的說法是「if err != nil { …; return } 之後直接寫正常流程,不要用 else 把正常路徑縮排進去」(見 Google Go 風格指南)。
→ 讀者只要垂直掃過最左邊那一欄,就看得到整個函式在幹嘛。
Go 的三種「先走掉」:
return v, nil // 函式:return
continue // ★ 迴圈內:continue 就是衛述句
break★ 反轉條件的機械步驟(最容易寫錯的地方)
// before
if okA {
if okB {
doIt()
}
}
// ★ 一次只反轉一層,每次跑測試
if !okA { return }
if !okB { return }
doIt()反轉複合條件時要用德摩根定律,這是最常見的錯誤來源:
!(a && b) == !a || !b
!(a || b) == !a && !b
if a && b { doIt() }
if !a || !b { return } // ✅
if !a && !b { return } // ★★ 錯了 —— 少走了一半的情況
doIt()→ 不要用腦算。用工具:gopls 有 refactor.rewrite.invertIf(2026-09-25 查證),
它會幫你正確反轉。反轉完一定要跑測試。
⚠️ Go 特有:衛述句和 defer 的互動
func process(path string) error {
f, err := os.Open(path)
if err != nil { return err }
defer f.Close() // ★ 必須在錯誤檢查「之後」
if !valid(f) { return errInvalid } // ★ 這裡 return,f.Close() 仍會執行 —— defer 保護了所有出口
…
}★ defer 是衛述句的最佳拍檔:加入衛述句就是「增加函式的出口」,
而在其他語言裡出口越多、資源洩漏風險越高;Go 的 defer 讓出口數量完全無關。
⚠️ 但 defer 必須放在錯誤檢查之後——f 是 nil 時 defer f.Close() 會 panic。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
若 if 與 else 兩條路「地位相當、都是正常流程」(不是主線 vs. 特例),就不適合硬改成衛述句——那樣反而扭曲了對等的邏輯。
-
團隊若堅持「單一出口(single return)」規範,需先取得共識;本手法刻意採用多重 return。
-
★ 兩個分支同等重要時不要用衛述句。
if/else表達「兩條對等的路」,衛述句表達「例外,趕快離開」——用錯會誤導讀者。 -
★★ 反轉複合條件時的德摩根定律是最常見的 bug 來源:
!(a && b)是!a || !b,不是!a && !b。用 gopls 的refactor.rewrite.invertIf,不要手算。 -
一次反轉一層,每層跑一次測試。 一口氣攤平三層然後測試紅了,你不知道錯在哪。
-
衛述句超過五、六個 → 函式責任太多。接著做 提煉函式(把驗證抽成
validate())或 引入參數物件(讓驗證變成型別的責任)。 -
defer要放在錯誤檢查之後,否則對 nil 資源呼叫Close()。 -
不要為了「只有一個出口」而拒絕衛述句。單一出口原則來自沒有
defer/finally的年代;在 Go 裡它是反模式。
🔗 相關工具
- 分解條件式 Decompose Conditional(另一種整理條件的手法,處理單一複雜條件)
- 合併條件式 Consolidate Conditional Expression(多個衛述句結果相同時可再合併)
- 重構
- 分解條件式 —— 衛述句的條件太長時給它一個名字
- 合併條件式 —— 反方向;多個衛述句後果相同時才合併
- 提煉函式 —— 衛述句太多時
- Google Go 風格指南 —— 官方風格指南對「錯誤流程往外推」的明確要求
- 工具-Go可讀性五原則 —— 縮排深度與可讀性
- 過長函式 —— 深層嵌套常是它的表徵