26

I have a struct Person.

type Person struct {
    Firstname string       
    Lastname  string       
    Years     uint8       
}

Then I have two instances of this struct, PersonA and PersonB.

PersonA := {"", "Obama", 6}
PersonB := {"President", "Carter", 8}

I want to write a function that copies the values from PersonA to PersonB given some condition for each field (i.e. non-empty). I know how to do this by hard-coding the field names, but I want a function that works even if I change the Person struct.

I know Go reflections is helpful, but the issue is getting and setting the values requires knowing the types, if you want to use something like SetInt. But is there is a "simple" way to do this?

** Javascript analogy ** In Javascript, you could just do a (for property in someObject) to loop through.

(for propt in personA) {
  if personA[propt] != "" {
    // do something
    personB[propt] = personA[propt]
  }
}

Options I've ruled out:

  1. Keeping track of the fields in each struct in a map, then using a combination of FieldByName and the collection of Set* functions in the reflect pkg.

  2. Creating a loop through the fields of Person manually (below). Because I want to do this type of "update" for many other structs (School, Animals, etc...)

    if PersonA.Firstname != "" {
      PersonB.Firstname = PersonA.Firstname 
    }
    

    ...

    if PersonA.Years != "" {
      PersonB.Years = PersonA.Years 
    }
    

The question below gets me half-way there, but still isn't extensible to all structs for which I want to utilize this "update" function.

in golang, using reflect, how do you set the value of a struct field?

** Other Helpful Links ** GoLang: Access struct property by name

Community
  • 1
  • 1
platwp
  • 3,025
  • 4
  • 15
  • 10

6 Answers6

23

Use reflect.ValueOf() to convert to concrete type. After that you could use reflect.Value.SetString to set the value you want.

structValue := FooBar{Foo: "foo", Bar: 10}
fields := reflect.TypeOf(structValue)
values := reflect.ValueOf(structValue)

num := fields.NumField()

for i := 0; i < num; i++ {
    field := fields.Field(i)
    value := values.Field(i)
    fmt.Print("Type:", field.Type, ",", field.Name, "=", value, "\n")

    switch value.Kind() {
    case reflect.String:
        v := value.String()
        fmt.Print(v, "\n")
    case reflect.Int:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    case reflect.Int32:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    case reflect.Int64:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    default:
        assert.Fail(t, "Not support type of struct")
    }
}
Mojtaba
  • 6,012
  • 4
  • 26
  • 40
jtianling
  • 1,977
  • 15
  • 19
  • Sorry but this isn't working! https://go.dev/play/p/3-Swft2YjIK ; see https://stackoverflow.com/questions/67236727/reflect-value-string-not-changing-with-setstring for more – Cirelli94 Dec 06 '21 at 11:44
  • I fixed it! https://go.dev/play/p/kaac0W7Cnyf The issue is that `value.Kind()` should be `field.Type.Kind()` – Carlo Nyte Jul 13 '23 at 18:07
5

Here is the solution f2.Set(reflect.Value(f)) is the key here

package main

   import (
    "fmt"
    "reflect"
   )

   func main() {
    type T struct {
        A int
        B string
    }
    t := T{23, "skidoo"}
    t2:= T{}
    s := reflect.ValueOf(&t).Elem()
    s2 := reflect.ValueOf(&t2).Elem()
    typeOfT := s.Type()
    fmt.Println("t=",t)
    fmt.Println("t2=",t2)

    for i := 0; i < s.NumField(); i++ {
        f := s.Field(i)
        f2:= s2.Field(i)
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f.Type(), f.Interface())
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f2.Type(), f2.Interface())
        f2.Set(reflect.Value(f))
        fmt.Printf("%d: %s %s = %v\n", i,
            typeOfT.Field(i).Name, f2.Type(), f2.Interface())

    }
    fmt.Println("t=",t)
    fmt.Println("t2=",t2)
}

Output:

t= {23 skidoo}
t2= {0 }
0: A int = 23
0: A int = 0
0: A int = 23
1: B string = skidoo
1: B string = 
1: B string = skidoo
t= {23 skidoo}
t2= {23 skidoo}

http://play.golang.org/p/UKFMBxfbZD

