Skip to content

Iterator

ใน Go คำสั่งที่ใช้สำหรับการวนซ้ำโครงสร้างข้อมูลเฉพาะคือ for range ในบทก่อนหน้าได้แนะนำการใช้งานบางส่วนไปแล้ว มันสามารถใช้งานได้กับโครงสร้างข้อมูลที่สร้าง sẵnในภาษาเท่านั้น

  • อาร์เรย์
  • สไลซ์
  • สตริง
  • map
  • chan
  • ค่าจำนวนเต็ม

การใช้งานเช่นนี้ไม่มีความยืดหยุ่น ไม่สามารถขยายได้ และไม่รองรับประเภทที่กำหนดเองเกือบทั้งหมด แต่หลังจากการอัปเดต Go1.23 คำสั่ง for range รองรับ range over func ทำให้การสร้าง iterator ที่กำหนดเองเป็นไปได้

ทำความรู้จัก

ต่อไปนี้จะแนะนำ iterator ผ่านตัวอย่างหนึ่ง ไม่ทราบว่าทุกท่านยังจำตัวอย่าง การใช้ closure แก้ปัญหาฟีโบนัชชี ที่อธิบายในหัวข้อฟังก์ชันหรือไม่ โค้ดการนำไปใช้เป็นดังนี้

go
func Fibonacci(n int) func() (int, bool) {
  a, b, c := 1, 1, 2
  i := 0
  return func() (int, bool) {
    if i >= n {
      return 0, false
    } else if i < 2 {
      f := i
      i++
      return f, true
    }

    a, b = b, c
    c = a + b
    i++

    return a, true
  }
}

เราสามารถเปลี่ยนมันเป็น iterator ได้ ดังแสดงด้านล่าง จะเห็นว่าจำนวนโค้ดลดลงบ้าง

go
func Fibonacci(n int) func(yield func(int) bool) {
  a, b, c := 0, 1, 1
  return func(yield func(int) bool) {
    for range n {
      if !yield(a) {
        return
      }
      a, b = b, c
      c = a + b
    }
  }
}

Iterator ของ Go เป็นสไตล์ range over func เราสามารถใช้คำสั่ง for range ได้โดยตรง ใช้งานสะดวกกว่าของเดิม

go
func main() {
    n := 8
  for f := range Fibonacci(n) {
    fmt.Println(f)
  }
}

ผลลัพธ์เป็นดังนี้

0
1
1
2
3
5
8
13

ดังแสดงด้านบน iterator คือฟังก์ชัน closure ที่รับฟังก์ชัน callback เป็นพารามิเตอร์ คุณอาจเห็นคำว่า yield ในนั้น ผู้ที่เคยเขียน python ควรคุ้นเคยดี มันคล้ายกับ generator ใน python Iterator ของ Go ไม่ได้เพิ่มคำสั่งหรือคุณสมบัติไวยากรณ์ใดๆ ในตัวอย่างด้านบน yield เป็นเพียงฟังก์ชัน callback ไม่ใช่คำสั่ง ทางการใช้ชื่อนี้เพื่อความสะดวกในการเข้าใจ

Pushing Iterator

สำหรับนิยามของ iterator เราสามารถพบคำอธิบายต่อไปนี้ในไลบรารี iter

An iterator is a function that passes successive elements of a sequence to a callback function, conventionally named yield.

Iterator คือฟังก์ชันที่ส่งองค์ประกอบของลำดับทีละตัวให้กับฟังก์ชัน callback โดยปกติเรียกว่า yield

สิ่งที่เราสามารถเข้าใจได้อย่างชัดเจนจากนี้คือ iterator คือฟังก์ชันที่รับฟังก์ชัน callback เป็นพารามิเตอร์ ในระหว่างกระบวนการวนซ้ำจะส่งองค์ประกอบของลำดับทีละตัวให้กับฟังก์ชัน callback yield ในตัวอย่างก่อนหน้าเราใช้งาน iterator ดังนี้

go
for f := range Fibonacci(n) {
    fmt.Println(f)
}

