🎯 什麼情境該想到我

當你有一個命令式迴圈「一邊篩選、一邊轉換、一邊累加」,讀起來要在腦中模擬執行才懂資料怎麼變化的時候,想讓資料的流動一眼看清。

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

意圖:用集合管道(map / filter / reduce 等)取代迴圈,讓「資料從哪來、經過哪些處理、變成什麼」像一條由上而下的流水線一樣清楚,消除中途的暫存變數。

小步安全進行:

  1. 建立一個變數指向迴圈要走訪的集合(管道的起點)。
  2. 從迴圈頂端開始,把每一段行為逐一搬成一個管道操作:篩選 → filter、轉換 → map、彙整 → reduce(或 slice、flatMap 等)。
  3. 每搬一段就把它從原迴圈移除,並執行一次測試。
  4. 全部搬完後移除整個迴圈。

★ before / after(TypeScript —— 這個手法的原生環境)

// before
const result = [];
for (const line of input) {
  const record = line.split(',');
  if (record[1] === 'India') result.push({ city: record[0], phone: record[2] });
}
 
// after ★ 每一步的意圖直接寫在臉上
const result = input
  .map(line => line.split(','))
  .filter(r => r[1] === 'India')
  .map(r => ({ city: r[0], phone: r[2] }));

★★ Go 的結論不一樣:這個手法在 Go 裡多半不該做

實測 Go 1.26.4(2026-09-25):標準庫沒有泛型的 Map / Filter / Reduce。
go doc slices 與 go doc maps 的完整清單裡只有這些和管道沾得上邊:

有的沒有的
slices.Values / slices.All / slices.Backward → iter.Seq★ Map
slices.Collect / slices.Sorted / slices.AppendSeq★ Filter(只有原地的 DeleteFunc)
slices.DeleteFunc / ContainsFunc / IndexFunc / Chunk★ Reduce
maps.Keys / maps.Values / maps.Collect

golang.org/x/exp/xiter(提案中的 Map/Filter/Reduce)不在標準庫。
所以在 Go 裡寫「管道」只有三種選擇,每一種都有代價:

// ① 手寫 iter.Seq 管道(Go 1.23+)—— 能做,但你得自己實作每個 operator
func Map[A, B any](s iter.Seq[A], f func(A) B) iter.Seq[B] {
    return func(yield func(B) bool) {
        for v := range s { if !yield(f(v)) { return } }
    }
}
result := slices.Collect(Map(slices.Values(input), parse))
// ★ 代價:每個 operator 一個泛型函式 + 每層一個 closure。讀者要先看懂你的 Map 實作
 
// ② 第三方(samber/lo 之類)
// ★ 代價:多一個依賴,而且 Go 社群普遍不愛在核心程式碼用
 
// ③ ★★ 就寫迴圈(Go 慣用做法)
var result []Record
for _, line := range input {
    r := strings.Split(line, ",")
    if r[1] != "India" { continue }              // ★ continue 就是 filter
    result = append(result, Record{r[0], r[2]})  // ★ append 就是 map
}

★ 判準:在 Go 裡,「拆分迴圈 + 提煉函式」幾乎總是比「管道化」好。
for … { if … { continue } … } 對 Go 讀者就是最清楚的寫法;
硬套管道會讓人先去讀你的 Map 實作,反而增加閱讀成本。

iter.Seq 真正的用途是別的:把「如何走訪」封裝起來(走 tree、分頁 API、DB cursor),
而不是取代簡單的 slice 迴圈。見 封裝集合 的方案②。

什麼時候在 Go 裡仍然值得管道化

  • 來源是無限或很大、不想先全部載入記憶體(iter.Seq 是惰性的)。
  • 走訪邏輯本身很複雜(樹、圖、分頁),值得封裝成 iter.Seq 讓呼叫端用 range。
  • 已經在用 iter.Seq 的 API(maps.Keys、bufio.Scanner 的現代包裝)。

⚠️ TS/JS 的管道也有代價

// ★ 三個 map/filter = 三次完整遍歷 + 三個中間陣列
arr.map(f).filter(g).map(h)
// 百萬級資料要注意;小資料完全無感

還有一個常見錯誤:

// ★ async 不能直接進管道
const users = ids.map(async id => await load(id));   // ★ 拿到 Promise[] 不是 User[]
const users = await Promise.all(ids.map(id => load(id)));   // ✅(但會同時打 N 個請求)

🧪 我實際套用的紀錄

  • (待填)

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

  • 管道太長、巢狀太深時反而更難讀,適度即可,不要為了無迴圈而硬串。

  • 效能敏感的熱點迴圈,管道可能產生額外中間集合,需要時再權衡。

  • 反向而言,若團隊不熟悉函式式風格,過度使用會增加閱讀門檻。

  • ★★ 在 Go 裡這個手法多半不該做(標準庫無 Map/Filter/Reduce,2026-09-25 查證)。先考慮 拆分迴圈 + 提煉函式。

  • 需要提早中斷(break)、複雜狀態累積、或要同時看前後元素的邏輯,管道會比迴圈難懂。

  • ★ 管道裡不能放會 error 的操作(Go 沒有 Result 型別)。Map(s, func(x) (Y, error)) 在管道中無處可放 err —— 這是 Go 不適合管道最根本的原因。

  • 除錯困難:管道中間沒有可以下中斷點的變數,堆疊裡是一串匿名函式。

  • TS:每個 map/filter 是一次完整遍歷加一個中間陣列。大資料要注意。

  • TS:async 函式不能直接放進 map,會得到 Promise[]。

  • 團隊熟悉度是真的考量。Go 團隊裡一個自製的 Map/Filter/Reduce 工具包,review 時常被要求拆掉。

🔗 相關工具