🎯 什麼情境該想到我
當你「面對一個又長又難一眼看完、要不斷捲動的函式」的時候——尤其它內部其實包含好幾個各自獨立的小任務。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:函式越長越難理解,因為讀者得同時記住很多細節與中間狀態。真正該追求的是「意圖」與「實作」分離——函式名說出做什麼,函式體怎麼做。積極地把「一段做某件事的碼」抽成有名字的小函式,是最有效的整理。
通常用哪些重構手法對治:
- 提煉函式(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 的錯誤處理讓函式天生變長,那不是壞味道。
-
★ 表格測試的資料表、線性的初始化組裝,都不該拆。
-
★ 拆成一堆只呼叫一次、名字等於本體的私有方法是負收益——讀者要跳十次,而且每個方法都可能動到接收者狀態。
-
提煉方法比提煉純函式危險:方法可以讀寫接收者,簽章看不出副作用。能寫成純函式就寫成純函式。
-
先攤平再拆。對一個四層嵌套的函式直接提煉,你會提煉出需要五個參數的函式。先做 以衛述句取代嵌套條件式。
🔗 相關工具
- 重複程式碼 Duplicated Code(提煉函式同時消除重複)
- 過長參數列 Long Parameter List(切小函式時要留意參數不要爆炸)
- 註釋 Comments(區塊註釋常是「這裡該提煉一個函式」的提示)
- 重構
- 提煉函式 —— 主要對治手法
- 以衛述句取代嵌套條件式 —— 先攤平嵌套
- 拆分迴圈、拆分階段 —— 一個函式做兩件事時
- 引入參數物件、搬移函式 —— 提煉後參數會爆時
- 以多型取代條件式 —— 一大坨依型別的 switch
- 工具-函式短小且單一職責 —— 「拆到多細」的收手判準
- 工具-管理複雜度 —— 複雜度為什麼是根本問題