ตามนิยามทางการ การใช้ iterator Backward ด้านบนเทียบเท่ากับโค้ดด้านล่างนี้

go
Fibonacci(n)(func(f int) bool {
    fmt.Println(f)
    return true
})

Body ของลูปคือฟังก์ชัน callback yield ของ iterator เมื่อฟังก์ชันส่งคืน true iterator จะดำเนินการวนซ้ำต่อ มิฉะนั้นจะหยุด

นอกจากนี้ ไลบรารีมาตรฐาน iter ยังกำหนดประเภทของ iterator iter.Seq ประเภทของมันคือฟังก์ชัน

go
type Seq[V any] func(yield func(V) bool)

type Seq2[K, V any] func(yield func(K, V) bool)

ฟังก์ชัน callback ของ iter.Seq รับพารามิเตอร์เพียงตัวเดียว ดังนั้นเมื่อวนซ้ำ for range จะมีค่าส่งกลับเพียงตัวเดียว ดังนี้

go
for v := range iter {
  // body
}

ฟังก์ชัน callback ของ iter.Seq2 รับพารามิเตอร์สองตัว ดังนั้นเมื่อวนซ้ำ for range จะมีค่าส่งกลับสองตัว ดังนี้

go
for k, v := range iter {
  // body
}

แม้ว่าไลบรารีมาตรฐานจะไม่ได้กำหนด Seq ที่มี 0 พารามิเตอร์ แต่สิ่งนี้ได้รับอนุญาตโดยสมบูรณ์ มันเทียบเท่ากับ

go
func(yield func() bool)

ใช้งานดังนี้

go
for range iter {
  // body
}

จำนวนพารามิเตอร์ของฟังก์ชัน callback ได้เพียง 0 ถึง 2 ตัว หากมากกว่านี้จะไม่สามารถผ่านการคอมไพล์ได้

กล่าวโดยสรุป Body ของลูปใน for range คือฟังก์ชัน callback yield ของ iterator for range ส่งคืนค่ากี่ตัว ฟังก์ชัน yield ที่สอดคล้องกันก็มีพารามิเตอร์เท่านั้นตัว ในแต่ละรอบของการวนซ้ำ iterator จะเรียกฟังก์ชัน yield ซึ่งก็คือการดำเนินการโค้ดใน body ของลูป โดยการส่งองค์ประกอบในลำดับให้กับฟังก์ชัน yield Iterator แบบ主动ส่งองค์ประกอบนี้เราเรียกว่า pushing iterator ตัวอย่างทั่วไปคือ foreach ในภาษาอื่น เช่น js

javascript
let arr = [1, 2, 3, 4, 5];
arr
  .filter((e) => e % 2 === 0)
  .forEach((e) => {
    console.log(e);
  });

รูปแบบการแสดงใน Go คือการส่งคืนองค์ประกอบที่ถูกวนซ้ำโดย range

go
for index, value := range iterator() {
  fmt.Println(index, value)
}

ในบางภาษา (เช่น Java) มันมีอีกชื่อหนึ่งว่า stream processing

เนื่องจากโค้ดใน body ของลูปถูกส่งเป็นฟังก์ชัน callback ให้กับ iterator และมัน很可能เป็นฟังก์ชัน closure Go จึงต้องทำให้ฟังก์ชัน closure แสดงผลเหมือนโค้ด body ของลูปทั่วไปเมื่อดำเนินการคำสั่ง defer, return, break, goto เป็นต้น ลองพิจารณากรณีต่อไปนี้

เช่น การ return ในลูป iterator แล้วจะจัดการ return นี้ในฟังก์ชัน callback yield อย่างไร

go
for index, value := range iterator() {
    if value > 10 {
        return
  }
  fmt.Println(index, value)
}

เป็นไปไม่ได้ที่จะ return โดยตรงในฟังก์ชัน callback การทำเช่นนี้จะทำให้การวนซ้ำหยุดเท่านั้น ไม่สามารถบรรลุผลการ return ได้

go
iterator()(func(index int, value int) bool {
  if value > 10 {
    return false
  }
  fmt.Println(index, value)
})

