1

I have a function that has a parameter with the type interface{}. This parameter represents my template data. So on each page it stores different data types (mostly structs). I want to append some data to this parameter's data, but it's an interface{} type and I can't do it.

This is what I tried:

func LoadTemplate(templateData interface) {
  appendCustomData(&templateData)
  ... //other functionality that is not relevant
}

func appendCustomData(dst interface{}) {
    // ValueOf to enter reflect-land
    dstPtrValue := reflect.ValueOf(dst)
    // need the type to create a value
    dstPtrType := dstPtrValue.Type()
    // *T -> T, crashes if not a ptr
    dstType := dstPtrType.Elem()
    // the *dst in *dst = zero
    dstValue := reflect.Indirect(dstPtrValue)
    // the zero in *dst = zero
    zeroValue := reflect.Zero(dstType)
    // the = in *dst = 0

    v := reflect.ValueOf(dst).Elem().Elem().FieldByName("HeaderCSS")
    if v.IsValid() {
        v = reflect.ValueOf("new header css value")
    }

    reflect.ValueOf(dst).Elem().Elem().FieldByName("HeaderCSS").Set(reflect.ValueOf(v))

    //dstValue.Set(zeroValue)
    fmt.Println("new dstValue: ", dstValue)
}

I can successfully get the "HeaderCSS" value. But I can't replace it with another value. What am I doing wrong?

My templateData looks like this:

I have a generic struct:

type TemplateData struct {
    FooterJS template.HTML
    HeaderJS template.HTML
    HeaderCSS template.HTML
    //and some more
}

and I have another struct, such as:

type pageStruct struct {
    TemplateData //extends the previous struct
    Form template.HTML
    // and some other maps/string
}

I send this second struct as templateData argument.

Right now I get this error:

"reflect.Value.Set using unaddressable value" at the line: reflect.ValueOf(dst).Elem().Elem().FieldByName("HeaderCSS").Set(reflect.ValueOf(v))

The code from above is inspired from this answer: https://stackoverflow.com/a/26824071/1564840

I want to be able to append/edit values from this interface. Any idea how can I do it? Thanks.

Pascut
  • 3,291
  • 6
  • 36
  • 64
  • 1
    It seems as if you know the actual type: Just type assert to that type and do whatever this type supports. If not: Full code you must show. BTW: Get rid of the empty `interface{}` and your code will be simpler. – Volker Jul 04 '17 at 12:03
  • I can't just remove the interface{} because on each page the parameter type will be different. interface{} was the option that allowed me to accept all kind of arguments. – Pascut Jul 04 '17 at 12:04
  • 1
    This has already been answered well but I wanted to suggest that maybe you should add a `SetHeaderCSS` method and pass in a `HeaderCSSSetter` interface that specifies that method. This would eliminate *all of the reflection* with a simple change, giving you better performance, better readability, and compile-time type safety. – Adrian Jul 05 '17 at 13:47
  • Good idea. Thanks! – Pascut Jul 05 '17 at 13:54

1 Answers1

2

Don't pass a pointer to interface. Instead the interface{} value should contain the pointer. And simply just hand over this interface{} value:

func LoadTemplate(templateData interface) {
  appendCustomData(templateData)
  ... //other functionality that is not relevant
}

Even if you can't use a more concrete type than interface{} (because you must allow multiple types), you can still use type assertion, it will be "super" easy:

func appendCustomData(d interface{}) {
    if ps, ok := d.(*pageStruct); ok {
        ps.TemplateData.HeaderCSS += "+new"
    }

}

Try this one on the Go Playground.

If you must or want to use reflection, this is how appendCustomData() can be implemented:

type Data struct {
    Name  string
    Age   int
    Marks []int
}

func appendCustomData(d interface{}) {
    v := reflect.ValueOf(d).Elem()

    if f := v.FieldByName("Name"); f.IsValid() {
        f.SetString(f.Interface().(string) + "2")
    }

    if f := v.FieldByName("Age"); f.IsValid() {
        f.SetInt(f.Int() + 2)
    }

    if f := v.FieldByName("Marks"); f.IsValid() {
        f.Set(reflect.ValueOf(append(f.Interface().([]int), 2)))
    }

    if f := v.FieldByName("Invalid"); f.IsValid() {
        f.Set(reflect.ValueOf(append(f.Interface().([]int), 2)))
    }
}

Testing it:

d := &Data{
    Name:  "Bob",
    Age:   22,
    Marks: []int{5, 4, 3},
}
fmt.Printf("%+v\n", d)
appendCustomData(d)
fmt.Printf("%+v\n", d)

Output (try it on the Go Playground):

&{Name:Bob Age:22 Marks:[5 4 3]}
&{Name:Bob2 Age:24 Marks:[5 4 3 2]}

Update:

To answer your edited question: there is no difference when the value passed is a struct that embeds another struct. But the value wrapped in the interface{} still must be a pointer.

Example appendCustomData() that appends content to pageStruct.TemplateData.HeaderCSS:

func appendCustomData(d interface{}) {
    v := reflect.ValueOf(d).Elem()

    if f := v.FieldByName("TemplateData"); f.IsValid() {
        if f = f.FieldByName("HeaderCSS"); f.IsValid() {
            f.Set(reflect.ValueOf(f.Interface().(template.HTML) + "+new"))
        }
    }
}

Testing it:

ps := &pageStruct{
    TemplateData: TemplateData{
        HeaderCSS: template.HTML("old"),
    },
}
fmt.Printf("%+v\n", ps)
appendCustomData(ps)
fmt.Printf("%+v\n", ps)

Output (try it on the Go Playground):

&{TemplateData:{FooterJS: HeaderJS: HeaderCSS:old} Form:}
&{TemplateData:{FooterJS: HeaderJS: HeaderCSS:old+new} Form:}
icza
  • 389,944
  • 63
  • 907
  • 827
  • Goog example. I edited my question, I added struct details. In my case I have a struct that extends another struct. Can you figure a way to make it work on that example also? – Pascut Jul 04 '17 at 12:31
  • 1
    @Pascut But if you know the type, just use type assertion, it's much more easier. See edited answer again. – icza Jul 04 '17 at 12:40