🎯 什麼情境該想到我
當你有一個命令式迴圈「一邊篩選、一邊轉換、一邊累加」,讀起來要在腦中模擬執行才懂資料怎麼變化的時候,想讓資料的流動一眼看清。
⚙️ 怎麼用(步驟 / 公式)
意圖:用集合管道(map / filter / reduce 等)取代迴圈,讓「資料從哪來、經過哪些處理、變成什麼」像一條由上而下的流水線一樣清楚,消除中途的暫存變數。
小步安全進行:
- 建立一個變數指向迴圈要走訪的集合(管道的起點)。
- 從迴圈頂端開始,把每一段行為逐一搬成一個管道操作:篩選 →
filter、轉換 →map、彙整 →reduce(或 slice、flatMap 等)。 - 每搬一段就把它從原迴圈移除,並執行一次測試。
- 全部搬完後移除整個迴圈。
★ 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 時常被要求拆掉。
🔗 相關工具
- 拆分迴圈 Split Loop(先把一個迴圈拆成各做一件事,較容易改成管道)
- 提煉函式 Extract Function(把整條管道包成一個有名字的查詢)
- 重構
- 拆分迴圈 —— ★ Go 裡的首選替代方案
- 迴圈 —— 觸發本手法的壞味道
- 管道與過濾器 —— 同一想法在系統/訊息層級
- 封裝集合 ——
iter.Seq真正該用的地方