อีกตัวอย่างคือการใช้ defer ในลูป iterator

go
for index, value := range iterator() {
    defer fmt.Println(index, value)
}

ก็ไม่สามารถใช้ defer ในฟังก์ชัน callback โดยตรงได้ เพราะการทำเช่นนี้จะทำให้การเรียกแบบ delayed เกิดขึ้นทันทีเมื่อฟังก์ชัน callback จบลง

go
iterator()(func(index int, value int) bool {
  defer fmt.Println(index, value)
})

คำสั่งอื่นๆ เช่น break, continue, goto ก็คล้ายกัน โชคดีที่ Go ได้จัดการกรณีเหล่านี้ให้เราแล้ว เราเพียงใช้งาน即可 ไม่ต้องกังวลเกี่ยวกับสิ่งเหล่านี้ชั่วคราว หากสนใจสามารถดูซอร์สโค้ดใน rangefunc/rewrite.go ได้

Pulling Iterator

Pushing iterator คือ iterator ที่ควบคุมตรรกะการวนซ้ำ ผู้ใช้รับองค์ประกอบแบบ passive ในทางตรงกันข้าม pulling iterator คือผู้ใช้ควบคุมตรรกะการวนซ้ำ主动รับองค์ประกอบในลำดับ โดยทั่วไป pulling iterator จะมีฟังก์ชันเฉพาะเช่น next(), stop() เพื่อควบคุมการเริ่มต้นหรือสิ้นสุดการวนซ้ำ มันอาจเป็น closure หรือ struct

go
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line, err := scanner.Text(), scanner.Err()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(line)
}

ดังแสดงด้านบน Scanner ใช้เมธอด Text() เพื่อรับบรรทัดถัดไปในไฟล์ และใช้เมธอด Scan() เพื่อระบุว่า การวนซ้ำสิ้นสุดหรือไม่ นี่ก็เป็นรูปแบบหนึ่งของ pulling iterator Scanner ใช้ struct เพื่อบันทึกสถานะ ส่วน pulling iterator ที่กำหนดในไลบรารี iter ใช้ closure เพื่อบันทึกสถานะ เราสามารถใช้ฟังก์ชัน iter.Pull หรือ iter.Pull2 เพื่อแปลง pushing iterator มาตรฐานเป็น pulling iterator ความแตกต่างระหว่าง iter.Pull กับ iter.Pull2 คือหลังมีค่าส่งกลับสองตัว signature เป็นดังนี้

go
func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())

func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())

ทั้งคู่รับ iterator เป็นพารามิเตอร์ แล้วส่งคืนสองฟังก์ชัน next() และ stop() สำหรับควบคุมการดำเนินการและหยุดการวนซ้ำ

go
func next() (V, bool)

func stop()

next จะส่งคืนองค์ประกอบที่ถูกวนซ้ำ และค่าบูลีนที่ระบุว่าค่าปัจจุบันมีผลหรือไม่ เมื่อการวนซ้ำสิ้นสุด ฟังก์ชัน next จะส่งคืนค่าศูนย์ขององค์ประกอบและ false ฟังก์ชัน stop จะสิ้นสุดกระบวนการวนซ้ำ เมื่อผู้เรียกไม่ใช้ iterator อีกต่อไป ต้องใช้ฟังก์ชัน stop เพื่อสิ้นสุดการวนซ้ำ นอกจากนี้ การเรียกฟังก์ชัน next ของ iterator เดียวกันจากหลาย goroutine เป็นวิธีการที่ผิด เพราะมันไม่ปลอดภัยสำหรับ concurrency

ต่อไปนี้จะแสดงตัวอย่างหนึ่ง ฟังก์ชันของมันคือเปลี่ยน iterator ฟีโบนัชชีก่อนหน้าเป็น pulling iterator ดังนี้

go
func main() {
  n := 10
  next, stop := iter.Pull(Fibonacci(n))
  defer stop()
  for {
    fibn, ok := next()
    if !ok {
      break
    }
    fmt.Println(fibn)
  }
}

ผลลัพธ์

