🎯 什麼情境該想到我

當你「看到一段有獨立意圖的程式碼,得停下來想一下才知道它在做什麼」的時候——把它抽成一個有名字的函式,用名字直接說明意圖,讀的人看名字就懂,不必再讀細節。

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

意圖:把一段程式碼抽成獨立函式,以函式名稱解釋這段碼的「意圖」而非「做法」。

動作要點(小步安全進行):

  1. 新建一個函式,用「它做什麼」而非「它怎麼做」來命名(就算實作只有一行也值得)。
  2. 把要提煉的程式碼複製進新函式。
  3. 檢查被提煉的碼用到哪些原本作用域的變數:只讀取的當參數傳入;在段內宣告只在段內用的,留在新函式當區域變數。
  4. 若被提煉碼會修改某個外部變數,看是否能改成「回傳該變數的值」。修改超過一個變數時,通常要先做別的重構(如以查詢取代暫時變數、拆分變數)再提煉。
  5. 原處改成呼叫新函式,編譯並測試。
  6. 檢查其他地方是否也有相同或相似的碼,可改呼叫這個新函式。

★ before / after(Go)

before——一段「得靠註解才讀得懂」的碼夾在流程中間:

func PrintOwing(inv *Invoice) {
    printBanner()
 
    // 算未付總額
    outstanding := 0.0
    for _, o := range inv.Orders {
        outstanding += o.Amount
    }
 
    // 印明細
    fmt.Println("name:", inv.Customer)
    fmt.Println("amount:", outstanding)
}

after——註解變成函式名,PrintOwing 只剩「步驟清單」:

func PrintOwing(inv *Invoice) {
    printBanner()
    outstanding := outstandingAmount(inv)   // ★ 查詢:有回傳值、無副作用
    printDetails(inv, outstanding)          // ★ 命令:有副作用、無回傳值
}

★ 切點的判準:切出來的東西要嘛是「查詢」要嘛是「命令」,不要兩者都是(見 將查詢與修改分離)。
切完發現新函式既回傳值又改狀態 → 切點選錯了。

★★ Go 特有的三個陷阱:提煉會真的改變行為

以下三點都在 Go 1.26.4 實測過(2026-09-25)。

① 選取範圍含 defer → 執行時機變了

func before() { open(); defer close(); work() }   // ★ 印 open → work → close
 
func step()  { open(); defer close() }            // ★ defer 被一起搬走
func after() { step(); work() }                   // ★ 印 open → close → work

defer 綁的是「函式回傳」,不是「區塊結束」。 一旦被提煉,資源在 work() 執行之前就釋放了。
測試通常抓不到(close 沒報錯,只是早了),但線上會變成 use-after-close。
→ 提煉前先看選取範圍裡有沒有 defer。

② recover() 不能被「再往內提煉一層」

func handle() { if r := recover(); r != nil { log(r) } }
 
func ok()  { defer handle(); panic("x") }              // ★ 攔得到
func bad() { defer func(){ handle() }(); panic("x") }  // ★ 攔不到,panic 繼續往外炸

recover() 只在「正在 panic 的那個 frame 直接 defer 的函式」裡有效。
所以「把整個 deferred 函式提煉成 defer handle()」可以;
但原本是 defer func(){ …recover()… }(),你把裡面那段再抽成一次呼叫 → 靜默失效。

③ 選取範圍含 return / break / continue
新函式沒辦法代替呼叫端 return。兩條路:改成回傳 (值, bool) 讓呼叫端自己決定,
或先做 以衛述句取代嵌套條件式 把 return 移出選取範圍。

工具支援(gopls 官方文件,2026-09-25 查證)

  • 有 refactor.extract.function / .method / .variable / .variable-all / .constant / .toNewFile。
  • ⚠️ 官方自承 Extract 演算法「遠不如 Rename 與 Inline 嚴謹」,並列出 golang/go#66289、#65944、#63394、#61496 等已知缺陷。→ 提煉完一定要看 diff。
  • ⚠️ Extract 與 Inline 容易掉註解——Go 的 AST 把註解放在 side table 而非節點上(go.dev/issue/20744)。
  • ★ Extract parameter struct(即 引入參數物件)到今天仍未實作(go.dev/issue/65552)→ 那手法在 Go 只能手動做。

TypeScript

VS Code 內建的 Extract to function 較可靠(TS 的 AST 帶註解)。但有自己的陷阱:
提煉含 await 的碼時新函式必須是 async,呼叫端漏了 await
→ 拿到的是 Promise 而不是值,而型別若被推成 any 或 unknown 還不會報錯。

🧪 我實際套用的紀錄

  • (待填)

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

  • 判準是「意圖與實作的距離」,不是長度:若函式本體和名字一樣直白、沒帶來額外理解價值,就別提煉,甚至該反向做「行內函式」。

  • 反向手法:行內函式 Inline Function。

  • ★ 選取範圍含 defer / recover / return 時,提煉不是行為保持的(見上)。這是 Go 的提煉與 JS 版《重構》最大的落差。

  • ★ 參數爆多 = 切點錯了。超過 3 個就停下來:通常該把函式搬到資料所在的型別上(搬移函式),或先 引入參數物件。

  • 提煉出來的測試輔助函式要加 t.Helper(),否則失敗訊息指向 helper 內部而不是呼叫它的那行測試。

  • 不要為了「函式要短」而提煉。抽出一堆只被呼叫一次、名字等於本體的 doStepA(),只會讓人在檔案裡跳來跳去。

  • 提煉不等於編譯器會幫你 inline 回去。實測 Go 1.26.4,內聯預算是 80:cannot inline f: function too complex: cost 163 exceeds budget 80(go build -gcflags='-m -m' 可看)。熱路徑上提煉可能真的多一次呼叫——但先 profile 再說。

🔗 相關工具