The Go programming language, renowned for its simplicity and efficiency, has changed quite a bit in the last couple of versions, with generics being one of the bigger changes.

Iterators in Go 1.23


The Go programming language, renowned for its simplicity and efficiency, has changed quite a bit in the last couple of versions, with generics being one of the bigger changes. One of the latest additions, introduced in Go 1.23, is the concept of iterators. This feature marks a shift in the language, bringing a little controversy to the Go community (more on this below). In this blog, we will delve into what iterators are, how they compare to previous versions of Go, and the potential impact they might have on the way we write Go code going forward.

If you want to know more about the Go 1.23 release, you can take our free course, Go 1.23 in 23 minutes.

Understanding Iterators in Go

Iterators are constructs that allow for traversing over a sequence of elements. They are widely used in many programming languages for their ability to provide a clean and efficient way to handle collections of data. With the introduction of iterators in Go 1.23, the language now includes an iter package, which defines basic operations related to iterating over sequences.

An iterator is a function that goes through elements of a sequence one by one and sends them to a callback function, usually called yield. The function stops when it reaches the end of the sequence or when yield signals to stop early by returning false. This package provides Seq and Seq2 (pronounced like “seek”), which are shortcuts for iterators that pass either 1 or 2 values from each sequence element to yield.

type (
    Seq[V any]     func(yield func(V) bool)
    Seq2[K, V any] func(yield func(K, V) bool)
)

Seq2 represents a sequence of paired values, typically used for key-value or index-value pairs.

The yield function returns true if the iterator should continue to the next element in the sequence, and false if it should stop.

Iterator functions are usually used within a range loop, like this:

func PrintAll[V any](seq iter.Seq[V]) {
    for v := range seq {
        fmt.Println(v)
    }
}

New Standard Library Packages Using Iterators

The slices and map packages add several functions that work with iterators. For slices, we now have:

  • All: returns an iterator over slice indexes and values.
  • Values: returns an iterator over slice elements.
  • Backward: returns an iterator that loops over a slice backward.
  • Collect: collects values from an iterator into a new slice.
  • AppendSeq: appends values from an iterator to an existing slice.
  • Sorted: collects values from an iterator into a new slice, and then sorts the slice.
  • SortedFunc: is like Sorted but with a comparison function.
  • SortedStableFunc: is like SortFunc but uses a stable sort algorithm.
  • Chunk: returns an iterator over consecutive sub-slices of up to n elements of a slice.

And for Maps:

  • All: returns an iterator over key-value pairs from a map.
  • Keys: returns an iterator over keys in a map.
  • Values: returns an iterator over values in a map.
  • Insert: adds the key-value pairs from an iterator to an existing map.
  • Collect: collects key-value pairs from an iterator into a new map and returns it.

How Will Iterators Change the Way I Write Go?

To illustrate the differences and advantages of using iterators, let’s compare two simple programs written in Go 1.22 and Go 1.23.

Go 1.22

Consider a function called backwards that iterates through a slice in reverse order and prints each element. In Go 1.22, this function might look like the following:

package main

import (
   "fmt"
)

func Backward(s []string) {
   for i := len(s) - 1; i >= 0; i-- {
       fmt.Println(i, s[i])
   }
}

func main() {
   s := []string{"hello", "world"}
   Backward(s)
}

Go 1.23

Here’s a similar version of the code in Go 1.23:

package main

import (
   "fmt"
)

func Backward(s []string) func(func(int, string) bool) {
   return func(yield func(int, string) bool) {
       for i := len(s) - 1; i >= 0; i-- {
           if !yield(i, s[i]) {
               return
           }
       }
   }
}

func main() {
   s := []string{"hello", "world"}
   for i, x := range Backward(s) {
       fmt.Println(i, x)
   }
}

In this version, the backwards function takes an additional yield function as a parameter. This yield function allows the caller to control the iteration process. The iteration stops when yield returns false, providing a mechanism to terminate the loop early if needed.

The new iterator feature introduces more control over the iteration process. For example, in the Go 1.23 version, we don’t have to just print with our backward func. Furthermore, the whole slice does not need to be processed for us to take action on it; we can operate on a few elements and then signal to the iterator we are done if we want to.

Why Add Iterators?

The introduction of iterators in Go 1.23 is closely tied to the language’s support for generics. The Go team anticipates that iterators will be useful for traversing complex data structures such as trees or linked lists, as well as for handling large files or data streams. While the initial implementation might seem more complex, the potential for more readable and maintainable code is significant, especially in large-scale applications. However, many asked the same question: was this really the most important language feature to add? Will it make Go code more complicated?

Want to Know More About the New Features in Go 1.23?

Check out our free course Go 1.23 in 23 minutes.