0
1
1
2
3
5
8
13
21
34

เช่นนี้เราสามารถควบคุมตรรกะการวนซ้ำด้วยตนเองผ่านฟังก์ชัน next และ stop ได้ คุณอาจคิดว่า这样做多余 หากจะทำเช่นนี้ทำไมไม่ใช้เวอร์ชัน closure ตั้งแต่แรกล่ะ ก็สามารถควบคุมการวนซ้ำได้เอง การใช้ closure เป็นดังนี้

go
func main() {
  fib := Fibonacci(10)
    for {
        n, ok := fib()
        if !ok {
            break
        }
        fmt.Prinlnt(n)
    }
}

กระบวนการแปลง: closure → iterator → pulling iterator การใช้ closure กับ pulling iterator ไม่แตกต่างกันมาก ความคิดของพวกมันเหมือนกัน หลังยังอาจทำให้ประสิทธิภาพลดลงเนื่องจากกระบวนการต่างๆ ที่หลากหลาย จริงๆ แล้ว这样做确实多余应用场景ของมัน确实不多 แต่ iter.pull มีอยู่เพื่อ iter.Seq也就是为了将 pushing iterator แปลงเป็น pulling iterator หากคุณเพียงต้องการ pulling iterator หนึ่งตัว แล้ว专门去实现 pushing iterator หนึ่งตัว来进行转换 หากจะทำเช่นนี้ควรพิจารณาความซับซ้อนและประสิทธิภาพของการนำไปใช้ด้วย เช่นตัวอย่างฟีโบนัชชีนี้ วน一圈又回到原点 ข้อดีเดียวอาจเป็นเพียงการ符合มาตรฐาน iterator ของทางการ

การจัดการข้อผิดพลาด

จะจัดการอย่างไรเมื่อเกิดข้อผิดพลาดในการวนซ้ำ เราสามารถส่งมันให้กับฟังก์ชัน yield เพื่อให้ for range ส่งคืน ให้ผู้เรียกจัดการ เช่นตัวอย่าง iterator บรรทัดนี้

go
func ScanLines(reader io.Reader) iter.Seq2[string, error] {
  scanner := bufio.NewScanner(reader)
  return func(yield func(string, error) bool) {
    for scanner.Scan() {
      if !yield(scanner.Text(), scanner.Err()) {
        return
      }
    }
  }
}

TIP

ควรทราบว่า iterator ScanLines ใช้ได้ครั้งเดียว หลังจากไฟล์ปิดแล้วไม่สามารถใช้อีกได้

จะเห็นว่าค่าส่งกลับตัวที่สองเป็นประเภท error ใช้งานดังนี้

go
for line, err := range ScanLines(file) {
    if err != nil {
        fmt.Println(err)
        break
    }
    fmt.Println(line)
}

การจัดการเช่นนี้ไม่แตกต่างจากการจัดการข้อผิดพลาดทั่วไป pulling iterator ก็เช่นเดียวกัน

go
next, stop := iter.Pull2(ScanLines(file))
defer stop()
for {
    line, err, ok := next()
    if err != nil {
        fmt.Println(err)
        break
    } else if !ok {
        break
    }
    fmt.Println(line)
}

หากเกิด panic ก็ใช้ recovery เหมือนปกติ

go
defer func() {
    if err := recover(); err != nil {
        fmt.Println("panic:", err)
        os.Exit(1)
    }
}()

for line, err := range ScanLines(file) {
    if err != nil {
        fmt.Println(err)
        break
    }
    fmt.Println(line)
}

pulling iterator ก็เช่นเดียวกัน ที่นี่จะไม่แสดง

ไลบรารีมาตรฐาน

มีไลบรารีมาตรฐานจำนวนมากที่รองรับ iterator ที่ใช้บ่อยที่สุดคือไลบรารีมาตรฐาน slices และ maps ต่อไปนี้จะแนะนำฟังก์ชันที่เป็นประโยชน์บางฟังก์ชัน

slices.All

go
func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E]

