Skip to content

Iterator

Di Go, keyword yang digunakan untuk mengiterasi struktur data tertentu adalah for range. Bab sebelumnya telah memperkenalkan beberapa aplikasinya, keyword ini hanya dapat bekerja pada beberapa struktur data built-in bahasa

  • Array
  • Slice
  • String
  • Map
  • Chan
  • Nilai integer

Penggunaannya sangat tidak fleksibel, tidak ada extensibility, hampir tidak mendukung untuk tipe custom. Namun setelah update versi go1.23, keyword for range mendukung range over func, sehingga membuat custom iterator menjadi mungkin.

Mengenal

Berikut melalui contoh untuk初步 mengenal iterator, tidak tahu apakah masih ingat contoh closure menghitung deret Fibonacci yang dibahas di bagian fungsi, implementasinya sebagai berikut

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
  }
}

Kita dapat mengubahnya menjadi iterator, sebagai berikut, dapat dilihat jumlah kode berkurang sedikit

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 bergaya range over func, kita dapat langsung menggunakan keyword for range untuk menggunakannya, penggunaannya juga lebih nyaman dibanding sebelumnya

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

Output sebagai berikut

0
1
1
2
3
5
8
13

Seperti ditunjukkan di atas, iterator adalah fungsi closure, ia menerima callback function sebagai parameter, Anda bahkan dapat melihat kata seperti yield di dalamnya, orang yang pernah menulis python seharusnya sangat familiar, ini mirip dengan generator di python. Iterator Go tidak menambahkan keyword atau fitur syntax baru, di contoh di atas yield juga hanya callback function, bukan keyword,官方 mengambil nama ini untuk memudahkan pemahaman.

Push Iterator

Tentang definisi iterator, kita dapat menemukan penjelasan berikut di library iter

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

Iterator adalah fungsi yang meneruskan elemen-elemen sequence secara berurutan ke callback function, secara konvensional bernama yield.

Dari sini kita dapat memastikan satu hal, iterator adalah fungsi, ia menerima callback function sebagai parameter, selama proses iterasi akan meneruskan elemen-elemen sequence secara berurutan ke callback function yield. Di contoh sebelumnya kita menggunakan iterator seperti berikut

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

Menurut definisi官方, penggunaan iterator Backward di atas setara dengan kode berikut

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

Body loop adalah callback function yield iterator, saat function mengembalikan true iterator akan melanjutkan iterasi, jika tidak akan berhenti.

Selain itu, library standar iter juga mendefinisikan tipe iterator iter.Seq, tipenya adalah fungsi.

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

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

Callback function iter.Seq hanya menerima satu parameter, maka saat iterasi for range hanya memiliki satu nilai return, sebagai berikut

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

Callback function iter.Seq2 menerima dua parameter, maka saat iterasi for range memiliki dua nilai return, sebagai berikut

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

Meskipun library standar tidak mendefinisikan Seq dengan 0 parameter, tetapi ini sepenuhnya diizinkan, ini setara dengan

go
func(yield func() bool)

Penggunaannya sebagai berikut

go
for range iter {
  // body
}

Jumlah parameter callback function hanya boleh 0 hingga 2, lebih banyak tidak akan lolos kompilasi.

Singkatnya, body loop di for range adalah callback function yield di iterator, berapa nilai yang dikembalikan for range,相应的 yeild function memiliki berapa parameter input, setiap iterasi, iterator akan memanggil function yield, yaitu mengeksekusi kode di body loop, secara aktif meneruskan elemen-elemen sequence ke function yield, iterator yang secara aktif meneruskan elemen ini umumnya kita sebut pushing iterator, contoh tipikal adalah foreach di bahasa lain, misalnya js

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

Bentuk representasi di Go adalah elemen yang diiterasi dikembalikan oleh range.

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

Di beberapa bahasa (seperti Java) ini memiliki nama lain: stream processing.

Karena kode di body loop adalah sebagai callback function diteruskan ke iterator, dan sangat mungkin adalah closure function, Go perlu membuat closure function saat mengeksekusi defer, return, break, goto dan keyword lainnya berperilaku seperti segmen kode loop biasa, pertimbangkan beberapa situasi berikut.

Misalnya return di loop iterator, lalu bagaimana cara menangani return ini di callback function yield?

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

