Go 1.23 standardizes the protocol for user-defined iteration
Range-over-function gives Go collections a common iteration contract. Its callback result, pull adapter, and standard-library consumers define how values and early termination move through that contract.
Returning a slice gives a caller a familiar way to loop, but it also commits the producer to constructing that slice. A custom callback avoids that commitment while leaving each library to choose its own iteration convention. Go 1.23, released on August 13, adds a common alternative. A range loop can call an iterator function, and standard-library operations can consume the same function shape.
The consequential change is agreement about how a producer supplies a value and how a consumer asks it to stop. The new iter package names that agreement. Additions in maps and slices make it useful without requiring a separate collection framework.
I like Go's preference for abstractions whose behavior remains apparent at the call site. Iterators can help preserve that property when an API needs to traverse a custom collection. The value depends on keeping the protocol small enough that a reader can understand its stopping and cleanup behavior.
The iterator calls the loop body through yield
The Go 1.23 release notes list three accepted iterator-function shapes. They take a callback that returns a boolean and accepts zero, one, or two values. The zero-value shape permits repeated execution without exposing a value. The other shapes supply one item or a pair to each iteration.
The release-tag documentation for iter calls the callback yield. An iterator calls it for successive elements. A true result means to continue, while false means to stop early. The producer must honor that false result and finish the iteration.
The package defines Seq[V] for single values and Seq2[K, V] for pairs. A pair can represent a key and value, or an index and value. Naming the generic types does not impose a particular collection structure. A producer supplies the sequence through the callback convention, and a consumer accepts the appropriate iterator type.
The boolean result is necessary because the producer controls traversal. A consumer may finish before the producer runs out of values. Returning false gives that decision a path back into the producer's control flow. A producer that ignores the result fails the protocol even if its ordinary complete traversal appears correct.
This makes early termination part of the API contract. It cannot be treated as an optional optimization in an adapter. An adapter that continues consuming its input after the downstream consumer stops changes the work performed and may delay the producer's cleanup.
Collection operations compose through the shared type
The release announcement gives a concise example of collecting and sorting map keys. With maps and slices imported and a map named m, the expression is:
slices.Sorted(maps.Keys(m))
maps.Keys returns an iterator. slices.Sorted consumes its values, collects them into a slice, and sorts that slice. The final allocation is an explicit consequence of asking for a sorted slice. The producer itself does not need to materialize a separate key slice before a caller decides what to do with the sequence.
Other additions let callers choose that boundary. slices.Collect constructs a slice from an iterator, while AppendSeq appends yielded values to an existing slice. maps.Collect constructs a map from yielded key-value pairs. maps.Insert adds those pairs to an existing map.
The slice producers also distinguish traversal choices. Values yields elements, All yields indexes and elements, and Backward reverses traversal order. These operations expose sequence choices without requiring each consumer to know the producer's internal representation.
The shared function types matter more than the number of helper functions. A third-party collection can return iter.Seq[V], and a standard-library consumer can accept it. A custom adapter can also accept one sequence and return another. Compatibility follows from the ordinary type contract, so libraries do not have to agree on a common container implementation.
Pulling one value requires a stopping function
A range loop is a natural consumer when it can process each value as the producer yields it. Other algorithms need more control over when the next value arrives. The iter documentation gives pairing successive values as one example. A consumer may need to obtain one value, then another, before deciding what to emit.
iter.Pull adapts a Seq into two functions named next and stop. Calling next returns a value and a boolean indicating whether that value exists. At the end of the sequence, it returns the value type's zero value and false. Pull2 provides the corresponding operation for sequences of pairs.
The stopping function has a resource-lifetime purpose. If a consumer stops before next reports exhaustion, it must call stop so the iterator can finish and return. The package documentation recommends deferring that call. The lifetime obligation remains even when the consumer only wanted one or two values.
The contract permits calling stop after exhaustion and permits multiple calls. It also permits further next calls after stopping, which continue returning no value. Those properties make cleanup straightforward to arrange at the adapter's entry point, without tracking whether every possible return path exhausted the sequence.
The adapter is not a concurrent work-queue interface. The documentation says calling next or stop simultaneously from multiple goroutines is an error. It also specifies that an iterator panic during one of those calls propagates through the call. These are synchronous iteration semantics that a consumer can reason about without assuming independent background production.
Reusability and mutation remain explicit contracts
Most iterators can start a sequence again when called again. The package documentation also recognizes single-use iterators, typically over streams that cannot rewind. Calling one after stopping early may continue the stream. Calling it after exhaustion yields nothing. Functions that return such iterators should document that behavior.
The type alone does not promise a repeatable sequence. That is a useful limitation to make explicit. Two functions can share iter.Seq[V] while differing in whether their underlying source can restart. A consumer that needs two passes must rely on the producer's documented behavior or collect the values it needs.
Mutation likewise requires a collection-specific agreement. Iterators supply sequence values without an inherent operation for changing the source. The documentation suggests yielding position objects when a collection needs to expose mutation during traversal. Such an object can provide methods for reading, replacing, or deleting the current element.
The example limits a position's validity to its yield call. That keeps a traversal-specific location from becoming an indefinitely retained mutable reference. The sequence contract communicates iteration, while the position type communicates the collection's mutation rules. Standardizing one does not eliminate the need to specify the other.
I consider these limits appropriate. A common loop protocol should make libraries interoperable without pretending that a stream, a slice, and a mutable tree have identical lifetime rules. Those differences belong in producer documentation and explicit collection operations.
The release also changes timer lifetime guarantees
The release notes describe another change concerned with stopping correctly. Unreferenced timers and tickers become eligible for garbage collection even if their Stop methods were not called. Previously an unstopped timer remained until it fired, while an unstopped ticker was not collected.
Timer channels also become unbuffered. The new behavior guarantees that values prepared before a Reset or Stop call will not be sent or received after the call. The old one-element buffer made those operations difficult to use correctly when a stale value could remain available.
The main module's Go version selects the new timer behavior. Modules declaring Go 1.23.0 or later receive it, while older programs retain the prior behavior. The asynctimerchan=1 GODEBUG setting can restore the old behavior. This is a separate compatibility decision from simply obtaining a Go 1.23 toolchain.
Toolchain additions have distinct scopes
Go 1.23's telemetry system defaults to local counter collection. Uploading reports requires opting in with go telemetry on. The release notes describe weekly uploads of anonymous counter reports in that mode. Distinguishing local collection from uploading is necessary to describe the default correctly.
Generic type aliases are a preview. Enabling GOEXPERIMENT=aliastypeparams while building the toolchain permits them within a package. Cross-package use is not supported by this preview. That is a different maturity level from range-over-function, which is part of the released language.
For iteration, the practical result is already concrete. A producer can return a standard sequence type, a range loop can consume it, and a caller can choose when to collect the results. Early termination travels back through yield, while pull consumers have an explicit stop obligation. Those rules let a library expose traversal without committing every caller to a prebuilt slice.