slices.All จะแปลงสไลซ์เป็น iterator สไลซ์

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  for i, n := range slices.All(s) {
    fmt.Println(i, n)
  }
}

ผลลัพธ์

0 1
1 2
2 3
3 4
4 5

slices.Values

go
func Values[Slice ~[]E, E any](s Slice) iter.Seq[E]

slices.Values จะแปลงสไลซ์เป็น iterator สไลซ์ แต่ไม่มีดัชนี

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  for n := range slices.Values(s) {
    fmt.Println(n)
  }
}

ผลลัพธ์

1
2
3
4
5

slices.Chunk

go
func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice]

ฟังก์ชัน slices.Chunk จะส่งคืน iterator ที่ส่งสไลซ์ที่มีองค์ประกอบ n ตัวให้กับผู้เรียก

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  for chunk := range slices.Chunk(s, 2) {
    fmt.Println(chunk)
  }
}

ผลลัพธ์

[1 2]
[3 4]
[5]

slices.Collect

func Collect[E any](seq iter.Seq[E]) []E

ฟังก์ชัน slices.Collect จะรวบรวม iterator สไลซ์เป็นสไลซ์

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  s2 := slices.Collect(slices.Values(s))
  fmt.Println(s2)
}

ผลลัพธ์

[1 2 3 4 5]

maps.Keys

go
func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K]

maps.Keys จะส่งคืน iterator ที่วนซ้ำคีย์ทั้งหมดของ map ร่วมกับ slices.Collect สามารถรวบรวมเป็นสไลซ์ได้โดยตรง

go
func main() {
  m := map[string]int{"one": 1, "two": 2, "three": 3}
  keys := slices.Collect(maps.Keys(m))
  fmt.Println(keys)
}

ผลลัพธ์

[three one two]

เนื่องจาก map ไม่มีลำดับ ผลลัพธ์จึงไม่คงที่

maps.Values

go
func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V]

maps.Values จะส่งคืน iterator ที่วนซ้ำค่าทั้งหมดของ map ร่วมกับ slices.Collect สามารถรวบรวมเป็นสไลซ์ได้โดยตรง

go
func main() {
  m := map[string]int{"one": 1, "two": 2, "three": 3}
  keys := slices.Collect(maps.Values(m))
  fmt.Println(keys)
}

ผลลัพธ์

[3 1 2]

เนื่องจาก map ไม่มีลำดับ ผลลัพธ์จึงไม่คงที่

maps.All

func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V]

maps.All สามารถแปลง map เป็น iterator map

go
func main() {
  m := map[string]int{"one": 1, "two": 2, "three": 3}
  for k, v := range maps.All(m) {
    fmt.Println(k, v)
  }
}

โดยปกติไม่ใช้โดยตรงเช่นนี้ มักใช้ร่วมกับฟังก์ชันประมวลผลข้อมูล流อื่นๆ

maps.Collect

go
func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]V

maps.Collect สามารถรวบรวม iterator map เป็น map

go
func main() {
  m := map[string]int{"one": 1, "two": 2, "three": 3}
  m2 := maps.Collect(maps.All(m))
  fmt.Println(m2)
}

ฟังก์ชัน collect มักใช้เป็นฟังก์ชันสิ้นสุดของการประมวลผลข้อมูล流

การเรียกแบบ Chain

ผ่านฟังก์ชันที่ไลบรารีมาตรฐานจัดให้ข้างต้น เราสามารถรวมมันเพื่อประมวลผลข้อมูล流 เช่น เรียงลำดับข้อมูล流 ดังนี้

go
sortedSlices := slices.Sorted(slices.Values(s))

Iterator ของ Go ใช้ closure สามารถเรียกฟังก์ชันแบบ nested เช่นนี้ได้เท่านั้น ตัวมันเองไม่สามารถเรียกแบบ chain ได้ เมื่อสายการเรียกยาวขึ้น ความสามารถในการอ่านจะแย่ลง แต่เราสามารถบันทึก iterator ผ่าน struct ได้เอง ก็สามารถ实现การเรียกแบบ chain ได้

demo