aarti
  • 2,815
  • 1
  • 23
  • 31
2

Reflection should be all you need. This seems similar (though not identical) to "deep copy" semantics, which has been implemented at https://godoc.org/github.com/getlantern/deepcopy

You should be able to adapt that to your needs, or at least take some ideas from it.

Diego Favero
  • 1,969
  • 2
  • 22
  • 32
Evan
  • 6,369
  • 1
  • 29
  • 30
  • There's a huge cost, in performance and code complexity, to using reflection. @platwp, might be worth posting a higher-level question about what you're trying to build when this comes up. If JavaScript-ish flexibility is that important, you might even want to use dynamic collections (`map[string]interface{}` or whatever) instead of object types. – twotwotwo Apr 28 '14 at 20:15
  • @twotwotwo I was hoping to avoid the "complexity" part. If there's no "easy" way to do it, then dynamic collections might make sense. Thanks! – platwp Apr 28 '14 at 20:44
1

You should use a map[string]interface{} instead, gonna be much faster (although still not as fast as you used the proper logic with actual structs).

package main

import "fmt"

type Object map[string]interface{}

var m = Object{
    "Firstname": "name",
    "Lastname":  "",
    "years":     uint8(10),
}

func main() {
    var cp = Object{}
    for k, v := range m {
        if s, ok := v.(string); ok && s != "" {
            cp[k] = s
        } else if ui, ok := v.(uint8); ok {
            cp[k] = ui
        }
    }
    fmt.Printf("%#v\n", cp)
}
OneOfOne
  • 95,033
  • 20
  • 184
  • 185
0

I don't even know how many ways this can go wrong...

package main

import (
    "fmt"
    "encoding/json"
)

type Serializable interface {
    fromMap(map[string]interface{}) error
    toMap() (map[string]interface{}, error)
}

type Person struct {
    Firstname string       
    Lastname  string       
    Years     uint8       
}

func (p *Person) fromMap(m map[string]interface{}) error {
    b, err := json.Marshal(m)
    if err != nil {
        return err
    }

    if err := json.Unmarshal(b, p); err != nil {
        return err
    }
    return nil
}

func (p Person) toMap() (map[string]interface{}, error) {
    b, err := json.Marshal(p)
    if err != nil {
        return nil, err
    }
    m := map[string]interface{}{}
    if err := json.Unmarshal(b, &m); err != nil {
        return nil, err
    }
    return m, nil
}

func copy(p1 Serializable, p2 Serializable) error {

    m1, err := p1.toMap()
    if err != nil {
        return err
    }

    m2, err := p2.toMap()
    if err != nil {
        return err
    }

    for k := range m1 {
        m2[k] = m1[k]
    }

    if err := p2.fromMap(m2); err != nil {
        return err
    }
    return nil
}

func main() {
    p1 := Person{
        "Mary",
        "Jane",
        26,
    }

    p2 := Person {
        "Random",
        "Lady",
        26,
    }

    if err := copy(&p1, &p2); err != nil {
        fmt.Printf("ERR: %s\n", err.Error())
        return
    }

    fmt.Printf("%v\n", p2)

}
Kelsnare
  • 635
  • 5
  • 12
0

Since this answer contains a bug and hasn't been fixed yet I'll add it here

structValue := FooBar{Foo: "foo", Bar: 10}
fields := reflect.TypeOf(structValue)
values := reflect.ValueOf(structValue)

num := fields.NumField()

for i := 0; i < num; i++ {
    field := fields.Field(i)
    value := values.Field(i)
    fmt.Print("Type:", field.Type, ",", field.Name, "=", value, "\n")

    switch field.Type.Kind() {
    case reflect.String:
        v := value.String()
        fmt.Print(v, "\n")
    case reflect.Int:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    case reflect.Int32:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    case reflect.Int64:
        v := strconv.FormatInt(value.Int(), 10)
        fmt.Print(v, "\n")
    default:
        assert.Fail(t, "Not support type of struct")
    }
}

Here the Go Playground to see test it out

UPDATE: One thing to note is that if your field name has an underscore in it it will be deleted. I'm not sure what other gotchas exist but do know that the conversion is not 1:1

Carlo Nyte
  • 665
  • 6
  • 17