Go Tip Of The Week: Range-Over-Func In a Nutshell

Quite a few blog articles have discussed the upcoming range-over-func mechanism already, but what I miss is a concise how-to document. So here we go.

Problem

The situation: You created a custom container type for a library you are working on:

type MyCollection []string

Your library's client code shall be able to iterate over a collection's elements.

(Ignore for a moment that the custom type is just a slice in disguise and that you'd be able to just range-loop over it. I want the code to stay minimal, so I refrained from constructing something more complex than that. As a homework assignment, try building a range-over-func iterator for a linked list.)

Solution #1: before Go 1.23

With Go releases before 1.23, you would probably write a Loop() method that receives a func() from the client. Let's call the client's function yield(), because at every iteration, we yield the current key and value to this function:

func (m MyCollection) Loop(yield func(int, string) bool) {
	for k, v := range m {
		if !yield(k, v) {
			break
		}
	}
}

The Loop() method iterates over the elements of the collection and calls the client's yield() function at each iteration. (Yes, that's exactly how fs.WalkDir() iterates over a directory tree.)

The client's yield() function receives the key (in our example, it's just the slice index) and the value of each element of your container. If the client's func decides to break the loop, it returns false, else true. In case of false, the Loop() code would exit.

The client's code would look like:

m := MyCollection{...}
m.Loop(func(k int, v string) bool) {
	// Do something with k and v here
	fmt.Printf("%d: %s\n", k, v)
	// If you want to break the loop,
	// return false
	return true
}

This client loop is a bit ugly, isn't it? If we must create ugly code, we'd want to hide it in the library rather than forcing it upon the client.

Moreover, your library users surely would love to be able to use the well-known for range loop.

But how can we magically summon key and value inside the client's loop body, at each iteration?

Solution #2: range-over-func

With the help of Go 1.23, we simply delegate the calling of Loop() to the range iterator. The difference to the pre-Go-1.23 code is entirely on the library client's side:

for k, v := range m.Loop {
	fmt.Printf("%d: %s\n", k, v)
}

Here you have it: a custom loop function used by the standard range operator. Now let your imagination go wild and write iterators for the weirdest collection types you can imagine!

(Play with this code in the Go Playground.)

Yield has variants

The yield function above has two parameters, key and value. You can also define and use yield with one parameter only, where you can either yield the current key or the current value to the loop body. Or you can even use yield with zero parameters! The loop body would neither receive a key nor a value. That would probably be good for just counting elements.

Further reading