🎯 什麼情境該想到我

當你「面對一個又長又難一眼看完、要不斷捲動的函式」的時候——尤其它內部其實包含好幾個各自獨立的小任務。

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

這味道是什麼徵兆:函式越長越難理解,因為讀者得同時記住很多細節與中間狀態。真正該追求的是「意圖」與「實作」分離——函式名說出做什麼,函式體怎麼做。積極地把「一段做某件事的碼」抽成有名字的小函式,是最有效的整理。

通常用哪些重構手法對治:

  • 提煉函式(Extract Function):最主力手法,把每個可命名的意圖抽成小函式。
  • 以查詢取代暫時變數(Replace Temp with Query):清掉妨礙提煉的中間變數。
  • 分解條件式(Decompose Conditional):把龐大的 if/else 判斷與分支各自抽成有名字的函式。
  • 參數過多時搭配引入參數物件、保持物件完整。

判斷準則不是行數,而是「意圖與實作的語義距離」:只要一段碼需要註釋解釋,就適合抽成以該註釋為名的函式。

★ 怎麼具體認出來:四個可量測的訊號(都不是「行數」)

訊號怎麼量門檻參考
★ 縮排深度最深幾層 if/for超過 3 層就該處理
★ 註解密度有沒有「以下這段在做 X」型的註解每一個這種註解就是一次 提煉函式 的機會
區域變數數量活過 20 行以上的變數有幾個超過 5~6 個就難以追蹤
認知複雜度gocognit(比 gocyclo 更貼近人的感受)依團隊校準
# golangci-lint 相關 linter(2026-09-24 查證的官方清單)
#   funlen    —— 函式長度(行數與敘述數)
#   gocyclo   —— 循環複雜度
#   gocognit  —— ★ 認知複雜度,對「嵌套」的懲罰比 gocyclo 重,更貼近可讀性
#   cyclop    —— 函式與套件的循環複雜度
#   nestif    —— ★ 深層嵌套的 if
#   maintidx  —— 可維護性指數
#   lll       —— 行長

★ 優先看 gocognit 和 nestif,不要只看 funlen。 一個 120 行的 switch 很好讀;
一個 30 行、四層嵌套、七個變數互相影響的函式才是災難。

★★ 判準:「意圖與實作的距離」,不是行數

★ Fowler 的實際準則是「每當你需要註解來說明一段程式在做什麼,就把它提煉成函式,並用註解的內容當名字」。

// ★ 這 100 行是好的:一個扁平的 switch,讀者只會看他關心的那一個 case
func (h *Handler) Route(e Event) error {
    switch e.Kind {
    case KindCreated: …
    case KindUpdated: …
    // … 二十個 case
    }
}
 
// ★ 這 30 行是壞的:三層嵌套 + 五個互相影響的變數
func settle(inv *Invoice) error {
    for _, l := range inv.Lines {
        if l.Taxable {
            for _, d := range l.Discounts {
                if d.Active && !d.Expired {
                    // 真正的邏輯埋在第四層

★ Go 特有:這三種長函式不要拆

// ① ★ 錯誤處理讓所有 Go 函式都變長 —— 那不是壞味道
f, err := os.Open(p)
if err != nil { return err }
defer f.Close()
// …每一步都配三行
 
// ② ★ 表格測試的表格 —— 200 行的 cases 是正常的
func TestParse(t *testing.T) {
    cases := []struct{ name, in string; want int }{ /* 50 列 */ }
    for _, c := range cases { … }     // ★ 要拆的是「執行邏輯」,不是「資料」
}
 
// ③ ★ 線性的初始化/組裝(wire-up)
func main() {
    cfg := loadConfig()
    db := openDB(cfg)
    …  // 50 行,但完全沒有分支,從上讀到下
}
//    ★ 判準:沒有分支、沒有迴圈、變數各自獨立 → 拆成 setupA()/setupB() 只會增加跳轉

對治的決策順序

有「以下這段在做 X」的註解? ─── 是 ──▶ [[提煉函式]](用註解內容當名字)
      │否
嵌套超過 3 層? ─── 是 ──▶ [[以衛述句取代嵌套條件式]](先攤平,再看還長不長)
      │否
一個迴圈裡在做好幾件事? ─── 是 ──▶ [[拆分迴圈]] → [[提煉函式]]
      │否
參數超過 3 個、或提煉後參數會爆? ─── 是 ──▶ [[引入參數物件]] 或 [[搬移函式]]
      │否
前半段在解析、後半段在計算? ─── 是 ──▶ [[拆分階段]]
      │否
一大坨 switch 依型別分派,而且同組 switch 出現多次? ─── 是 ──▶ [[以多型取代條件式]]

⚠️ 拆過頭的代價是真的

// ★ 十個只被呼叫一次、名字等於本體的函式
func (s *S) Run() { s.step1(); s.step2(); s.step3() }
func (s *S) step1() { … }    // ★ 讀者要在檔案裡跳十次,而且每次都得檢查有沒有動到 s 的狀態

★ 拆函式的收益來自「名字帶來理解」。名字沒帶來理解時,拆就是純成本——
而且在 Go 裡更明顯:拆出的方法都能讀寫接收者,讀者無法只看簽章判斷它改了什麼。
→ 純函式(吃參數、回傳值)比方法更值得拆。

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 別只看行數就切;抽出來的每個小函式都要有清楚的名字與單一意圖。

  • 若抽得太碎、彼此傳遞一堆參數,反而更難讀,考慮改用參數物件或提煉類別。

  • ★ 行數不是判準。扁平的 100 行 switch 好讀,嵌套四層的 30 行不好讀。看 gocognit 與 nestif,不要只看 funlen。

  • ★ Go 的錯誤處理讓函式天生變長,那不是壞味道。

  • ★ 表格測試的資料表、線性的初始化組裝,都不該拆。

  • ★ 拆成一堆只呼叫一次、名字等於本體的私有方法是負收益——讀者要跳十次,而且每個方法都可能動到接收者狀態。

  • 提煉方法比提煉純函式危險:方法可以讀寫接收者,簽章看不出副作用。能寫成純函式就寫成純函式。

  • 先攤平再拆。對一個四層嵌套的函式直接提煉,你會提煉出需要五個參數的函式。先做 以衛述句取代嵌套條件式。

🔗 相關工具