Using Reflection in Go to assign a Value to a Property of a Struct Contained in a Map

This solution had me literally working on-again off-again for a full week.

One of the main reasons it took so long was I was trying to write a wrapper to simplify because I find the reflect package in Go so unintuitive and hard-to-work-with. But it turns out that trying to write a wrapper around an unintuitive and hard-to-work-with package simply created for me two problems (much like trying to use regular expressions, but I digress…)

So I paired it back to the basics and figured out the problem I was having. I had declared the data structure as map[string]info instead of using a pointer to the struct, e.g.: map[string]*info.

For those playing along at home, here is code that illustrates the use-case and my solution, or you can jump to the Go playground to see it in action. (You can also watch a screencast I prepared for the good folks at JetBrains in hopes they will add features to improve debugging reflection in their GoLand IDE for Go):

package main

import (
    "encoding/json"
    "fmt"
    "github.com/mikeschinkel/go-only"
    "reflect"
    "testing"
)

var defaultChild = child{
    Key: "default",
}
var defaultInfo = info{
    Data:  "default-data",
    Child: &defaultChild,
}

type child struct {
    Key string
}
type info struct {
    Data  string
    Child *child
}

var testInfo = map[string]*info{
    "test1": {
        Data: "foo",
    },
    "test2": {
        Child: &child{Key:"bar"},
    },
}

const indent = "    "

func TestReflectAssign(t *testing.T) {
    /* 1 */ d := testInfo["test1"].Data            ;    p(d);
    /* 2 */ d  = testInfo["test2"].Data            ;    p(d);

    /* 3 */ v := reflect.ValueOf(&testInfo)        ;    p(v);
    /* 4 */ v =  v.Elem()                          ;    p(v);
    /* 5 */ k := v.MapKeys()                       ;    p(k);
    /* 6 */ i := k[0]                              ;    p(i);

    /* 7 */ di := reflect.ValueOf(&defaultInfo)    ;    p(di);
    /* 8 */ de := di.Elem()                        ;    p(de);

    for _,mi := range k {
        /*  9+4(n-1) */ f := v.MapIndex(mi)         ;   p(f)
        /* 10+4(n-1) */ fe := f.Elem()              ;   p(fe)
        /* 11+4(n-1) */ ff :=  fe.Field(0)          ;   p(ff)
        if ff.String() == "" {
            ff.SetString(de.Field(0).String())
     }

        /* 12+4(n-1) */ ff = fe.Field(1)            ;    p(ff);
        if ff.IsNil() {
            ff.Set(de.Field(1))
        }
   }
    /* n */                                              p(testInfo)

}

//
// Everything below here is just used to output Type, Kind and Value
// for each of the intermediate steps captured in the variables.
//

var counter = 0

func p(i interface{}) {
    counter++
    v := reflect.ValueOf(i)
    if v2, k := i.(reflect.Value); k {
        v = v2
    }
    t:= v.Kind().String()
    k:= v.Kind().String()
    var nl string
    if t == k {
        k = ""
        nl = "\n\n"
    } else {
        k = fmt.Sprintf("%sKind:  %s\n",
            indent,
            t)
    }

    fmt.Printf("%d.) Type:  %s\n%s%sValue: %#v%s",
        counter,
        t,
        k,
        indent,
        i,
        nl)
    x := byType(i)
    if x != nil {
        fmt.Printf("%s%s\n\n",
            indent,
            toJson(x))
    }
}

func byType(i interface{}) (x interface{}) {
    for range only.Once {

        if v, k := i.([]reflect.Value); k {
            var is []interface{}
            for _, s := range v {
                is = append(is, s.Interface())
            }
            x = is
            break
        }

        if v, k := i.(reflect.Value); k {
            i = v.Interface()
            if v.Kind() == reflect.String {
                break
            }
            x = i
            break
        }
        x = byType(reflect.ValueOf(i))

    }
    return x
}

func toJson(i interface{}) string {
    j,err := json.MarshalIndent(i, indent,"   ")
    if err != nil {
        println(err.Error())
    }
    return string(j)
}

Hope this helps someone avoid the week of unproductive head banging I went through to get to this.