Skip to content

proposal: reflect: add reflect.Value.Elems for slices, arrays, and strings #76357

@mvdan

Description

@mvdan

Background

#66056 gave us iterator APIs on reflect.Value, which are great. I went ahead and started using them in our code, which led to simpler code.

What I didn't notice at the time is that I actually added a number of allocations per iteration: https://review.gerrithub.io/c/cue-lang/cue/+/1226673

Below is a set of benchmarks which shows this problem:

package p

import (
	"reflect"
	"testing"
)

var slice = []string{"a", "b", "c", "d", "e", "f", "g"}

func foreachSlice(i int, elem reflect.Value) {
	// we need an integer index here
}

func BenchmarkSliceValueIndex(b *testing.B) {
	for b.Loop() {
		val := reflect.ValueOf(slice)
		for i := range val.Len() {
			elem := val.Index(i)
			foreachSlice(i, elem)
		}
	}
}

func BenchmarkSliceValueSeq2WithIndex(b *testing.B) {
	for b.Loop() {
		val := reflect.ValueOf(slice)
		i := 0
		for _, elem := range val.Seq2() {
			foreachSlice(i, elem)
			i++
		}
	}
}

func BenchmarkSliceValueSeq2(b *testing.B) {
	for b.Loop() {
		val := reflect.ValueOf(slice)
		for ival, elem := range val.Seq2() {
			i := int(ival.Int())
			foreachSlice(i, elem)
		}
	}
}

As of go version go1.26-devel_d55ecea9e5 2025-11-14 15:13:00 -0800 linux/amd64, I see:

goos: linux
goarch: amd64
pkg: test
cpu: AMD Ryzen AI 9 HX 370 w/ Radeon 890M           
BenchmarkSliceValueIndex-24            	66029337	        18.00 ns/op	       0 B/op	       0 allocs/op
BenchmarkSliceValueSeq2WithIndex-24    	13339879	        86.85 ns/op	     104 B/op	       5 allocs/op
BenchmarkSliceValueSeq2-24             	14808429	        76.92 ns/op	      88 B/op	       5 allocs/op

In other words, the classic integer iteration doesn't allocate and is pretty fast, but both of the two reasonable ways to use Seq2 to iterate over the slice values seem to allocate and are significantly slower.

I also cannot use Seq here, as it iterates over the slice indices, not the elements, much like for i := range slice {...}.

When dealing with reflect.Slice, but also similar "indexed sequence" containers like reflect.Array, and reflect.String, it's common to want to iterate over their elements via iter.Seq2[int, Value], much like for i, v := range slice {...} gives you (int, T). Seq2 is fine, but its iter.Seq2[Value, Value] seems most appropriate for cases where the key can be of any type, such as a map or an iterator func.

Proposal

I propose that we add this new API:

// Elems returns an iter.Seq2[int, Value] that loops over the elements of v.
// v's kind must be Array, Slice, or String
func (v Value) Elems() iter.Seq2[int, Value]

The advantages of this API are two-fold:

  1. Performance; see above how trying to use Seq2 is slower and allocates more.
  2. Usability; the code sample below is shorter and simpler than the three existing ones above. Or one could even imagine that foreachSlice could take the iter.Seq2[int, reflect.Value] directly, as no wrapping is necessary anymore.
func BenchmarkSliceValueSeq2(b *testing.B) {
	for b.Loop() {
		val := reflect.ValueOf(slice)
		for i, elem := range val.Elems() {
			foreachSlice(i, elem)
		}
	}
}

Alternatives

If we improved reflect.Value.Seq2 such that the original benchmarks were all roughly equivalent, then point 1 (performance) would be largely moot. I'm not sure if that is possible. However, I'd argue that point 2 (usability) is still valid; I don't find any of the existing three approaches particularly nice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions