Go Optimizations 101, Go Details & Tips 101 and Go Generics 101 are all updated for Go 1.24 now. The most cost-effective way to get them is through this book bundle in the Leanpub book store.

TapirMD - a powerful, next-generation markup language that simplifies content creation (much more powerful than markdown). You can experience it online here.

Some Facts About Go Iterators

A simple introduction of Go iterators

Go 1.23 inroduced iterators, Iteators in Go are some functions which can be ranged over. Specifically, function expressions of types whose underlying types are in the following forms can be ranged over.

func(func() bool)
func(func(V) bool)
func(func(K, V) bool)

// K and V both denote types.

All of the types take a callback function parameter which is called yield by convention. Such ballback functions might take zero, one or two parameters and return a bool result.

For example, the following Go program prints abc (since Go 1.23).

package main

import "fmt"

func main() {
	for v := range abc {
		fmt.Print(v)
	}
}

func abc(yield func(string) bool) {
	if yield("a") {
		if yield("b") {
			yield("c")
		}
	}
}

The function abc is used as an iterator here. Its parameter yield is a callback function which takes a string parameter and returns a bool result.

The iterator function in the above example can also be written as

func abc(yield func(string) bool) {
	_ = yield("a") && yield("b") && yield("c")
}

Don't call a yield callback again if a call to the callback has ever returned false

The callback function of an iterator function must not be called any more if a call of the callback function ever returned false. For example, the following program crashes when running.

package main

import "fmt"

func main() {
	for v := range ab {
		fmt.Println(v)
		break // remove this line to avoid crashing.
	}
}

func ab(yield func(string) bool) {
	var done = yield("a")
	fmt.Println(done) // false
	
	yield("b") // runtime error: range function
	           // continued iteration after function
	           // for loop body returned false.
}

The official Go compiler doesn't check such invalid uses. So such an invalid use produces a runtime (recoverable) error.

Note, go vet also doesn't check such invalid uses (up to v1.24.x).

Don't call the yield callback of an iterator function if the iterator function has returned

The callback function of an iterator function must not be called any more if the iterator function has returned. For example, the following program crashes when running.

package main

import "fmt"

var f func(string) bool

func main() {
	for v := range abc {
		fmt.Print(v)
	}
	f("d") // runtime error: range function
	       // continued iteration after
	       // whole loop exit
}

func abc(yield func(string) bool) {
	_ = yield("a") && yield("b") && yield("c")
	f = yield
}

Calling a yield callback function concurrently needs careful synchronizations

Parallel calls to a yield callback function are not synchronized by default. You need to do the synchronizations by yourself.

For example, the following program crashes for a confusing panic.

package main

import "fmt"
import "sync"

var c = make(chan int)

func main() {
	// runtime error: range function continued iteration after loop body panic
	for v := range oneHundred {
		<-c
		fmt.Println(v)
	}
}

func oneHundred(yield func(int) bool) {
	var wg sync.WaitGroup
	for v := range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			yield(v)
		}()
	}
	close(c)
	wg.Wait()
}

To make it work, it should be synchronized, like

func abc(yield func(int) bool) {
	var wg sync.WaitGroup
	var mu sync.Mutex // use a mutex to sync
	for v := range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			mu.Lock()         // prevent yield being
			defer mu.Unlock() // called parallelly.
			yield(v)
		}()
	}
	close(c)
	wg.Wait()
}

However, it is strongly recommended not to use yield callback functions in other goroutines.

The top function calls in the call stack of a goroutine will be executed concurrently when ranging over iterator functions

When an iterator function is ranged over, it is called implicitly. During ranging over the iterator function, the execution of a goroutine will jump back and forth between the iterator function call and the loop body (the loop body is a statement within the caller of the iterator function call).

For example, the following program prints 0a1b2c.

package main

import "fmt"

func main() {
	for v := range abc {
		fmt.Print(v)
	}
}

