golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.99k stars 17.54k forks source link

proposal: structs: add `Fields()` iterator #69398

Closed myaaaaaaaaa closed 1 week ago

myaaaaaaaaa commented 1 week ago

Proposal Details

Currently, the only way to dynamically access the fields in a struct is through reflection. Having a simple way to range over structs as if they were map[string]anys would enable various struct idioms to be used more often.

See below for some examples of such idioms:


type vars struct {
    Editor string
    Shell  string
    Pid    int
}

func ExampleGetFields() {
    env := vars{
        Editor: "vim",
        Shell:  "/bin/dash",
        Pid:    100,
    }

    for name, val := range structs.Fields(env) {
        name := strings.ToUpper(name)
        val := fmt.Sprint(val) // val is a string/int because env was passed by value
        os.Setenv(name, val)
    }

    fmt.Println(os.Environ())

    //Output:
    // [...    EDITOR=vim    SHELL=/bin/dash    PID=100    ...]
}
func ExampleSetFields() {
    var env vars

    for name, val := range structs.Fields(&env) {
        name := strings.ToUpper(name)
        switch val := val.(type) { // val is a *string/*int because &env was passed
        case *string:
            *val = os.Getenv(name)
        case *int:
            *val, _ = strconv.Atoi(os.Getenv(name))
        }
    }

    fmt.Printf("%#v\n", env)

    //Output:
    // {Editor:"nano", Shell:"/bin/bash", Pid:2499}
}
func ExampleStringKeys() {
    var env vars

    {
        envMap := maps.Collect(structs.Fields(&env))
        *envMap["Editor"].(*string) = "code"
        *envMap["Shell"].(*string) = "/bin/zsh"

        pidKey := "P" + strings.ToLower("ID")
        *envMap[pidKey].(*int) = 42
    }

    fmt.Printf("%#v\n", env)

    //Output:
    // {Editor:"code", Shell:"/bin/zsh", Pid:42}
}

Sample implementation (may contain errors):

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 {
            if strukt.IsNil() {
                return
            }
            strukt = strukt.Elem()
        }

        for i := range strukt.NumField() {
            field := strukt.Type().Field(i)
            if !field.IsExported() {
                continue
            }

            fieldName := field.Name
            fieldVal := strukt.Field(i)
            if isPointer {
                fieldVal = fieldVal.Addr()
            }

            if !yield(fieldName, fieldVal.Interface()) {
                return
            }
        }
    }
}
seankhliao commented 1 week ago

reflection is the intended way to work with this, it doesn't make sense to put this in Fields.