Iterators
ทำไมต้องมี iterator?
หัวข้อที่มีชื่อว่า “ทำไมต้องมี iterator?”ก่อน Go 1.23 function ที่ผลิตลำดับข้อมูลมีทางเลือกที่ไม่สวยอยู่สามทาง: สร้างแล้วคืน slice ทั้งก้อน (allocate ทุกอย่างตั้งแต่แรก), ส่ง channel ออกมา (ต้องเปิด goroutine และช้า) หรือคิด type Next() ของตัวเองที่ range ไม่รู้จัก Go 1.23 เพิ่ม range-over-func: for วนซ้ำบน function ที่มีรูปแบบเฉพาะได้ และ standard library ได้ package iter มาตั้งชื่อรูปแบบเหล่านั้น
ผลลัพธ์คือลำดับข้อมูลแบบ lazy ที่ไม่ allocate และใช้กับ syntax ของ loop ที่คุณรู้จักอยู่แล้ว
iter.Seq และ iter.Seq2
หัวข้อที่มีชื่อว่า “iter.Seq และ iter.Seq2”package iter นิยาม generic function type ไว้สองแบบ:
package iter
type Seq[V any] func(yield func(V) bool)type Seq2[K, V any] func(yield func(K, V) bool)iterator คือ function ที่รับ callback ชื่อ yield มันเรียก yield หนึ่งครั้งต่อ element; yield คืน false เมื่อผู้ใช้ต้องการหยุด และ iterator ต้อง return ทันทีในกรณีนั้น Seq2 yield เป็นคู่ — index กับ value หรือ key กับ value — ล้อกับ range แบบสองตัวแปร
การเขียน push iterator
หัวข้อที่มีชื่อว่า “การเขียน push iterator”นี่คือ counter ที่ yield 0..n-1 และหยุดก่อนจบเมื่อถูกขอ:
func Count(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < n; i++ { if !yield(i) { return // consumer broke out of the loop } } }}
for v := range Count(5) { if v == 3 { break // yield returns false inside Count, which returns } fmt.Println(v)}break, return และ continue ใน loop body ทำงานตามที่คาดทุกอย่าง: compiler แปลง body ให้เป็น function yield และแปลง control flow ให้เอง iterator เห็นแค่ yield คืน false
เพราะลำดับถูกผลิตตามความต้องการ Count(1_000_000_000) จึงไม่มีต้นทุนจนกว่าจะเริ่มวน — และถ้าหยุดหลังสามตัว ก็ผลิตแค่สามตัว
การใช้ iterator ซ้ำหลัง early exit จะ panic
หัวข้อที่มีชื่อว่า “การใช้ iterator ซ้ำหลัง early exit จะ panic”กฎ yield == false จากหัวข้อก่อนหน้าไม่ใช่แค่คำแนะนำ — iterator ที่ไม่สนใจมันแล้วยังเรียก yield ต่อหลังจากที่มันคืน false ไปแล้วครั้งหนึ่งคือ runtime error ไม่ใช่แค่ bug ทาง logic:
func Count(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < n; i++ { yield(i) // BUG: ignores yield's return value and keeps going } }}ตรวจสอบแล้วบน 1.26.5 การเรียกโค้ดนี้แล้ว break ออกจาก loop range ก่อนจบจะ panic:
panic: runtime error: range function continued iteration after function for loop body returned falseGo runtime ติดตามว่า loop body ขอหยุดไปแล้วหรือยังและบังคับใช้กฎนี้ — iterator ที่เขียนเองทุกตัวต้องเช็กค่าที่ yield คืนกลับมาและ return ทันทีเมื่อมันเป็น false เหมือนที่ Count ทำไว้ก่อนหน้านี้ในบทเรียน
defer ข้างใน range-over-func body
หัวข้อที่มีชื่อว่า “defer ข้างใน range-over-func body”defer ที่เขียนไว้ข้างใน body ของ for ... range someIterator ไม่ได้รันตอนจบแต่ละ iteration — มันรันตอนที่ function ที่ครอบมันอยู่ return เพราะ loop body ถูก compile เป็น closure ที่ iterator เรียกซ้ำ ๆ และ defer ทุกตัวข้างในนั้นถูกผูกกับ function ที่มี for loop อยู่ ไม่ใช่ผูกกับ closure เอง:
func process() { for v := range Count(3) { defer fmt.Println("deferred for", v) // queues up, doesn't run yet fmt.Println("processing", v) } fmt.Println("loop finished")}ตรวจสอบแล้วบน 1.26.5 โค้ดนี้พิมพ์ทั้งสามบรรทัด "processing" และ "loop finished" ก่อน บรรทัด "deferred for" ใด ๆ — deferred call ทั้งสามตัวสะสมข้าม iteration และรันตามลำดับ LIFO ก็ต่อเมื่อ process เอง return โค้ดที่คาดหวังว่า defer ข้างใน range-over-func loop จะทำงานเหมือน defer ตอนจบ iteration เดียว (เหมือนเป็น body ของ for loop ธรรมดา) จะแปลกใจ — ให้ใช้ helper function ต่อ iteration หรือเรียก cleanup ตรง ๆ แทน defer ถ้าต้องการให้มันเกิดขึ้นทุกรอบของ loop
Iterator บน type ของคุณเอง
หัวข้อที่มีชื่อว่า “Iterator บน type ของคุณเอง”รูปแบบ idiomatic คือ method ชื่อ All, Keys, Values หรือทำนองนั้น ที่คืน iterator แทน slice:
type List[T any] struct{ items []T }
func (l *List[T]) All() iter.Seq2[int, T] { return func(yield func(int, T) bool) { for i, v := range l.items { if !yield(i, v) { return } } }}
for i, v := range list.All() { fmt.Println(i, v)}caller ไม่ต้องแตะ backing slice และคุณเปลี่ยน representation ทีหลังได้อิสระ — linked list, database cursor, HTTP API แบบแบ่งหน้า — โดย loop ไม่ต้องเปลี่ยนเลย
Single-pass เทียบกับ re-iterable
หัวข้อที่มีชื่อว่า “Single-pass เทียบกับ re-iterable”ไม่มีอะไรใน type ของ iter.Seq บังคับให้ลำดับต้องรองรับการ range ซ้ำมากกว่าหนึ่งครั้ง — ขึ้นอยู่กับสิ่งที่ closure ทำทั้งหมด List.All() ด้านบนเป็น re-iterable: ทุกการเรียกสร้าง closure ใหม่ที่อ่าน l.items ตั้งแต่ต้น ส่วนลำดับที่อิงกับสิ่งที่ถูก “ใช้ไป” ตอนอ่าน — channel, network connection, bufio scanner — เป็น single-pass: เมื่อดึงหมดแล้ว การ range ค่า iter.Seq ตัวเดิมอีกครั้งจะไม่ให้อะไรเลย เงียบ ๆ:
func FromChannel(ch <-chan int) iter.Seq[int] { return func(yield func(int) bool) { for v := range ch { if !yield(v) { return } } }}ตรวจสอบแล้วบน 1.26.5: การ range seq := FromChannel(ch) ตัวเดิมสองครั้งจะพิมพ์ค่าของ channel ในรอบแรก แล้วไม่พิมพ์อะไรเลยในรอบที่สอง — ไม่มี error ไม่มี panic แค่ loop ที่ว่างเปล่า เพราะ channel ถูกดึงจนหมดไปแล้ว เมื่อ function คืนค่า iter.Seq ไม่มีอะไรใน type บอกว่ามัน range ซ้ำได้ปลอดภัยหรือเปล่า ต้องเช็ก doc comment หรือ source เหมือนที่ทำกับ function ที่คืน *bufio.Reader
Standard library พูดภาษา iterator
หัวข้อที่มีชื่อว่า “Standard library พูดภาษา iterator”ตั้งแต่ Go 1.23 package slices และ maps (ส่วน strings/bytes ตั้งแต่ Go 1.24) ทั้ง ผลิต และ ใช้ iterator:
import ("maps"; "slices"; "strings")
// consume: turn an iterator into a sliceevens := slices.Collect(filter(Count(10), isEven)) // []intkeys := slices.Sorted(maps.Keys(m)) // sorted keys, one line
// produce: iterate without copyingfor i, v := range slices.All(xs) { /* index, value */ }for v := range slices.Values(xs) { /* value only */ }for k, v := range maps.All(m) { /* key, value */ }
// strings (Go 1.24): lazy splittingfor line := range strings.Lines(text) { /* each line, newline kept */ }for part := range strings.SplitSeq(csv, ",") { /* no []string allocated */ }slices.Collect, slices.Sorted, slices.SortedFunc, maps.Collect และ maps.Insert คือสะพานกลับสู่ข้อมูลจริง ส่วน strings.SplitSeq และ strings.FieldsSeq คือฝาแฝดแบบ lazy ของ Split และ Fields
helper อีกไม่กี่ตัวที่ควรรู้จักชื่อไว้:
| Function | ผลิตอะไร |
|---|---|
slices.Backward(s) | iter.Seq2[int, E] — คู่ index/value เรียงจาก index มากไปน้อย |
slices.Chunk(s, n) | iter.Seq[Slice] — sub-slice ต่อเนื่องขนาดไม่เกิน n element |
maps.Values(m) | iter.Seq[V] — เฉพาะ value เท่านั้น ลำดับไม่กำหนด |
maps.Keys(m) | iter.Seq[K] — เฉพาะ key เท่านั้น ลำดับไม่กำหนด |
maps.Keys คืน iter.Seq ไม่ใช่ slice
หัวข้อที่มีชื่อว่า “maps.Keys คืน iter.Seq ไม่ใช่ slice”ก่อน Go 1.23 maps.Keys (ใน golang.org/x/exp/maps) คืน []K แต่ version ใน standard library คืน iter.Seq[K] แทน — เป็น function ไม่ใช่ slice — ดังนั้นโค้ดที่เขียนตามรูปแบบเก่าจะพังตอน compile time แทนที่จะทำงานผิดเงียบ ๆ:
m := map[string]int{"z": 1, "a": 2}sort.Strings(maps.Keys(m)) // BUG: maps.Keys returns iter.Seq[string], not []stringตรวจสอบแล้วบน 1.26.5 นี่คือ compile error:
cannot use maps.Keys(m) (value of func type iter.Seq[string]) as []string value in argument to sort.Stringsทางแก้ปรากฏอยู่แล้วก่อนหน้านี้ในบทเรียน: slices.Sorted(maps.Keys(m)) รวบรวม iterator ให้เป็น []string ที่ sort แล้วในการเรียกครั้งเดียว แทนที่รูปแบบเก่าสองขั้นตอน “รวบรวมเป็น slice แล้วค่อย sort.Strings” ไปเลย
filter เล็ก ๆ ไว้อ้างอิง — combinator แบบนี้ประกอบกันได้เป็นธรรมชาติ:
func filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] { return func(yield func(T) bool) { for v := range seq { if keep(v) && !yield(v) { return } } }}Pull iterator
หัวข้อที่มีชื่อว่า “Pull iterator”range-over-func เป็นโมเดลแบบ push: ฝั่งผู้ผลิตเป็นคนขับ ส่วน loop body ของผู้ใช้เป็น callback เมื่อต้องดึงทีละ element เอง — เช่น merge ลำดับที่ sort แล้วสองชุด — iter.Pull แปลง push iterator ให้เป็นคู่ next()/stop():
next, stop := iter.Pull(Count(3))defer stop()v, ok := next() // 0, trueiter.Pull ใช้ coroutine อยู่ข้างใน จึงช้ากว่า range ธรรมดา ใช้เฉพาะเมื่อจำเป็นต้องได้รูปแบบ pull จริง ๆ
flowchart LR
subgraph Push["push (for ... range seq)"]
Producer["iterator drives"] --> Body["loop body runs as yield callback"]
end
subgraph Pull["pull (iter.Pull)"]
Caller["caller drives"] -->|"next()"| Coroutine["iterator's coroutine resumes to the next yield"]
Coroutine -->|"value, true"| Caller
Caller -->|"stop()"| Coroutine
Coroutine -->|"releases resources"| Done["coroutine exits"]
end iter.Pull ต้องการ stop()
หัวข้อที่มีชื่อว่า “iter.Pull ต้องการ stop()”contract อย่างเป็นทางการจาก doc ของ package iter: stop “must be called when the caller is no longer interested in next values and next has not yet signaled that the sequence is over” ในทางปฏิบัติ iter.Pull รัน push iterator เดิมบน coroutine ที่ suspend ตัวเองระหว่างแต่ละค่า stop() คือสิ่งที่บอกให้ coroutine นั้น unwind และปล่อยสิ่งที่มันถืออยู่ — ถ้าไม่เรียก การใช้ next() แล้วเลิกกลางทางจะทำให้ coroutine ค้างอยู่แบบนั้นตลอดไป การเรียก stop ซ้ำหลายครั้งถูกต้องเสมอ และเรียก next หลัง stop ก็ถูกต้องเช่นกัน (มันจะคืน zero value กับ false ต่อไป) — นี่คือเหตุผลที่ defer stop() ทันทีหลัง iter.Pull เป็น pattern มาตรฐาน เหมือน reflex เดียวกับ defer resp.Body.Close() หรือ defer f.Close()
แนวทางการออกแบบ
หัวข้อที่มีชื่อว่า “แนวทางการออกแบบ”- คืน slice เมื่อข้อมูลเป็น slice อยู่แล้ว และมีขนาดเล็ก iterator ไม่ได้เพิ่มอะไรตรงนั้น
- คืน iterator เมื่อลำดับมีขนาดใหญ่ ไม่มีที่สิ้นสุด ผลิตแพง มี I/O อยู่ข้างหลัง หรือเมื่อต้องการซ่อน representation
- เคารพ
yield == falseเสมอ iterator ที่ยังเรียกyieldต่อหลังจากมันคืนfalseจะ panic - ตั้งชื่อ accessor ว่า
All,Keys,Values,Backwardให้ตรงกับ standard library - iterator เป็น function ธรรมดา: ไม่ได้ปลอดภัยต่อ goroutine โดยอัตโนมัติ และถือทุกอย่างที่ closure จับไว้
ลองรันดู
หัวข้อที่มีชื่อว่า “ลองรันดู”ทุกอย่างข้างบนรวมอยู่ในโปรแกรมเดียวที่รันได้ — Count, filter, List.All และตัวเชื่อม slices/maps:
package main
import ( "fmt" "iter" "maps" "slices")
// Count yields 0, 1, …, n-1 lazily.func Count(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < n; i++ { if !yield(i) { return // the consumer broke out of its loop } } }}
func filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] { return func(yield func(T) bool) { for v := range seq { if keep(v) && !yield(v) { return } } }}
type List[T any] struct{ items []T }
func (l *List[T]) All() iter.Seq2[int, T] { return func(yield func(int, T) bool) { for i, v := range l.items { if !yield(i, v) { return } } }}
func main() { isEven := func(n int) bool { return n%2 == 0 } fmt.Println(slices.Collect(filter(Count(10), isEven)))
for v := range Count(100) { if v > 2 { break // Count returns as soon as yield reports false } fmt.Println("got", v) }
list := &List[string]{items: []string{"a", "b", "c"}} for i, v := range list.All() { fmt.Println(i, v) }
m := map[string]int{"z": 26, "a": 1, "m": 13} fmt.Println(slices.Sorted(maps.Keys(m)))} อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อโค้ดที่ AI generate เขียนหรือใช้ iterator ให้มองหาสิ่งเหล่านี้ก่อนเชื่อมัน:
- iterator ที่เขียนเองเพิกเฉยต่อค่าที่
yieldคืนกลับมาแล้วยัง loop ต่อหลังจากมันคืนfalseหรือเปล่า? นั่นคือ runtime panic ไม่ใช่ bug ที่เงียบ sort.Strings,sort.Intsหรือคล้ายกันถูกเรียกตรง ๆ กับผลลัพธ์ของmaps.Keys/maps.Values/slices.Valuesหรือเปล่า? สิ่งเหล่านั้นคืนiter.Seqไม่ใช่ slice — compile ไม่ผ่านdeferข้างใน range-over-func loop body ถูกสมมติว่ารันครั้งเดียวต่อ iteration สำหรับ cleanup ที่ต้องเกิดทุกรอบของ loop หรือเปล่า?- โค้ด range ค่า
iter.Seqเดิมซ้ำเป็นครั้งที่สอง โดยคาดหวังผลลัพธ์เดิม ทั้งที่ source ข้างใต้ (channel, scanner, network read) ดึงได้แค่ครั้งเดียวหรือเปล่า? iter.Pullถูกใช้โดยไม่มีdefer stop()ครอบทุก return path รวมถึง early return และ error หรือเปล่า?
🐛 หา bug ในโค้ดนี้
func Count(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < n; i++ { yield(i) } }}
func main() { for v := range Count(5) { if v == 2 { break } fmt.Println(v) }}เฉลย
iterator ของ Count เรียก yield(i) แต่ไม่เช็กค่าที่คืนกลับมาเลย จึงยังเรียก yield ต่อทุก i ที่เหลือ แม้ loop จะ break ออกไปแล้วก็ตาม ตรวจสอบแล้วบน 1.26.5: โค้ดนี้ panic ด้วย panic: runtime error: range function continued iteration after function for loop body returned false ทางแก้คือ if !yield(i) { return } เหมือนที่ Count เวอร์ชันถูกต้องก่อนหน้านี้ในบทเรียนทำ
package main
import ( "fmt" "iter")
func Count(n int) iter.Seq[int] { return func(yield func(int) bool) { for i := 0; i < n; i++ { if !yield(i) { return } } }}
// process shows that defer inside a range-over-func body queues up for the// whole enclosing function, not per iteration -- it runs LIFO when process// itself returns.func process() { for v := range Count(3) { defer fmt.Println("deferred for", v) fmt.Println("processing", v) } fmt.Println("loop finished")}
// FromChannel is single-pass: once the channel is drained, ranging over the// same Seq again yields nothing.func FromChannel(ch <-chan int) iter.Seq[int] { return func(yield func(int) bool) { for v := range ch { if !yield(v) { return } } }}
func main() { process()
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 close(ch) seq := FromChannel(ch)
fmt.Println("first pass:") for v := range seq { fmt.Println(v) } fmt.Println("second pass on the same (now-drained) sequence:") for v := range seq { fmt.Println(v) } fmt.Println("second pass produced nothing -- the channel has no more values")
// iter.Pull for manual, one-at-a-time control. next, stop := iter.Pull(Count(3)) defer stop() // required: releases the iterator's underlying coroutine for { v, ok := next() if !ok { break } fmt.Println("pulled", v) }}