Tidak mungkin langsung return di callback function, melakukan ini hanya akan membuat iterasi berhenti, tidak mencapai efek return

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

Lagi misalnya menggunakan defer di loop iterator

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

Juga tidak bisa langsung menggunakan defer di callback function, karena melakukan ini saat callback function selesai akan langsung delayed call

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

Beberapa keyword lainnya break, continue, goto juga serupa, untungnya situasi-situasi ini sudah ditangani oleh Go, kita hanya perlu menggunakannya saja, dapat暂时 tidak perlu memperhatikan ini, jika tertarik dapat自行浏览 source code di rangefunc/rewrite.go.

Pull Iterator

Pushing iterator dikontrol logika iterasi oleh iterator, user pasif mendapatkan elemen, sebaliknya pulling iterator dikontrol logika iterasi oleh user, secara aktif mendapatkan elemen sequence. Umumnya, pulling iterator akan memiliki fungsi spesifik seperti next(), stop() untuk mengontrol mulai atau berakhir iterasi, ini dapat berupa closure atau 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)
}

Seperti ditunjukkan di atas, Scanner mendapatkan baris teks berikutnya di file melalui metode Text(), melalui metode Scan() untuk表示 apakah iterasi berakhir, ini juga salah satu mode pulling iterator. Scanner menggunakan struct untuk merekam status, sedangkan pulling iterator yang didefinisikan di library iter menggunakan closure untuk merekam status, kita dapat mengkonversi pushing iterator standar menjadi pulling iterator melalui fungsi iter.Pull atau iter.Pull2, perbedaan iter.Pull dan iter.Pull2 adalah nilai return yang kedua memiliki dua, signature sebagai berikut

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())

Keduanya menerima iterator sebagai parameter, lalu mengembalikan dua fungsi next() dan stop(), digunakan untuk mengontrol lanjutan dan penghentian iterasi.

go
func next() (V, bool)

func stop()

next akan mengembalikan elemen yang diiterasi, dan nilai boolean yang表示 apakah nilai saat ini valid, saat iterasi berakhir function next akan mengembalikan nilai nol elemen dan false. Function stop akan mengakhiri proses iterasi, saat caller tidak lagi menggunakan iterator, harus menggunakan function stop untuk mengakhiri iterasi. Sekadar informasi, memanggil function next iterator yang sama di beberapa goroutine adalah praktik yang salah, karena ini bukan concurrent safe.

Berikut melalui contoh untuk mendemonstrasikan, fungsinya adalah mengubah iterator Fibonacci sebelumnya menjadi pulling iterator, sebagai berikut

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

Output

0
1
1
2
3
5
8
13
21
34

Dengan demikian kita dapat mengontrol logika iterasi secara manual melalui fungsi next dan stop. Anda mungkin merasa ini berlebihan, jika ingin melakukan ini mengapa tidak langsung menggunakan versi closure awal saja, sama juga dapat mengontrol iterasi sendiri, penggunaan closure adalah seperti ini

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

Proses konversi: closure → iterator → pulling iterator, penggunaan closure dan pulling iterator hampir sama, ide mereka sama, yang kedua akan karena berbagai pemrosesan menyebabkan penurunan performa. Jujur melakukan ini memang berlebihan, scenario aplikasinya memang tidak banyak, tetapi iter.pull adalah untuk iter.Seq yang ada, yaitu untuk mengkonversi pushing iterator menjadi pulling iterator yang ada, jika Anda hanya ingin pulling iterator, masih khusus mengimplementasikan pushing iterator untuk melakukan konversi, untuk melakukan ini lebih baik pertimbangkan kompleksitas dan performa implementasi sendiri, seperti contoh deret Fibonacci ini, berputar-putar kembali ke titik awal, satu-satunya keuntungan mungkin sesuai dengan spesifikasi iterator官方.

Penanganan Error

Bagaimana jika terjadi error saat iterasi? Kita dapat meneruskannya ke function yield untuk dikembalikan oleh for range, biarkan caller yang menangani, seperti contoh iterator baris berikut

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

Perlu dicatat, iterator ScanLines adalah one-time use, setelah file ditutup tidak dapat digunakan lagi.

Dapat dilihat nilai return keduanya adalah tipe error, penggunaannya sebagai berikut

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

Penanganan seperti ini tidak berbeda dengan penanganan error biasa, pulling iterator juga sama

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)
}

