🎯 什麼情境該想到我
當你「看到一段有獨立意圖的程式碼,得停下來想一下才知道它在做什麼」的時候——把它抽成一個有名字的函式,用名字直接說明意圖,讀的人看名字就懂,不必再讀細節。
⚙️ 怎麼用(步驟 / 公式)
意圖:把一段程式碼抽成獨立函式,以函式名稱解釋這段碼的「意圖」而非「做法」。
動作要點(小步安全進行):
- 新建一個函式,用「它做什麼」而非「它怎麼做」來命名(就算實作只有一行也值得)。
- 把要提煉的程式碼複製進新函式。
- 檢查被提煉的碼用到哪些原本作用域的變數:只讀取的當參數傳入;在段內宣告只在段內用的,留在新函式當區域變數。
- 若被提煉碼會修改某個外部變數,看是否能改成「回傳該變數的值」。修改超過一個變數時,通常要先做別的重構(如以查詢取代暫時變數、拆分變數)再提煉。
- 原處改成呼叫新函式,編譯並測試。
- 檢查其他地方是否也有相同或相似的碼,可改呼叫這個新函式。
★ 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 → workdefer 綁的是「函式回傳」,不是「區塊結束」。 一旦被提煉,資源在 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 版《重構》最大的落差。 -
提煉出來的測試輔助函式要加
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 再說。
🔗 相關工具
- 行內函式 Inline Function(本手法的反向操作)
- 以查詢取代暫時變數 Replace Temp with Query(先消除暫時變數,能讓提煉更順)
- 重構
- 將查詢與修改分離 —— 切點的判準:一個函式別同時是查詢和命令
- 以衛述句取代嵌套條件式 —— 選取範圍裡有
return時的前置處理 - 搬移函式 —— 提煉後參數太多,代表該連資料一起搬
- 工具-提煉函式 —— 同一手法的即用速查卡
- 工具-小步重構 —— 每一步都跑測試的節奏
- 工具-函式短小且單一職責 —— 「要拆到多細」的收手判準
- 工具-Go表格測試與子測試 ——
t.Helper()的用法