demo การเรียกแบบ chain อย่างง่ายแสดงด้านล่าง ประกอบด้วยฟังก์ชันที่ใช้บ่อยเช่น Filter, Map, Find, Some เป็นต้น

go
package iterx

import (
  "iter"
  "slices"
)

type SliceSeq[E any] struct {
  seq iter.Seq2[int, E]
}

func (s SliceSeq[E]) All() iter.Seq2[int, E] {
  return s.seq
}

func (s SliceSeq[E]) Filter(filter func(int, E) bool) SliceSeq[E] {
  return SliceSeq[E]{
    seq: func(yield func(int, E) bool) {
      // จัดระเบียบดัชนีใหม่
      i := 0
      for k, v := range s.seq {
        if filter(k, v) {
          if !yield(i, v) {
            return
          }
          i++
        }
      }
    },
  }
}

func (s SliceSeq[E]) Map(mapFn func(E) E) SliceSeq[E] {
  return SliceSeq[E]{
    seq: func(yield func(int, E) bool) {
      for k, v := range s.seq {
        if !yield(k, mapFn(v)) {
          return
        }
      }
    },
  }
}

func (s SliceSeq[E]) Fill(fill E) SliceSeq[E] {
  return SliceSeq[E]{
    seq: func(yield func(int, E) bool) {
      for i, _ := range s.seq {
        if !yield(i, fill) {
          return
        }
      }
    },
  }
}

func (s SliceSeq[E]) Find(equal func(int, E) bool) (_ E) {
  for i, v := range s.seq {
    if equal(i, v) {
      return v
    }
  }
  return
}

func (s SliceSeq[E]) Some(match func(int, E) bool) bool {
  for i, v := range s.seq {
    if match(i, v) {
      return true
    }
  }
  return false
}

func (s SliceSeq[E]) Every(match func(int, E) bool) bool {
  for i, v := range s.seq {
    if !match(i, v) {
      return false
    }
  }
  return true
}

func (s SliceSeq[E]) Collect() []E {
  var res []E
  for _, v := range s.seq {
    res = append(res, v)
  }
  return res
}

func (s SliceSeq[E]) Sort(cmp func(x, y E) int) []E {
  collect := s.Collect()
  slices.SortFunc(collect, cmp)
  return collect
}

func (s SliceSeq[E]) SortStable(cmp func(x, y E) int) []E {
  collect := s.Collect()
  slices.SortStableFunc(collect, cmp)
  return collect
}

func Slice[S ~[]E, E any](s S) SliceSeq[E] {
  return SliceSeq[E]{seq: slices.All(s)}
}

จากนั้นเราสามารถประมวลผลผ่านการเรียกแบบ chain ได้ ดูตัวอย่างการใช้งานบางตัวอย่าง

ประมวลผลค่าองค์ประกอบ

go
func main() {
  s := []string{"apple", "banana", "cherry"}
  all := iterx.Slice(s).Map(strings.ToUpper).All()
  for i, v := range all {
    fmt.Printf("index: %d, value: %s\n", i, v)
  }
}

ผลลัพธ์

index: 0, value: APPLE
index: 1, value: BANANA
index: 2, value: CHERRY

ค้นหาค่าที่ระบุ某一个

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  result := iterx.Slice(s).Find(func(i int, e int) bool {
    return e == 3
  })
  fmt.Println(result)
}

ผลลัพธ์

3

เติมสไลซ์

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  result := iterx.Slice(s).Fill(6).Collect()
  fmt.Println(result)
}

ผลลัพธ์

[6 6 6 6 6]

กรององค์ประกอบ

go
func main() {
  s := []int{1, 2, 3, 4, 5}
  filter := iterx.Slice(s).Filter(func(i int, e int) bool {
    return e%2 == 0
  }).All()
  for i, v := range filter {
    fmt.Printf("Index: %d, Value: %d\n", i, v)
  }
}

ผลลัพธ์

Index: 0, Value: 2
Index: 1, Value: 4

น่าเสียดายที่ Go ยังไม่รองรับฟังก์ชันไม่ระบุชื่อแบบย่อ เช่น arrow function ใน js, rust, java มิฉะนั้นการเรียกแบบ chain ยังสามารถกระชับและสง่างามยิ่งขึ้น