Jika terjadi panic, sama seperti biasa menggunakan recovery saja.

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 tetap sama, tidak akan didemonstrasikan lagi di sini.

Library Standar

Banyak library standar juga mendukung iterator, yang paling umum digunakan adalah library standar slices dan maps, berikut memperkenalkan beberapa fungsi yang praktis.

slices.All

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

slices.All akan mengkonversi slice menjadi iterator slice

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

Output

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 akan mengkonversi slice menjadi iterator slice, tetapi tanpa index

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

Output

1
2
3
4
5

slices.Chunk

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

Fungsi slices.Chunk akan mengembalikan iterator, iterator ini akan meneruskan slice dengan n elemen ke caller

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

Output

[1 2]
[3 4]
[5]

slices.Collect

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

Fungsi slices.Collect akan mengumpulkan iterator slice menjadi slice

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

Output

[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 akan mengembalikan iterator yang mengiterasi semua key map,配合 slices.Collect dapat langsung dikumpulkan menjadi slice.

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

Output

[three one two]

Karena map tidak berurutan, output juga tidak tetap

maps.Values

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

maps.Values akan mengembalikan iterator yang mengiterasi semua value map,配合 slices.Collect dapat langsung dikumpulkan menjadi slice.

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

Output

[3 1 2]

Karena map tidak berurutan, output juga tidak tetap

maps.All

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

maps.All dapat mengkonversi map menjadi 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)
  }
}

Umumnya tidak langsung menggunakan seperti ini, biasanya配合 fungsi stream processing lainnya.

maps.Collect

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

maps.Collect dapat mengkonversi iterator map menjadi map

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

Fungsi collect umum digunakan sebagai fungsi terminal stream processing.

Chain Call

Melalui fungsi yang disediakan library standar di atas, kita dapat mengombinasikannya untuk memproses stream data, misalnya mengurutkan stream data, sebagai berikut

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

Iterator Go menggunakan closure, hanya dapat nested function call seperti ini, sendiri tidak dapat chain call, setelah call chain menjadi panjang readability akan sangat buruk, tetapi kita dapat sendiri melalui struct untuk merekam iterator, sehingga dapat mengimplementasikan chain call.

Demo

Demo chain call sederhana sebagai berikut, ini berisi fungsi yang umum digunakan seperti Filter, Map, Find, Some dll.

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) {
      // Reorganisasi index
      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)}
}

Lalu kita dapat memproses melalui chain call, lihat beberapa contoh penggunaan.

Memproses Nilai Elemen

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)
  }
}

Output

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

Mencari Nilai Spesifik Tertentu

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)
}

Output

3

Mengisi Slice

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

Output

[6 6 6 6 6]

Filter Elemen

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)
  }
}

Output

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

Sayang Go saat ini belum mendukung anonymous function shorthand, seperti arrow function di js, rust, java, jika tidak chain call dapat lebih ringkas dan elegan.

Performa

Karena Go melakukan banyak pemrosesan untuk iterator, performanya tentu tidak sebaik loop for range native, kita ambil loop slice sederhana untuk menguji perbedaan performa mereka, dibagi beberapa berikut

  • Loop for native
  • Pushing iterator
  • Pulling iterator

Kode tes sebagai berikut, panjang slice tes adalah 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)
  }
}

Hasil tes sebagai berikut

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

Dari hasil kita dapat melihat pushing iterator tidak terlalu berbeda dengan for range native, tetapi pulling iterator hampir dua order of magnitude lebih lambat dibanding dua sebelumnya, saat menggunakan各位 dapat mempertimbangkan sesuai situasi aktual.

Ringkasan

Sama seperti situasi generik, iterator Go juga kontroversial, sebagian orang berpendapat iterator memperkenalkan terlalu banyak kompleksitas, melanggar filosofi kesederhanaan Go, seperti kode closure iterator ini setelah banyak, debugging sepertinya agak sulit, reading lebih menyebalkan.

Anda dapat melihat banyak diskusi intens tentang iterator di berbagai tempat

Memandang iterator Go secara rasional, ini memang membuat penulisan kode lebih nyaman, terutama saat menangani tipe slice, tetapi sekaligus juga memperkenalkan sedikit kompleksitas, readability kode bagian iterator akan menurun, namun secara keseluruhan, saya pikir ini memang fitur yang praktis.

Golang by www.golangdev.cn edit