Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Append to a slice field in a struct using reflection

Tags:

reflection

go

I have a struct that looks like this:

type guitaristT struct {
    Surname  string   `required=true`
    Year     int64    `required=false`
    American bool     // example of missing tag
    Rating   float32  `required=true`
    Styles   []string `required=true,minsize=1`
}

I have an environment variable that looks like the following, and I'm using reflection to fill the struct based on the keys.

jimiEnvvar :="surname=Hendrix|year=1942|american=true|rating=9.99
  |styles=blues|styles=rock|styles=psychedelic"

I'm able to set the string, int64, bool and float32 fields using reflection, but I'm stuck on how to append to the slice field Styles. For example, based on the above jimiEnvvar I would like the field jimi.Styles to have the values ["blues","rock", "psychedelic"].

I have the following (simplified) code:

result := guitaristT{}
// result.Styles = make([]string, 10) // XXX causes 10 empty strings at start
result.Styles = make([]string, 0)     // EDIT: Alessandro's solution
...
v := reflect.ValueOf(&result).Elem()
...
field := v.FieldByName(key) // eg key = "styles"
...
switch field.Kind() {

case reflect.Slice:
     // this is where I get stuck
     //
     // reflect.Append() has signature:
     //   func Append(s Value, x ...Value) Value

     // so I convert my value to a reflect.Value
     stringValue := reflect.ValueOf(value) // eg value = "blues"

     // field = reflect.Append(field, stringValue)  // XXX doesn't append
     field.Set(reflect.Append(field, stringValue))  // EDIT: Alessandro's solution

EDIT:

A second part (which I solved) was filling a map in the struct. For example:

type guitaristT struct {
    ...
    Styles   []string `required=true,minsize=1`
    Cities   map[string]int
}

Where jimiEnvvar looks like:

jimiEnvvar += "|cities=New York^17|cities=Los Angeles^14"

I wrote in this manner:

    case reflect.Map:
        fmt.Println("keyAsString", keyAsString, "is Map, has value:", valueAsString)
        mapKV := strings.Split(valueAsString, "^")
        if len(mapKV) != 2 {
            log.Fatalln("malformed map key/value:", mapKV)
        }
        mapK := mapKV[0]
        mapV := mapKV[1]
        thisMap := fieldAsValue.Interface().(map[string]int)
        thisMap[mapK] = atoi(mapV)
        thisMapAsValue := reflect.ValueOf(thisMap)
        fieldAsValue.Set(thisMapAsValue)

The final result was:

main.guitaristT{
    Surname:  "Hendrix",
    Year:     1942,
    American: true,
    Rating:   9.989999771118164,
    Styles:   {"blues", "rock", "psychedelic"},
    Cities:   {"London":11, "Bay Area":9, "New York":17, "Los Angeles":14},
}

If you're interested the full code is at https://github.com/soniah/reflect/blob/master/structs.go. Code is just some notes/exercises I'm writing.


EDIT2:

Chapter 11 of "Go in Practice" (Butcher and Farina) has detailed explanations of reflection, structs and tags.

like image 735
Sonia Hamilton Avatar asked Oct 21 '17 13:10

Sonia Hamilton


1 Answers

You were not too far off. Just replace with

field.Set(reflect.Append(field, stringValue)) 

and you are done. Also, make sure you that you initialise the slice using

result.Styles = make([]string, 0)

or you will end up having 10 blank string at the top of the Styles array.

Hope this helps and good luck with your project.

like image 183
Alessandro Santini Avatar answered Nov 15 '22 04:11

Alessandro Santini