The Go 1.23 release candidates are out. That means it’s time for another entry in the What’s New in Go series where I talk about my contribution to this release. For Go 1.23, I proposed (but did not implement) four new methods: reflect.Value.Seq, reflect.Value.Seq2, reflect.Type.CanSeq, and reflect.Type.CanSeq2. So, where do they come from and what do they do?


If you’ve been following what’s new in Go 1.23 at all, you’ve probably heard about new generic custom iteration functions. The signature for writing these iterators is a little tricky, but how you use them is simple.

Here’s an example:

// counter counts up from zero
func counter() iter.Seq[int] {
	// let's omit this for now
}

func main() {
	for n := range counter() {
		if n > 5 {
			break
		}
		fmt.Println(n)
	}
}

The output is what you would expect:

0
1
2
3
4
5

If using a custom iterator is simple, how do you implement it?

First, let’s define a similar function in Python:

def counter():
    n = 0
    while True:
        yield n
        n += 1

And in JavaScript:

function* counter() {
  let n = 0;
  while (true) {
    yield n;
    n++;
  }
}

Now the Go version:

// counter counts up from zero
func counter() iter.Seq[int] {
	return func(yield func(int) bool) {
		n := 0
		for {
			if !yield(n) {
				return
			}
			n++
		}
	}
}

What’s interesting about the Go version of iterators is that there is no special syntax for creating an iterator function like yield or function *. The type iter.Seq is a generic named type for func(yield func(V) bool) where V is any type. In other words, an iterator is just a regular function that receives a callback. The callback is called with values from the stream of iterated values and either returns true or false. If it returns true, the iterator should return the next value in the sequence. If it returns false, the iterator should do any necessary cleanup (none in this case) and stop iterating.

(Incidentally, in Go 1.24, this is likely to be able to be written as return func(yield iter.Yield[int]) { once generic type aliases are available. The old signature will still work, but the new one is slightly easier to write and read.)


You could already write something a lot like iterators with Go channels. In fact, I wrote a blog post about this ten (!) years ago.

The channel-based iterators in that blog post had three major problems:

  1. Because Go did not have generics at that time, you had to write new utilities for each type you wanted to stream.
  2. Because they used channels and goroutines, each value sent and received had to be scheduled by the Go runtime scheduler, which is slow.
  3. Because a channel should be either written to or read from in one place but not both, you needed to pass around cancel channels everywhere to signal that a generator should exit or you’ll be stuck with a lot of zombie goroutines.

Problem 1 has since been solved. Problem 2 is harder to solve, but in theory, it might be possible to optimize away some calls to the runtime scheduler under some circumstances. (Indeed, this is how some iterators now work under the hood.) But problem 3 was really fatal for the idea of using channels as iterators.

Here is how the use of a quit channel looks using some code adapted from my blog post of ten years ago:

func sendOrFail(n int64, out chan<- int64, quit <-chan struct{}) bool {
	select {
	case out <- n:
		return true
	case <-quit:
		return false
	}
}

func counter(quit <-chan struct{}) <-chan int64 {
	out := make(chan int64)
	go func() {
		defer close(out)
		var n int64

		if ok := sendOrFail(n, out, quit); !ok {
			return
		}
		n++
	}()
	return out
}

The blog post predated the invention of context.Context, so you could clean it up a little today by using a context instead of a quit channel, but the end result would be the same. You need to constantly pass around a close token to know when to stop iterating, and this gets quite complicated as you nest iterators. Here’s what using that could look like:

ctx, cancel := context.WithCancel(context.Background())
for n := range counter(ctx.Done()) {
	if n > 5 {
		break
	}
	fmt.Println(n)
}
cancel() // tell counter to close or it will leak forever!

Indeed if you took sendOrFail and tried to make it a little more generic so it takes a context and can automatically create and close the channel, it might look like this:

func buildChannel[T any](ctx context.Context, callback func(func(T) bool)) <-chan T {
	ch := make(chan T)
	yield := func(v T) bool {
		select {
		case ch <- v:
			return true
		case <-ctx.Done():
			return false
		}
	}
	go func() {
		defer close(ch)
		callback(yield)
	}()
	return ch
}

func counter(ctx context.Context) <-chan int64 {
	return buildChannel(ctx, func(yield func(int64) bool) {
		var n int64
		for {
			if !yield(n) {
				return
			}
			n++
		}
	})
}

In other words, you end up with the same signature as Go 1.23 iterators, just with an extra wrapper function and context argument!

Using the custom iterators actually added in Go 1.23 and writing return func(yield func(int) bool) is a little bit awkward, but it’s much better than needing to pass around close tokens and use them at every call site.


We’ve seen that Go iterators show that we live in the best of all possible worlds (or maybe not). What about reflect.Value.Seq, reflect.Value.Seq2, reflect.Type.CanSeq, and reflect.Type.CanSeq2?

The doc string for reflect.Value.Seq says,

Seq returns an iter.Seq[Value] that loops over the elements of v. If v’s kind is Func, it must be a function that has no results and that takes a single argument of type func(T) bool for some type T. If v’s kind is Pointer, the pointer element type must have kind Array. Otherwise v’s kind must be Int, Int8, Int16, Int32, Int64, Uint, Uint8, Uint16, Uint32, Uint64, Uintptr, Array, Chan, Map, Slice, or String.

Calling the Seq method is like calling for x := range v { xValue := reflect.ValueOf(x) } for an arbitrary type v. The CanSeq method just reports whether a type can be used as an iterator.

Possibly surprisingly, since it works just like a range statement, the value yielded by the Seq method when iterating over a slice will just be an integer representing the index of each slice item.

func printEach(x any) {
	v := reflect.ValueOf(x)
	if !v.Type().CanSeq() {
		panic("bad type")
	}
	for x := range v.Seq() {
		fmt.Println(x.Interface())
	}
}

func main() {
	printEach([]string{"a", "b", "c"})
}

Output:

0
1
2

To get the slice elements, use Seq2 and the second range value:

func printEach(x any) {
	v := reflect.ValueOf(x)
	if !v.Type().CanSeq2() {
		panic("bad type")
	}
	for x, y := range v.Seq2() {
		fmt.Println(x.Interface(), y.Interface())
	}
}

func main() {
	printEach([]string{"a", "b", "c"})
}

Output:

0 a
1 b
2 c

The reflect package already had a method for iterating over a map, reflect.Value.MapRange, but using it is not quite as nice as using reflect.Value.Seq2:

iter := reflect.ValueOf(m).MapRange()
for iter.Next() {
	k := iter.Key()
	v := iter.Value()
	// ...
}

// vs.

for k, v := range reflect.ValueOf(m).Seq2() {
	// ...
}

How did I come to propose these new methods? First, Github user gophun asked about how to call an iterator of an unknown type, for example when marshaling JSON. The answer was to write a little bit of reflect code. I opened an issue to add it to the reflect package itself, and once the proposal was approved (with improved method names suggested by Ian Lance Taylor), user qiulaidongfeng created a changelist to implement it before I got around to doing it. (This makes it the opposite of reflect.TypeFor, where I just implemented Josh Bleecher Snyder’s proposal after coming up with the name for the function.)

There was a subsequent issue to add support for range funcs to the Go template package that didn’t make it into Go 1.23, but should be in Go 1.24 that relies on this new feature of the reflect package.


That’s what I worked on for Go 1.23. The creation of Go itself is an iterative process, so I hope to return in six months to write about Go 1.24 as the next development cycle loops again.