6

在 Go 中,http 表单数据(例如来自 POST 或 PUT 请求)可以作为表单的映射来访问map[string][]string。我很难以可概括的方式将其转换为结构。

例如,我想加载如下地图:

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

变成这样的模型:

type Person struct {
    Age   int
    Name string
}

所以我正在尝试编写一个带有签名的函数LoadModel(obj interface{}, m map[string][]string) []error,它将表单数据加载到一个接口{}中,我可以将其类型转换回一个人。使用反射,以便我可以在具有任何字段的任何结构类型上使用它,而不仅仅是一个人,并且我可以根据需要将字符串从 http 数据转换为 int、boolean 等。

使用golang中这个问题的答案,使用reflect,如何设置struct字段的值?我可以使用反射设置一个人的价值,例如:

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

但是我必须为我拥有的每种结构复制加载函数。当我为 interface{} 尝试它时,它不起作用。

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

在一般情况下我该怎么做?或者更好的是,有没有更惯用的 Go 方式来完成我想要做的事情?

4

3 回答 3

10

您需要针对一般情况进行切换,并相应地加载不同的字段类型。这是基本部分。

当您在结构中有切片时(然后您必须将它们加载到表单字段中的元素数量),或者您有嵌套的结构,它会变得更加困难。

我写了一个包来做到这一点。请参见:

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

于 2012-10-17T09:05:28.503 回答
9

为了好玩,我试了一下。请注意,我作弊了一点(见评论),但你应该得到图片。使用反射和静态类型的分配通常是有成本的(比如 nemo 的回答),所以一定要在你的决定中权衡一下(虽然我没有对它进行基准测试)。

另外,很明显的免责声明,我没有测试所有的边缘情况等等。不要只是复制粘贴到生产代码中:)

所以这里是:

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)
}
于 2012-10-16T02:16:23.613 回答
7

我建议使用特定的接口,而不是interface{}LoadModel 的类型必须实现才能加载的接口。

例如:

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)
    }
}

并且您的Person实现方式如下:LoadableLoadModel

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. 
    }
}

例如,这就是encoding/binary包装或encoding/json包装工作的方式。

于 2012-10-16T00:18:06.357 回答