🎯 什麼情境該想到我
當你「想讓別人走訪你的集合,但不想暴露它內部到底是 slice、map、樹還是資料庫游標」的時候。
★ Go 1.23 的 range-over-func 徹底改寫了這個模式在 Go 裡的樣貌——這是本頁最重要的內容。
⚙️ 怎麼用
意圖
提供一種方法循序存取一個聚合物件中的各個元素,而又不暴露該物件的內部表示。
三個價值:
- 封裝內部結構(換實作不影響呼叫端)
- 統一走訪介面(slice、樹、資料庫游標用同一種方式走)
- ★ 惰性求值(不用一次把全部資料載入記憶體)
★ Go 1.23 之前:沒有統一慣例,各寫各的
// ① 回傳 slice —— 簡單,但★ 必須把全部載入記憶體
func (s *Store) All() []Item
// ② callback —— 可惰性,但★ 中途停止很醜、巢狀難讀
func (s *Store) Each(f func(Item) bool)
// ③ 游標物件 —— 像 sql.Rows,★ 呼叫端要記得 Close 與檢查 Err
rows, _ := db.Query(...)
defer rows.Close()
for rows.Next() {
rows.Scan(&x)
}
if err := rows.Err(); err != nil { ... } // ★ 漏掉這行是超常見的 bug★ Go 1.23 起:iter.Seq 與 range-over-func(2026-09-20 查證)
標準庫的定義:
package iter
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)寫一個迭代器:
func (t *Tree) All() iter.Seq[Item] {
return func(yield func(Item) bool) {
var walk func(*node) bool
walk = func(n *node) bool {
if n == nil { return true }
if !walk(n.left) { return false }
if !yield(n.item) { return false } // ★ yield 回 false = 呼叫端 break 了
return walk(n.right)
}
walk(t.root)
}
}使用它——就像走訪 slice 一樣:
for item := range t.All() { // ★ 這就是 range-over-func
if item.Bad { break } // ★ break 會讓 yield 回傳 false,迭代器自然收尾
process(item)
}★ 這解決了前面三種作法的所有問題:
- 惰性(不用全部載入)
break自然可用(不用發明「回傳 false 代表停止」的慣例)defer在迭代器內正常運作(可以安全地清理游標、關檔)- 和內建型別的
range語法完全一致
標準庫已全面採用:
func maps.Keys[...](m Map) iter.Seq[K] // 2026-09-20 查證
func maps.Values[...](m Map) iter.Seq[V]
func slices.Values[...](s S) iter.Seq[E]
func slices.All[...](s S) iter.Seq2[int, E]
// 配合收集函式
keys := slices.Sorted(maps.Keys(m)) // ★ 取出 key 並排序,一行★ slices.Sorted(maps.Keys(m)) 值得記起來——
它解決了「Go 的 map range 順序隨機」這個最常見的困擾(見 雜湊表)。
TypeScript 的長相:Symbol.iterator 與生成器
class Tree<T> {
*[Symbol.iterator](): Generator<T> { // ★ 語言內建
yield* this.walk(this.root);
}
private *walk(n: Node<T> | null): Generator<T> {
if (!n) return;
yield* this.walk(n.left);
yield n.item;
yield* this.walk(n.right);
}
}
for (const item of tree) { ... } // ★ 直接可用
const arr = [...tree]; // 展開非同步版本(Node 處理串流的主力):
async function* readLines(path: string): AsyncGenerator<string> {
const rl = readline.createInterface({ input: createReadStream(path) });
for await (const line of rl) yield line; // ★ 惰性,不會把整個檔案載入
}
for await (const line of readLines("big.log")) { ... }★ JS 的生成器比 Go 的
iter.Seq更早也更完整(支援雙向溝通、return、throw)。
Go 選擇了更簡單的 closure 形式——功能少一點,但編譯器能做更好的優化。
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
- ★ 小集合就直接回傳 slice。
[]Item更簡單、可索引、可重複走訪、可排序。
iter.Seq的價值在「大」與「惰性」,不是「比較潮」。 - ★
iter.Seq只能走一次(它是一個函式,每次呼叫重新執行)。
如果背後是網路游標或 channel,重複 range 的行為取決於實作——要在文件裡寫清楚。 - 錯誤處理要另外設計。
iter.Seq[V]沒有 error 的位置。三種做法: 沒有標準答案,但一定要選一個並寫進文件。iter.Seq2[V, error] // 每個元素帶 error func All() (iter.Seq[V], func() error) // 走完後查 err(類似 rows.Err()) *Iterator struct { ...; Err error } // 把 err 放在物件上 - 不要在 range-over-func 裡改動被走訪的集合。行為未定義(和 map 一樣)。
- ★ 惰性迭代器持有資源時要能正確釋放。
break時yield回 false,
迭代器函式應該正常返回並執行defer——不要在yield回 false 後還繼續做事。 - 效能:
iter.Seq有函式呼叫開銷。極熱的迴圈上,直接for i := range slice仍然最快。 - Go 1.23 之前的專案沒有這個。要確認
go.mod的版本。