ประสิทธิภาพ

เนื่องจาก Go ได้จัดการ iterator มากมาย ประสิทธิภาพของมัน肯定ไม่ดีกว่าลูป for range ดั้งเดิม เราจะทดสอบความแตกต่างของประสิทธิภาพด้วยการ遍历สไลซ์อย่างง่าย แบ่งเป็นประเภทต่อไปนี้

  • ลูป for ดั้งเดิม
  • Pushing iterator
  • Pulling iterator

โค้ดทดสอบดังนี้ ความยาวสไลซ์ทดสอบคือ 1000

go
package main

import (
  "iter"
  "slices"
  "testing"
)

var s []int

const n = 10000

func init() {
  for i := range n {
    s = append(s, i)
  }
}

func testNaiveFor(s []int) {
  for i, n := range s {
    _ = i
    _ = n
  }
}

func testPushing(s []int) {
  for i, n := range slices.All(s) {
    _ = i
    _ = n
  }
}

func testPulling(s []int) {
  next, stop := iter.Pull2(slices.All(s))
  for {
    i, n, ok := next()
    if !ok {
      stop()
      return
    }
    _ = i
    _ = n
  }
}

func BenchmarkNaive_10000(b *testing.B) {
  for range b.N {
    testNaiveFor(s)
  }
}

func BenchmarkPushing_10000(b *testing.B) {
  for range b.N {
    testPushing(s)
  }
}

func BenchmarkPulling_10000(b *testing.B) {
  for range b.N {
    testPulling(s)
  }
}

ผลลัพธ์การทดสอบเป็นดังนี้

goos: windows
goarch: amd64
pkg: golearn
cpu: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz
BenchmarkNaive_10000
BenchmarkNaive_10000-16           492658              2398 ns/op               0 B/op          0 allocs/op
BenchmarkPushing_10000
BenchmarkPushing_10000-16         315889              3707 ns/op               0 B/op          0 allocs/op
BenchmarkPulling_10000
BenchmarkPulling_10000-16           2016            574509 ns/op             440 B/op         14 allocs/op
PASS
ok      golearn 4.029s

จากผลลัพธ์เราจะเห็นว่า pushing iterator ไม่แตกต่างจากลูป for range ดั้งเดิมมากนัก แต่ pulling iterator ช้ากว่าสองตัวก่อนหน้าเกือบสองลำดับขนาด เมื่อใช้งาน各位可以根据自己的实际情况来进行考虑

สรุป

เช่นเดียวกับ generics iterator ของ Go ก็ถูกวิพากษ์วิจารณ์เช่นกัน บางคนมองว่า iterator นำความซับซ้อนที่มากเกินไปมา ขัดกับปรัชญาความเรียบง่ายของ Go เช่นโค้ด closure ของ iterator นี้多了以后 การดีบัก恐怕有点困难 การอ่านก็ยิ่งน่าหงุดหงิดมากขึ้น

คุณสามารถเห็นการอภิปรายที่รุนแรงเกี่ยวกับ iterator ได้ในหลายที่

  • Why People are Angry over Go 1.23 Iterators บทวิจารณ์เกี่ยวกับ iterator จากพี่ชายชาวต่างชาติคนหนึ่ง น่าดู
  • golang/go · Discussion #56413 การอภิปรายในชุมชนที่ rsc เริ่มต้น มี很多人发表了自己的观点

มอง iterator ของ Go อย่างมีเหตุผล มัน确实ทำให้การเขียนโค้ดสะดวกขึ้น โดยเฉพาะเมื่อประมวลผลประเภทสไลซ์ แต่ในขณะเดียวกันก็นำความซับซ้อนบางอย่างมา ความสามารถในการอ่านของโค้ดส่วน iterator จะลดลง แต่โดยรวมแล้ว ฉันคิดว่านี่เป็นคุณสมบัติที่เป็นประโยชน์จริงๆ

Golang by www.golangdev.cn edit