Go version 1.23 added iterator support 1 and the iter
package
2. We can now loop
over constants, containers (maps, slices,
arrays,
strings) and functions. At first I found the iterator creation clunky,
while consuming the iterator seems straightforward.
My issue with the go way of iterators is, you can’t chain them like you would in JavaScript:
1[1,2,3,4]
2 .reverse()
3 .map(e => e*e)
4 .filter(e => e % 2 == 0)
5 .forEach(e => console.log(e))
The Annoyance
Writing this in go would require us to chain 5 function calls:
1slices.ForEach(
2 slices.Filter(
3 slices.Map(
4 slices.Reverse(slices.All([]int{1,2,3,4})),
5 func(i int) int { return i * i},
6 ),
7 func(i int) bool { return i % 2 == 0 }
8 ),
9 func(i int) { fmt.Println(i) }
10)
This is an example, there are are no Map
, Filter
or ForEach
functions in the slices
package 3.
The Solution (sort of)
Because i have a big distaste for writing chained “functional” operations like this, looking at you python (don’t come at me haskell bros) - I wanted to use the new iterators and the iter
package and wrap it with a structure to allow the nice and clean chaining JavaScript provides. Below are the same operations, but instead of using the iter
and slices
package, I use my abstraction:
1func TestIterator(t *testing.T) {
2 From([]int{1, 2, 3, 4}).
3 Reverse().
4 Map(func(i int) int { return i * i }).
5 Filter(func(i int) bool { return i%2 == 0 }).
6 Each(func(a int) { println(a) })
7 // 16
8 // 4
9}
The Logic
Lets take a look a the implementation and let me introduce the Iterator
struct.
It wraps the iterator (*Iterator).iter
and thus allows me to callfunctions on
this structure, instead of having to use each iterator functionas a param to the
next one.
1type Iterator[V any] struct {
2 iter iter.Seq[V]
3}
Lets take a look at the first functions coming to mind when talking about iterators, creating one from a slice and collection one into a slice:
1func (i Iterator[V]) Collect() []V {
2 collect := make([]V, 0)
3 for e := range i.iter {
4 collect = append(collect, e)
5 }
6 return collect
7}
8
9func From[V any](slice []V) *Iterator[V] {
10 return &Iterator[V]{
11 iter: func(yield func(V) bool) {
12 for _, v := range slice {
13 if !yield(v) {
14 return
15 }
16 }
17 },
18 }
19}
The first one is as straight forward as possible - create a slice, consume the
iterator, append each element, return the slice. The second highlights the weird
way iterators are created in go. Lets first take a look at the signature, we are
returning a pointer to the struct, so the callee can invoke all methods without
having to use a temporary variable for each. In the function itself, the
iterator is created by returning a closure, that loops over the param and
returning, which stops the iterator, when the yield
function returns false
.
Each
The ForEach
/ Each
method is the next function I want, It simply executes
the passed in function for every element of the iterator.
1func (i *Iterator[V]) Each(f func(V)) {
2 for i := range i.iter {
3 f(i)
4 }
5}
It can be used like this:
1From([]int{1, 2, 3, 4}).Each(func(a int) { println(a) })
2// 1
3// 2
4// 3
5// 4
Reverse
A way to reverse the iterator, we first need to collect all elements and after that construct a new iterator from the collected slice, luckily we have functions for exactly this:
1func (i *Iterator[V]) Reverse() *Iterator[V] {
2 collect := i.Collect()
3 counter := len(collect) - 1
4 for e := range i.iter {
5 collect[counter] = e
6 counter--
7 }
8 return From(collect)
9}
Again, useful to reverse a slice:
1From([]int{1, 2, 3, 4}).Reverse().Each(func(a int) { println(a) })
2// 4
3// 3
4// 2
5// 1
Map
Mutating every element of the iterator is a necessity, too:
1func (i *Iterator[V]) Map(f func(V) V) *Iterator[V] {
2 cpy := i.iter
3 i.iter = func(yield func(V) bool) {
4 for v := range cpy {
5 v = f(v)
6 if !yield(v) {
7 return
8 }
9 }
10 }
11 return i
12}
At first we copy the previous iterator, by doing this, we skip causing a stack
overflow by referencing the i.iter
iterator in the iterator itself. Map
is
works by overwriting the i.iter
with a new iterator thats consuming every
field of the cpy
iterator and overwriting the iterator value with the result
of passing v
to f
, thus mapping over the iterator.
Filter
After mapping, possibly the most used streaming / functional api method is
Filter
. So lets take a look at our final operation:
1func (i *Iterator[V]) Filter(f func(V) bool) *Iterator[V] {
2 cpy := i.iter
3 i.iter = func(yield func(V) bool) {
4 for v := range cpy {
5 if f(v) {
6 if !yield(v) {
7 return
8 }
9 }
10 }
11 }
12 return i
13}
Similar to Map
, we consume the copied iterator and invoke f
with v
as the
param for each one, if f
returns true, we keep it in the new iterator.
Examples and Thoughts
The slices
and the iter
package both play very good together with the generic system introduced in go 1.18 4.
While this API is easier to use, I understand the reasoning of the go team for not implementing iterators like this. Below are some tests that serve as examples and the result of running them.
1package iter1
2
3import (
4 "fmt"
5 "testing"
6 "unicode"
7)
8
9func TestIteratorNumbers(t *testing.T) {
10 From([]int{1, 2, 3, 4}).
11 Reverse().
12 Map(func(i int) int { return i * i }).
13 Filter(func(i int) bool { return i%2 == 0 }).
14 Each(func(a int) { println(a) })
15}
16
17func TestIteratorRunes(t *testing.T) {
18 r := From([]rune("Hello World!")).
19 Reverse().
20 // remove all spaces
21 Filter(func(r rune) bool { return !unicode.IsSpace(r) }).
22 // convert every rune to uppercase
23 Map(func(r rune) rune { return unicode.ToUpper(r) }).
24 Collect()
25 fmt.Println(string(r))
26}
27
28func TestIteratorStructs(t *testing.T) {
29 type User struct {
30 Id int
31 Name string
32 Hash int
33 }
34
35 u := []User{
36 {0, "xnacly", 0},
37 {1, "hans", 0},
38 {2, "gedigedagedeio", 0},
39 }
40
41 From(u).
42 // computing the hash for each user
43 Map(func(u User) User {
44 h := 0
45 for i, r := range u.Name {
46 h += int(r)*31 ^ (len(u.Name) - i - 1)
47 }
48 u.Hash = h
49 return u
50 }).
51 Each(func(u User) { fmt.Printf("%#+v\n", u) })
52}
Running these, results in:
1$ go test ./... -v
2=== RUN TestIteratorNumbers
316
44
5--- PASS: TestIteratorNumbers (0.00s)
6=== RUN TestIteratorRunes
7!DLROWOLLEH
8--- PASS: TestIteratorRunes (0.00s)
9=== RUN TestIteratorStructs
10&iter1.User{Id:0, Name:"xnacly", Hash:20314}
11&iter1.User{Id:1, Name:"hans", Hash:13208}
12&iter1.User{Id:2, Name:"gedigedagedeio", Hash:44336}
13--- PASS: TestIteratorStructs (0.00s)
14PASS
15ok iter1 0.263s
So there u have it, a wrapper around iter
and slices
to mirror the way
JavaScript provides streaming, only in go.