6

In Go, http form data (e.g. from a POST or PUT request) can be accessed as a map of the form map[string][]string. I'm having a hard time converting this to structs in a generalizable way.

For example, I want to load a map like:

m := map[string][]string {
    "Age": []string{"20"},
    "Name": []string{"John Smith"},
}

Into a model like:

type Person struct {
    Age   int
    Name string
}

So I'm trying to write a function with the signature LoadModel(obj interface{}, m map[string][]string) []error that will load the form data into an interface{} that I can type cast back to a Person. Using reflection so that I can use it on any struct type with any fields, not just a Person, and so that I can convert the string from the http data to an int, boolean, etc as necessary.

Using the answer to this question in golang, using reflect, how do you set the value of a struct field? I can set the value of a person using reflect, e.g.:

p := Person{25, "John"}
reflect.ValueOf(&p).Elem().Field(1).SetString("Dave")

But then I'd have to copy the load function for every type of struct I have. When I try it for an interface{} it doesn't work.

pi := (interface{})(p)
reflect.ValueOf(&pi).Elem().Field(1).SetString("Dave")
// panic: reflect: call of reflect.Value.Field on interface Value

How can I do this in the general case? Or even better, is there a more idiomatic Go way to accomplish what I'm trying to do?

Community
  • 1
  • 1
danny
  • 10,103
  • 10
  • 50
  • 57

3 Answers3

10

You need to make switches for the general case, and load the different field types accordingly. This is basic part.

It gets harder when you have slices in the struct (then you have to load them up to the number of elements in the form field), or you have nested structs.

I have written a package that does this. Please see:

http://www.gorillatoolkit.org/pkg/schema

moraes
  • 13,213
  • 7
  • 45
  • 59
  • 1
    Thanks for the link, someone else actually just pointed me to that package today. Looks like I won't have to write my own solution after all :) I love the Gorilla toolkit, btw, already using mux heavily in this project – danny Oct 17 '12 at 14:52
9

For fun, I tried it out. Note that I cheated a little bit (see comments), but you should get the picture. There is usually a cost to use reflection vs statically typed assignments (like nemo's answer), so be sure to weigh that in your decision (I haven't benchmarked it though).

Also, obvious disclaimer, I haven't tested all edge cases, etc, etc. Don't just copy paste this in production code :)

So here goes:

package main

import (
    "fmt"
    "reflect"
    "strconv"
)

type Person struct {
    Age    int
    Name   string
    Salary float64
}

// I cheated a little bit, made the map's value a string instead of a slice.
// Could've used just the index 0 instead, or fill an array of structs (obj).
// Either way, this shows the reflection steps.
//
// Note: no error returned from this example, I just log to stdout. Could definitely
// return an array of errors, and should catch a panic since this is possible
// with the reflect package.
func LoadModel(obj interface{}, m map[string]string) {
    defer func() {
        if e := recover(); e != nil {
            fmt.Printf("Panic! %v\n", e)
        }
    }()

    val := reflect.ValueOf(obj)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }

    // Loop over map, try to match the key to a field
    for k, v := range m {
        if f := val.FieldByName(k); f.IsValid() {
            // Is it assignable?
            if f.CanSet() {

                // Assign the map's value to this field, converting to the right data type.
                switch f.Type().Kind() {
                // Only a few kinds, just to show the basic idea...
                case reflect.Int:
                    if i, e := strconv.ParseInt(v, 0, 0); e == nil {
                        f.SetInt(i)
                    } else {
                        fmt.Printf("Could not set int value of %s: %s\n", k, e)
                    }
                case reflect.Float64:
                    if fl, e := strconv.ParseFloat(v, 0); e == nil {
                        f.SetFloat(fl)
                    } else {
                        fmt.Printf("Could not set float64 value of %s: %s\n", k, e)
                    }
                case reflect.String:
                    f.SetString(v)

                default:
                    fmt.Printf("Unsupported format %v for field %s\n", f.Type().Kind(), k)
                }
            } else {
                fmt.Printf("Key '%s' cannot be set\n", k)
            }
        } else {
            // Key does not map to a field in obj
            fmt.Printf("Key '%s' does not have a corresponding field in obj %+v\n", k, obj)
        }
    }
}

func main() {
    m := map[string]string{
        "Age":     "36",
        "Name":    "Johnny",
        "Salary":  "1400.33",
        "Ignored": "True",
    }
    p := new(Person)
    LoadModel(p, m)
    fmt.Printf("After LoadModel: Person=%+v\n", p)
}
mna
  • 22,989
  • 6
  • 46
  • 49
  • +1 for the reflection example. The problem with this solution however, is that it won't work with private struct fields as they are not settable using reflection. – nemo Oct 16 '12 at 02:25
  • 1
    Of course, but that didn't seem to be a problem for the OP (his example are with public fields). – mna Oct 16 '12 at 02:26
  • Great example, thanks. Gonna have to study this for a bit :) Only working with public struct fields is a reasonable restriction - the use case I'm going for is loading a database model from an http request and any db "ORM" is also probably using reflect and will have the same restriction. – danny Oct 16 '12 at 03:11
7

I'd propose to use a specific interface instead of interface{} in your LoadModel which your type has to implement in order to be loaded.

For example:

type Loadable interface{
    LoadValue(name string, value []string)
}

func LoadModel(loadable Loadable, data map[string][]string) {
    for key, value := range data {
        loadable.LoadValue(key, value)
    }
}

And your Person implements Loadable by implementing LoadModel like this:

type Person struct {
    Age   int
    Name string
}

func (p *Person) LoadValue(name string, value []string) {
    switch name {
    case "Age":
        p.Age, err = strconv.Atoi(value[0])
    // etc. 
    }
}

This is the way, the encoding/binary package or the encoding/json package work, for example.

nemo
  • 55,207
  • 13
  • 135
  • 135
  • Awesome, thanks - I was just playing around with a similar solution. Now I could define the `LoadValue` function using reflection so that there's only one case statement per _type of field_ instead of one case statement _per field_ in `Person`. But is there a way to define it more generically so that if I add another object type I won't have to duplicate that code? Or is that fundamentally at odds with the lack of inheritance and lack of generics in Go? – danny Oct 16 '12 at 01:28
  • On second thought, this way is probably better - to assign the attributes explicitly and avoid e.g. the mass-assignment vulnerability that was found in Rails. Still curious if it's possible to do though – danny Oct 16 '12 at 02:11
  • @nemo there seems to be a performance penalty if you range over the input data, isn't it? – themihai Dec 08 '14 at 02:32
  • I know that this post has more than 2 years but be careful to strconv.Atoi that returns 2 values that you should consider here (int, error). – Devatoria May 05 '15 at 12:33