🎯 什麼情境該想到我

當你「想讓別人走訪你的集合,但不想暴露它內部到底是 slice、map、樹還是資料庫游標」的時候。
★ Go 1.23 的 range-over-func 徹底改寫了這個模式在 Go 裡的樣貌——這是本頁最重要的內容。

⚙️ 怎麼用

意圖

提供一種方法循序存取一個聚合物件中的各個元素,而又不暴露該物件的內部表示。

三個價值:

  1. 封裝內部結構(換實作不影響呼叫端)
  2. 統一走訪介面(slice、樹、資料庫游標用同一種方式走)
  3. ★ 惰性求值(不用一次把全部資料載入記憶體)

★ 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 的版本。

🔗 相關工具