blog

12020-09-30

SEAN K.H. LIAO

iterators

language

the language is special, it gets to use range.

maps
func maps() {
        m := make(map[string]string)

        for key := range m {
                _ = key
        }

        for key, value := range m {
                _, _ = key, value
        }
}
slices
func slices() {
        s := make([]string, 0)

        for index := range s {
                _ = index
        }

        for index, value := range s {
                _, _ = index, value
        }
}
channels
func channels() {
        c := make(chan string)

        for value := range c {
                _ = value
        }
}

standard-ish

these are the usually seen ones

no error

No errors, no need to handle any iteration errors.

func noError() {
        s := bufio.NewScanner(nil)
        for s.Scan() {
                _ = s.Text()
        }
}
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.

func deferredError() {
        r := sql.Rows{}

        for r.Next() {
                _ = r.Scan()
        }
        err := r.Err()
        if err != nil {
                // handle iteration error
                _ = err
        }
}
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.

func immediateError() {
        r := tar.NewReader()

        h, err := r.Next()
        for ; err != nil; h, err = r.Next() {
                _ = h
        }
        if err != nil {
                // handle error
                _ = err
        }

        // alternative
        var err error
        var h *tar.Header
        for h, err = r.Next(); err != nil; h, err = r.Next() {
                _ = h
        }
        // handle error
        // most likely needs to handle the "no more results" error differently
        _ = err
}

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.

type X struct{}
func (x X) Iterator() <-chan string {
        c := make(chan string)
        go func(){
                for {
                        // TODO: conditionally beakout
                        c <- generateValue()
                }
                close(c)
        }()
        return c
}

func exoticChannelIterator() {
        var x X

        for value := range x.Iterator() {

        }
}