Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

in golang, general function to load http form data into a struct

Tags:

go

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?

like image 602
danny Avatar asked Oct 15 '12 23:10

danny


People also ask

What does * struct mean in Golang?

A struct (short for "structure") is a collection of data fields with declared data types. Golang has the ability to declare and create own data types by combining one or more types, including both built-in and user-defined types.

How do you write a struct in Golang?

It is followed by the name of the type (Address) and the keyword struct to illustrate that we're defining a struct. The struct contains a list of various fields inside the curly braces. Each field has a name and a type. The above code creates a variable of a type Address which is by default set to zero.

Can a struct have functions Golang?

Any real-world entity which has some set of properties or fields can be represented as a struct. As we know that in Go language function is also a user-defined type so, you are allowed to create a function field in the Go structure.


3 Answers

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

like image 184
moraes Avatar answered Nov 15 '22 07:11

moraes


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)
}
like image 36
mna Avatar answered Nov 15 '22 07:11

mna


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.

like image 42
nemo Avatar answered Nov 15 '22 08:11

nemo