Proposal Details
reflect.Type.Fields() iter.Seq[StructField]
and reflect.Type.Methods() iter.Seq[Method]
would be more convenient than having to call i := range myType.NumFields()
and myType.Field(i)
etc.
You could also add iterators to reflect.Value
as well but those are less obviously useful. Probably worth doing just for completeness though.
Edit: The proposal is now to return an iter.Seq2[int, StructField]
so you can use .Field(i)
.
Comment From: thediveo
could this be a duplicate of #66626?
Comment From: adonovan
could this be a duplicate of https://github.com/golang/go/issues/66626?
No, that issue relates to the go/types package, not reflect.
Comment From: thediveo
ah, overlooked this crucial detail, thank you!
Comment From: earthboundkid
This was in fact inspired by #66626, but yes, it is for reflect, not types.
Comment From: adonovan
Alternatively, the method value could be the iterator as opposed to returning an iterator, like so:
package reflect
type Type interface {
...
// Fields is a go1.23 iterator over all the fields of a struct type (etc).
//
// Example: for ftype := range t.Fields { ... }
Fields(yield func(ftype *Func) bool)
}
func (t *rtype) Fields(yield func(ftype *Func) bool) {
for i := range t.NumFields() {
if !yield(t.Field(i)) {
break
}
}
}
// client code
var t reflect.Type = ...
for ftype := range t.Fields { ... }
Comment From: earthboundkid
Maybe. That keeps from introducing an import dependency we might not want, but it goes against the general conventions used elsewhere.
Comment From: earthboundkid
I used this in a project and the signature should be iter.Seq2[int, reflect.StructField]
so that you can do rv.Field(i).Set(whatever)
if the field matches some criteria. It's very convenient though and worth having.
Comment From: DeedleFake
I'd like an iterator version of VisibleFields()
, too. I had a need for that the other day, actually.
Comment From: earthboundkid
Isn't that just a filter on field.IsExported() or is there more to it than that? I did only want the exported fields for my thing, but I also needed a type and tag test for the field, so having VisibleFields() per se wouldn't have saved much coding.
Comment From: DeedleFake
VisibleFields()
gets the fields recursively into anonymous struct fields as well as unexported ones.
Comment From: earthboundkid
Got it. There's already a slice version of VisibleFields(), so I'm not sure how much the iterator version saves since you'll presumably cache the slice somehow if performance is a concern. I'm willing to be wrong though. There could be AllVisibleFields and VisibleFields could just call and collect that.
Comment From: myaaaaaaaaa
It's possible to take Fields()
out of the reflect
package so that it can benefit more users:
package structs
func Fields(strukt any) iter.Seq2[string, any] {
return func(yield func(string, any) bool) {
strukt := reflect.ValueOf(strukt)
isPointer := strukt.Kind() == reflect.Pointer
if isPointer {
strukt = strukt.Elem()
}
for i := range strukt.NumField() {
fieldName := strukt.Type().Field(i).Name
fieldVal := strukt.Field(i)
if isPointer {
fieldVal = fieldVal.Addr()
}
if !yield(fieldName, fieldVal.Interface()) {
return
}
}
}
}
Usage would be like so:
type vars struct {
Editor string
Lang string
Shell string
}
func ExampleReadFields() {
env := vars{
Editor: "vim",
Shell: "/bin/dash",
}
for name, val := range structs.Fields(env) {
name := strings.ToUpper(name)
val := fmt.Sprint(val) // val is a string because env was passed by value
os.Setenv(name, val)
}
fmt.Println(os.Environ())
//Output:
// [... EDITOR=vim SHELL=/bin/dash ...]
}
func ExampleWriteFields() {
var env vars
for name, val := range structs.Fields(&env) {
name := strings.ToUpper(name)
*val.(*string) = os.Getenv(name) // val is a *string because &env was passed
}
fmt.Printf("%#v\n", env)
//Output:
// {Editor:"nano", Lang:"en_US.utf8", Shell:"/bin/bash"}
}
func ExampleStringKeys() {
var env vars
{
envMap := maps.Collect(structs.Fields(&env))
*envMap["Editor"].(*string) = "code"
*envMap["Shell"].(*string) = "/bin/zsh"
}
fmt.Printf("%#v\n", env)
//Output:
// {Editor:"code", Lang:"", Shell:"/bin/zsh"}
}
This is also possible for Methods()
, although I don't know anything about the real-world usage for that.
Comment From: earthboundkid
Package structs is about compiler magic fields. I'm not sure how it would help to move something out of reflect and into a different package if that package just imports reflect itself.
Comment From: myaaaaaaaaa
Package structs is about compiler magic fields.
It seemed like the natural place to put it (akin to slices
for slice functions and maps
for map functions). Where else would you suggest?
I'm not sure how it would help to move something out of reflect and into a different package if that package just imports reflect itself.
Since there's no reference to reflect
types in the signature, it allows users to avoid importing the reflect
package, much like how encoding/json
uses reflection but doesn't expose it.
Comment From: adonovan
This Fields
function makes too many arbitrary choices: it returns field values or field addresses depending on whether the argument was a pointer; it accepts a pointer but panics if given a nil pointer; it discards type information; and it panics if any field is unexported. Each of these choices may be right for your project, but the result is not general enough to belong in the standard library. Users can always customize the loop below for their own needs:
s := reflect.ValueOf(s)
for i := range s.NumField() {
use(s.Type().Field(i), s.Field(i))
}
Comment From: myaaaaaaaaa
it returns field values or field addresses depending on whether the argument was a pointer
This was intended to improve usability, and to make it more generalized and intuitive. Restricting it to pointer-to-structs would also work, although that seems more arbitrary to me.
it accepts a pointer but panics if given a nil pointer; and it panics if any field is unexported.
These were mistakes, not design choices. Sorry about that.
it discards type information
Can you elaborate on this? From a cursory glance, it seems like type information can be fully recovered from the field pointer.
for name, val := range structs.Fields(&env) {
val := reflect.TypeOf(val)
// ...
}
Users can always customize the loop below for their own needs
On the other hand, that would also seem to be an argument against the currently proposed Type.Fields()
.
Comment From: adonovan
From a cursory glance, it seems like type information can be fully recovered from the field pointer.
This returns the type of the field value, but not the reflect.StructField describing the field itself.
On the other hand, that would also seem to be an argument against the currently proposed Type.Fields().
Not at all. This proposal is for a modest and uncontroversial convenience method to enumerate each element of the Type.Field(i) sequence using go1.23 iterator conventions, nothing more.
Comment From: myaaaaaaaaa
On the other hand, that would also seem to be an argument against the currently proposed Type.Fields().
Not at all. This proposal is for a modest and uncontroversial convenience method to enumerate each element of the Type.Field(i) sequence using go1.23 iterator conventions, nothing more.
In that case, should I file structs.Fields()
as a separate proposal? Is there room in the standard library for both structs.Fields()
and reflect.Type.Fields()
?
Comment From: ianlancetaylor
I understand the appear of the name structs.Fields
, but if we had such a function I do think it should be in the reflect package. In particular I think it would be something like
// Fields returns an iterator over the values of the fields of the struct v.
// It panics if v's kind is not [Struct].
func (v Value) Fields() iter.Seq[Value] {
return func(func (Value) bool) {
typ := v.Type()
for i := range typ.NumFields() {
if !yield(v.Field(i)) {
return
}
}
}
}
If we really want the field name also, it should perhaps be iter.Seq2[StructField, value]
.
I don't see any problem having both reflect.Type.Fields
and reflect.Value.Fields
. I do think they should be separate proposals.
Comment From: myaaaaaaaaa
I understand the appear of the name
structs.Fields
, but if we had such a function I do think it should be in the reflect package. In particular I think it would be something like
go func (v reflect.Value) Fields() iter.Seq[reflect.Value]
Sorry for making things confusing - structs.Fields(any) iter.Seq2[string, any]
is a completely separate (and higher-level) function that iterates through structs as if they were map[string]any
s, without needing to involve reflect
types, but I mentioned it here just in case due to the overlap in functionality.
I don't see any problem having both
reflect.Type.Fields
andreflect.Value.Fields
. I do think they should be separate proposals.
To clarify, does that mean structs.Fields
(as described above) is also different enough to be a separate, third proposal?
Comment From: ianlancetaylor
To clarify, does that mean
structs.Fields
(as described above) is also different enough to be a separate, third proposal?
Yes. I don't think they really have anything to do with each other.
Comment From: dsnet
As a data point, I'm updating a bunch of Go code to use range-over-int, and I'm discovering many cases of:
for i := range t.NumField() {
f := t.Field(i)
...
}
In fact, in the particular codebase I'm working on, that's the most common reason (~95%) to ever call NumField
and Field
is to iterate over all the fields.
Comment From: adonovan
In fact, in the particular codebase I'm working on, that's the most common reason (~95%) to ever call
NumField
andField
is to iterate over all the fields.
Unfortunately all your pleasing uses of range t.NumField()
will go away when in go1.24 you'll be able to say just for f := range t.Fields() { ... }
. ;-)
Comment From: dsnet
Such is the life of someone who cares about modern Go code 😫. I think I held the record for most Go code commits in a single day before I left Google.
Comment From: andig
I don't find t.Fields()
in https://github.com/golang/go/blob/master/src/reflect/type.go?
Comment From: earthboundkid
This proposal hasn't been evaluated yet. https://github.com/golang/go/issues/66626 will be in Go 1.24, however.
Comment From: mvdan
@ianlancetaylor could this proposal be considered for the current Go 1.26 release cycle? It seems fairly uncontroversial, and it would be a nice quality of life improvement for the API :)
Comment From: adonovan
@ianlancetaylor could this proposal be considered for the current Go 1.26 release cycle? It seems fairly uncontroversial, and it would be a nice quality of life improvement for the API :)
I agree; I'll make sure we look at it in time for the 1.26 cycle.
Comment From: earthboundkid
The current version of the proposal is
func (v Value) Fields() iter.Seq2[StructField, Value]
func (v Value) Methods() iter.Seq2[Method, Value]
type Type interface {
// ...
Fields() iter.Seq[StructField]
Methods() iter.Seq[Method]
}
~~It should step through embedded fields, so~~ edit: Use reflect.VisibleFields if you want nested fields. This will just return the top level fields.
type user struct {
firstName string
lastName string
}
type data struct {
user
firstName string
lastName string
}
This will yield user, firstName, lastName. If you need the index, use StructField.Index.
Comment From: apparentlymart
It should step through embedded fields, so [...]
[...] If you need the index, use
StructField.Index
.
The combination of these two seems a little "tricky": correctly using StructField.Index
presumably means that you also need to use StructField.Type
to know which type to call Field
on. It also seems hazardous that Type.Field
/Type.NumField
work shallowly while the similarly-named Type.Fields
does not. Finally, it would presumably need to behave differently if the embedded type is not a struct type, since there would be no nested fields to describe in that case.
It seems less confusing overall for Type.Fields
to not automatically recurse into embedded types in any case. Perhaps we'd consider a separate function for automatically recursing later, but perhaps its result should be iter.Seq2[[]int, StructField]
where that []int
corresponds to the argument of Type.FieldByIndex
, describing the "index path" to each item. However, I think we should wait to see if that's needed first, since the hypothetical recursive version could be built in terms of the non-recursive version.
Comment From: aclements
This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — aclements for the proposal review group