func abc(yield func(string) bool) {
	for i, v := range "abc" {
		fmt.Print(i)
		if (!yield(string(v))) {
			break
		}
	}
}

The following is another example, in which the execution of a goroutine will jump back and forth between 222 iterator function calls and the loop body in the main function. Finally, the program prints 4194304, 4194305 and 4194306 in turn.

package main

func wrap(it func(yield func(int) bool)) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		for v := range it {
			yield(v+1)
		}
	}
}

func main() {
	iterator := func(yield func(int) bool) {
		_ = yield(0) && yield(1) && yield (2)
	}
	for range 1 << 22 { // 23 crashes the program
		iterator = wrap(iterator)
	}
	for v := range iterator {
		println(v)
	}
}

Calling an iterator function explicitly might be equivalent to ranging over the iterator function or not

For the iterator function shown above:

func abc(yield func(string) bool) {
	_ = yield("a") && yield("b") && yield("c")
}

the following two ways of using it are equvalent.

	for v := range abc {
		fmt.Print(v)
	}

	abc(func(v string) bool {
		fmt.Print(v)
		return true
	})

However, for the following iterator function:

func abcThenPanic(yield func(string) bool) {
	_ = yield("a") && yield("b") && yield("c")
	panic("bye")
}

the following two ways of using it are not equvalent.

	for v := range abcThenPanic {
		defer func() {
			recover()
		}()
		fmt.Print(v)
	}

	abcThenPanic(func(v string) bool {
		defer func() {
			recover()
		}()
		fmt.Print(v)
		return true
	})

When loop bodies contain goto or return statements, the equivalence between the two ways becomes even less likely.

The iterator design has a flaw

The following two programs should behave the same, but they don't now (as of Go toolchain v1.24.1):

package main

func main() {
	defer func() {
		var v = recover()
		println(v == 123)
	}()
	for range iter {
		panic(123)
	}
}

func iter(yield func() bool) {
	defer func() {
		recover()
	}()
	yield()
}

The program prints false.

package main

func main() {
	defer func() {
		var v = recover()
		println(v == 123)
	}()
	for range iter {}
	panic(123)
}

func iter(yield func() bool) {
	defer func() {
		recover()
	}()
	yield()
}

The program prints true.

The cause of the behavior difference is, by the current design,
  1. the iterator function should not recover the panic created in the loop body. If the iterator function recovers the panic propagated from the loop body, then the runtime will create a new runtime panic implicitly.

If the yield call just returns false when the loop body creates a panic, then there will no behavior differences between the two programs.

The implementation of iterators still has some bugs

For example, the following two programs should have the same behavior, but they don't now (as of Go toolchain v1.24.1).

package main

import "fmt"

func main() {
	defer foo()
	panic(123)
}

func foo() {
	for range iter {
		fmt.Println(recover())
	}
}

func iter(yield func() bool) {
	yield()
}

The program prints <nil> then crashes.

package main

import "fmt"

func main() {
	defer foo()
	panic(123)
}

func foo() {
	for range 1 {
		fmt.Println(recover())
	}
}

The program prints 123 then exits normally.

The importance of iterators has not been proved yet

Iterators surely have certain usefulness. However, it is still not proved that it is worth adding so much complexity to the language.


(more articles ↡)

The Go 101 project is hosted on Github. Welcome to improve Go 101 articles by submitting corrections for all kinds of mistakes, such as typos, grammar errors, wording inaccuracies, description flaws, code bugs and broken links.

If you would like to learn some Go details and facts every serveral days, please follow Go 101's official Twitter account @zigo_101.

Tapir, the author of Go 101, has been on writing the Go 101 series books and maintaining the go101.org website since 2016 July. New contents will be continually added to the book and the website from time to time. Tapir is also an indie game developer. You can also support Go 101 by playing Tapir's games (made for both Android and iPhone/iPad):
Individual donations via PayPal are also welcome.

Articles in this book: