🎯 什麼情境該想到我

當你「盯著一個迴圈,卻得逐行追蹤才能看出它到底在篩選、轉換還是彙總」的時候——尤其這些動作全擠在同一個迴圈裡。

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

這味道是什麼徵兆:傳統迴圈把「做什麼」藏在控制流程細節裡。集合管道(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[]。

🔗 相關工具