July 14th, 2024

Go Range Iterators Demystified

The Go 1.23 release introduces range iterators for custom collection types, offering flexibility for iteration beyond maps and slices. These iterators support various loop forms and enable powerful iteration scenarios.

Read original articleLink Icon
Go Range Iterators Demystified

In the latest Go 1.23 release, range iterators have been introduced, allowing iteration over custom collection types using the range keyword. Three types of range iterator functions are available, each corresponding to different forms of the range loop. These functions can take 0, 1, or 2 arguments, enabling various iteration scenarios. The yield function within a range iterator is crucial, as it invokes the loop body and controls loop continuation based on its return value.

Range iterators offer flexibility for iterating over collections beyond maps and slices, enabling scenarios like filtering values based on specific criteria, handling errors during iteration, and converting traditional iterators to be compatible with the range keyword. While this new feature may challenge existing conventions, such as returning only values without keys or indexes, it provides a concise and powerful way to work with custom collection types. As developers explore and adopt range iterators, their diverse applications and potential benefits are expected to become clearer over time.

Link Icon 11 comments
By @deergomoo - 4 months
I’m glad this is being added for the ergonomic benefits. But the number of times the article points out cases where conventions will need to be formed by the community makes me fear this will add to the already longer-than-I’d-like list of anti-patterns or footguns that Go’s design and type system makes easy to fall into.
By @Someone - 4 months
FTA: “Maybe you just want to use the range keyword to iterate over every element of your collection. Easy enough.

  func (s Slice) All() func(yield func(i int) bool) {
    return func(yield func(i int) bool) {
      for i := range s {
        if !yield(s[i]) {
          return
        }
      }
    }
  }

So, for an “easy enough” example correctly, you have to write func five times in order to, if I understand this correctly, wrote a function returning a function that takes a function as an argument?

For comparison, C# does that this way (https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...)

  IEnumerable<int> ProduceEvenNumbers(int upto)
  {
    for (int i = 0; i <= upto; i += 2)
    {
        yield return i;
    }
  }
Yes, that introduces “magic” where the runtime figures out that ProduceEvenNumbers won’t continue, but why give such functions the flexibility not to listen to such requests (in the golang version, is forgetting the if and just yielding instead ever useful?)
By @pjmlp - 4 months
I am all for having them in the language, however the way they have been designed, or how the new magic fields for structure aligment (in Go 1.23) are being designed, this shows how attacking other languages as PhD level complexity and then coming out with such special case designs, is kind of ironic.

I give it 10 more years of such special cased improved, for Go not to be any better than the languages the community regularly complains about, while Go is "perfect".

By @rundev - 4 months
`yield` being a function that is passed into the iterator seems like suboptimal design to me. Questions like "What happens if I store `yield` somewhere and call it long after the loop ended?" and "What happens if I call `yield` from another thread during the loop?" naturally arise. None of this can happen with a `yield` keyword like in JavaScript or C#. So why did the Go-lang people go for this design?
By @tapirl - 4 months
Correction:

    // Only care about the iteration count
    for range aContainer { ... }
    
    // Just the values
    for v := range myChannel { ... }

    // Indexes and values (or keys and values for a map)
    for i, v := range mySlice { ... }
By @pansa2 - 4 months
What's the rationale behind Go choosing internal iteration (the iterator calls a function for each value, like Ruby) over external (the iterator returns each value, like Python and C#)?

My understanding is that internal iteration makes it easier to write iterators (producers) but harder to write the consuming code. That's why Go needs to re-write the body of each `for` loop as a function body, including special handling for `break`, `return` etc.

External iteration OTOH makes it harder to write producers but easier to write consumers. Python and C# therefore allow external iterators to be written via coroutines/generators.

Wouldn't Go's goroutines make the coroutine approach to external iterators straightforward? Whereas the re-writing necessary for internal iterators seems convoluted?

By @neonsunset - 4 months
This looks incredibly clunky and counter-intuitive.

Compare that to yield return, .Select and .Where methods in C#, or Filter and Map in many popular languages - it's not a good look.

Or when writing manually, compare it to

    static IEnumerable<T> Filter<T>(
        IEnumerable<T> source, Func<T, bool> predicate)
    {
        foreach (var item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
Could also compare to how easy it is to use for extremely common patterns in general purpose code:

    var numbers = Enumerable.Range(0, 10);
    var even = numbers.Where(n => n % 2 is 0);
    var strings = even.Select(n => n.ToString());
By @meling - 4 months
I was hoping the blog author would have revealed some plans for supporting a new iteration API in dolt. The range over func API is particularly useful if you need to iterate and compute over something that doesn’t all fit in memory (as is necessary for a slice and map).
By @rochak - 4 months
The syntax just doesn’t sit right with me due to some reason. It gives me the same heebie jeebies as Python’s decorators. Not a fan of either. Maybe I need to get used to them.
By @asmor - 4 months
This feels like it's undermining channels, the feature go really wants you to use in other places too (but people tend to still use mutexes). Channels aren't quite as lazy (if you use an unbuffered one you supply one element in advance), but they're close.