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
- The
For statements with range clause section of the language reference has two interesting examples (scroll down in this section to see them):
- An iterator function that endlessly generates Fibonacci numbers (the client has to break the loop),
- and an iterator method that traverses a binary tree. (Just from looking at the code, can you tell the sequence in which the tree nodes’ keys and values are yielded to the loop body?)
- Go range iterators demystified | DoltHub Blog
- First impressions of Go 1.23's range-over-func feature - Boldly Go
- Iterators in Go — Bitfield Consulting