🎯 什麼情境該想到我
當你「盯著一個迴圈,卻得逐行追蹤才能看出它到底在篩選、轉換還是彙總」的時候——尤其這些動作全擠在同一個迴圈裡。
⚙️ 怎麼用(步驟 / 公式)
這味道是什麼徵兆:傳統迴圈把「做什麼」藏在控制流程細節裡。集合管道(collection pipeline,如 map / filter / reduce)能讓每一步的意圖直接寫在臉上:這一段是過濾、那一段是轉換,一眼就懂。
通常用哪些重構手法對治:
- 以管道取代迴圈(Replace Loop with Pipeline):把迴圈拆解成一連串具名的管道操作,讓資料的流動與每步意圖清晰可讀。
作者坦言,隨著語言支援一級函式,迴圈本身已逐漸成為一種味道。
★★ Go 的結論和原書相反:這個「味道」在 Go 多半不成立
Fowler(2018 年第二版)說「隨著一級函式普及,迴圈本身已逐漸成為一種味道」。
那是針對 JavaScript 說的。在 Go 裡,情況完全不同:
實測 Go 1.26.4(2026-09-25)——標準庫沒有泛型的 Map / Filter / Reduce:
slices 有的 | 沒有的 |
|---|---|
Values / All / Backward / Chunk → iter.Seq | ★ Map |
Collect / Sorted / AppendSeq | ★ Filter(只有原地的 DeleteFunc) |
DeleteFunc / ContainsFunc / IndexFunc | ★ Reduce |
maps.Keys / maps.Values / maps.Collect |
golang.org/x/exp/xiter(提案中的 Map/Filter/Reduce)不在標準庫。
// ★ Go 慣用寫法 —— continue 就是 filter,append 就是 map
var out []Record
for _, line := range input {
r := strings.Split(line, ",")
if r[1] != "India" { continue }
out = append(out, Record{r[0], r[2]})
}★★ 對 Go 讀者來說,這就是最清楚的寫法。
硬套管道要先自己實作Map/Filter,讀者得先讀你的實作——閱讀成本反而增加。
★ 而且 Go 沒有Result型別,管道裡無處放error—— 這是最根本的限制。
★ 在 Go 裡,迴圈真正的味道是這五種
// ① ★ 一個迴圈同時做好幾件不相關的事 → [[拆分迴圈]] → [[提煉函式]]
for _, p := range people {
if p.Age < youngest { youngest = p.Age }
totalSalary += p.Salary // ★ 和上一行毫無關係
}
// ② ★ 四層嵌套 → [[以衛述句取代嵌套條件式]](用 continue)
for _, o := range orders {
if o.Paid {
for _, l := range o.Lines {
if l.Taxable { … } // ★ 真正的邏輯在第四層
}
}
}
// ③ ★★ 迴圈裡有 I/O —— N+1 查詢,這是最該處理的一種
for _, id := range ids { u, _ := db.LoadUser(ctx, id) } // ★ 打了 N 次 DB
// → 改成批次:db.LoadUsers(ctx, ids)
// ④ ★ 在迴圈裡 append 卻沒預配容量
out := []Item{} // ★ 每次成長都重新配置 + 複製
out := make([]Item, 0, len(in)) // ✅ 一次配置
// golangci-lint 的 prealloc 抓得到
// ⑤ ★ 手寫的迴圈可以被標準庫取代
for i := range s { if s[i] == v { … } } // → slices.Index / slices.Contains# golangci-lint 相關(2026-09-24 查證)
# prealloc —— slice 可以預配容量
# intrange —— for i := 0; i < n; i++ 可以寫成 for i := range n(Go 1.22+)
# copyloopvar —— 不必要的迴圈變數複製(Go 1.22 之後 i := i 是多餘的)
# modernize —— 用新版語言/標準庫特性簡化
# nestif —— 深層嵌套★ Go 1.22 / 1.23 改變了什麼
// ★ Go 1.22:迴圈變數改為「每輪新建」—— 經典的 closure bug 消失了
for _, v := range items {
go func() { use(v) }() // ★ 1.22 前:所有 goroutine 拿到同一個 v(最後一個)
} // 1.22 後:各自拿到自己那輪的 v
// ⚠️ 取決於 go.mod 裡的 go 指令版本,不是編譯器版本
// ★ Go 1.22:for i := range 10 —— 整數 range
// ★ Go 1.23:range-over-func,讓 iter.Seq 可以直接 range
for c := range slices.Values(courses) { … }★ iter.Seq 真正的用途不是取代 slice 迴圈,而是把「如何走訪」封裝起來
(走樹、分頁 API、DB cursor)——見 封裝集合。
★★ 什麼時候迴圈完全不是問題
- 就是要走一遍做一件事——這是絕大多數情況。
- 需要提早
break、需要同時看前後元素、需要複雜狀態累積 → 管道會更難懂。 - 熱路徑:手寫迴圈沒有 closure 呼叫開銷(雖然 Go 會內聯小函式,預算是 80)。
- 要處理 error:迴圈裡
if err != nil { return err }很自然;管道裡沒地方放。
TypeScript:反過來,管道才是慣用寫法
const result = input.map(parse).filter(isIndia).map(toRecord);
// ⚠️ 代價:三次完整遍歷 + 兩個中間陣列(小資料無感,百萬級要注意)
// ⚠️ async 不能直接進管道:
const users = await Promise.all(ids.map(id => load(id))); // ★ 但會同時打 N 個請求🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
管道並非萬靈丹;極長或需要提前中斷、複雜狀態累積的邏輯,硬套管道可能更難懂。
-
團隊對管道風格的熟悉度、以及效能敏感場景,都要一併考量。
-
★★ 在 Go 裡「迴圈是味道」多半不成立。標準庫無
Map/Filter/Reduce(2026-09-25 實測),且管道裡沒地方放error。 -
★ Go 裡真正該處理的是:一個迴圈做多件事、深層嵌套、迴圈裡有 I/O(N+1)、沒預配容量。
-
★ 迴圈裡有 I/O 才是最該處理的一種——那不是可讀性問題,是效能事故。
-
需要
break、要看前後元素、要累積複雜狀態 → 迴圈更清楚。 -
★ Go 1.22 的迴圈變數語義改變取決於
go.mod的go指令版本,不是編譯器版本。 -
iter.Seq的用途是封裝「如何走訪」(樹、分頁、cursor),不是取代簡單的 slice 迴圈。 -
TS 的管道要注意遍歷次數與
async:map(async …)會得到Promise[]。
🔗 相關工具
- 重複程式碼 Duplicated Code(相似迴圈可用管道統一表達)
- 過長函式 Long Function(管道化常讓函式更短更清楚)
- 重構
- 拆分迴圈 —— ★ Go 裡的首選對治
- 以管道取代迴圈 —— 管道化的完整判準(含為什麼在 Go 多半不做)
- 以衛述句取代嵌套條件式 —— 用 continue 攤平嵌套
- 過長函式 —— 深層嵌套的一般形式
- 封裝集合 ——
iter.Seq真正該用的地方 - 工具-迴圈不變式 —— 手寫迴圈的正確性論證