🎯 什麼情境該想到我

當你面對「if-else 巢狀好幾層」,得一路縮排讀進去才找得到真正的主要邏輯,而且分不清哪些是特例、哪些是正常流程的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:對每個「特例/異常狀況」在函式開頭就用一句提早 return(衛述句)把它擋掉,讓後面的程式碼專心表達正常流程。衛述句傳達的訊息是「這種情況不是本函式的重點,處理掉就閃人」,把巢狀結構攤平成一連串平行的檢查。

小步安全進行:

  1. 挑最外層的條件,把它改寫成衛述句:條件成立就直接 return(或丟例外)。
  2. 執行測試。
  3. 由外而內,逐一把其他巢狀條件也改成衛述句,每改一個就測一次。
  4. 全部攤平後,若有多個衛述句回傳相同結果,考慮用合併條件式收斂。

★ 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 裡它是反模式。

🔗 相關工具