Mark Reed
Mark Reed

Reputation: 95375

How to assign to struct fields from an array of values in order?

I know you can create a struct with a literal, listing the fields in order:

type Foo struct {
    A string 
    B string
    C string 
}

foo := Foo{ "foo", "bar", "baz" }

Is there any way to do the same thing dynamically? I have an array of values (actually an array of arrays) and I want to assign them to an array of structs in field order, and there are rather more than three fields. Is there a way to say "assign this struct's fields from this array of values in order"? I really don't want to write a bunch of structArray[i].field1 = dataArray[i][0]; structArray[i].field2 = dataArray[i][1], etc.

My thoughts so far have been to use reflect, which seems overkillish, or maybe to create an array of field names and build json strings and unmarshal them. Any better ideas?

Upvotes: 1

Views: 400

Answers (1)

blackgreen
blackgreen

Reputation: 45162

With reflection you can write a function like this:

func populate(dst any, src any) {
    v := reflect.ValueOf(dst)
    if v.Type().Kind() != reflect.Pointer {
        panic("dst must be a pointer")
    }
    v = v.Elem()
    if v.Type().Kind() != reflect.Struct {
        panic("dst must be a pointer to struct")
    } 

    w := reflect.ValueOf(src)
    if w.Type().Kind() != reflect.Slice {
        panic("src must be a slice")
    }
    for i := 0; i < v.NumField(); i++ {
        // in case you need to support source slices of arbitrary types
        value := w.Index(i)
        if value.Type().Kind() == reflect.Interface {
            value = value.Elem()
        }
        v.Field(i).Set(value)
    }
}

You must make sure that dst is addressable, hence pass a pointer to Foo into populate; and that the i-th element in the source slice is actually assignable to the corresponding i-th field in the struct.

The code above is in a quite simplified form. You can add additional checks to it, e.g. with CanAddr or AssignableTo, if you think callers may misbehave.

Call it like:

func main() {
    f := Foo{}
    populate(&f, []string{"foo", "bar", "baz"})
    fmt.Println(f) // {foo bar baz}
}

Here's a playground that also shows that you can pass a slice of []any as the source slice, in case the struct fields aren't all the same type: https://go.dev/play/p/G8qjDCt79C7

Upvotes: 4

Related Questions