go iterators

iterators in go

SEAN K.H. LIAO

go iterators

iterators in go

iterators

language

the language is special, it gets to use range.

maps
 1func maps() {
 2        m := make(map[string]string)
 3
 4        for key := range m {
 5                _ = key
 6        }
 7
 8        for key, value := range m {
 9                _, _ = key, value
10        }
11}
slices
 1func slices() {
 2        s := make([]string, 0)
 3
 4        for index := range s {
 5                _ = index
 6        }
 7
 8        for index, value := range s {
 9                _, _ = index, value
10        }
11}
channels
1func channels() {
2        c := make(chan string)
3
4        for value := range c {
5                _ = value
6        }
7}

standard-ish

these are the usually seen ones

no error

No errors, no need to handle any iteration errors.

1func noError() {
2        s := bufio.NewScanner(nil)
3        for s.Scan() {
4                _ = s.Text()
5        }
6}
deferred error

You keep any errors internally, stopping iteration on error, and providing a way to check iteration errors later. Users may forget to check errors.

 1func deferredError() {
 2        r := sql.Rows{}
 3
 4        for r.Next() {
 5                _ = r.Scan()
 6        }
 7        err := r.Err()
 8        if err != nil {
 9                // handle iteration error
10                _ = err
11        }
12}
immediate error

You return any iteration errors immediately, forcing users to to use a 3 valued for. Technically the first iteration can also be in the for loop, but then you need to predeclare the error and value var to get the right scope to handle errors.

 1func immediateError() {
 2        r := tar.NewReader()
 3
 4        h, err := r.Next()
 5        for ; err != nil; h, err = r.Next() {
 6                _ = h
 7        }
 8        if err != nil {
 9                // handle error
10                _ = err
11        }
12
13        // alternative
14        var err error
15        var h *tar.Header
16        for h, err = r.Next(); err != nil; h, err = r.Next() {
17                _ = h
18        }
19        // handle error
20        // most likely needs to handle the "no more results" error differently
21        _ = err
22}

exotic

You want to be cool and use range too.

channel abuse

maps and slices need the entire thing present at the start of iteration, but channels....

This does not expose a way to return errors, other than it being part of the iteration value. Also this will leak a goroutine and channel if iteration ever stops early (goroutine holds reference to channel because it is blocked on send, can't be GC'ed).

The leak could be handled by instead returning a read-write channel and catching a write-on-closed channel panic or by passing a done chan to the iterator to signal an end to iteration.

No wonder nobody does this, and people recommend not returning channels as part of public APIs.

 1type X struct{}
 2func (x X) Iterator() <-chan string {
 3        c := make(chan string)
 4        go func(){
 5                for {
 6                        // TODO: conditionally beakout
 7                        c <- generateValue()
 8                }
 9                close(c)
10        }()
11        return c
12}
13
14func exoticChannelIterator() {
15        var x X
16
17        for value := range x.Iterator() {
18
19        }
20}