在golang中,将HTTP表单数据加载到结构体的通用函数

huangapple go评论76阅读模式
英文:

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

问题

在Go语言中,可以将http表单数据(例如来自POST或PUT请求)作为map[string][]string的形式访问。我在以一种通用的方式将其转换为结构体方面遇到了困难。

例如,我想要加载一个类似的map:

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的函数,该函数将表单数据加载到一个可以强制转换回Person的interface{}中。使用反射,以便我可以在任何结构体类型上使用它,而不仅仅是Person,并且可以根据需要将http数据的字符串转换为int、boolean等。

使用这个问题的答案https://stackoverflow.com/questions/6395076/in-golang-using-reflect-how-do-you-set-the-value-of-a-struct-field,我可以使用反射设置一个person的值,例如:

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语言习惯的方法来实现我想要做的事情?

英文:

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 https://stackoverflow.com/questions/6395076/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?

答案1

得分: 10

你需要为一般情况制作开关,并相应地加载不同的字段类型。这是基本部分。

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

我已经编写了一个可以做到这一点的包。请参见:

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

英文:

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

答案2

得分: 9

对于好玩,我试了一下。请注意,我稍微作弊了一点(见注释),但你应该能够理解。使用反射与静态类型赋值(如nemo的答案)相比通常会有一些成本,所以在做决策时要权衡一下(尽管我还没有对其进行基准测试)。

另外,明显的免责声明,我没有测试所有边缘情况等等。不要只是将这段代码复制粘贴到生产代码中 在golang中,将HTTP表单数据加载到结构体的通用函数

所以开始吧:

package main

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

type Person struct {
    Age    int
    Name   string
    Salary float64
}

// 我稍微作弊了一点,将map的值设为了字符串而不是切片。
// 可以只使用索引0,或者填充一个结构体数组(obj)。
// 无论哪种方式,都展示了反射的步骤。
//
// 注意:此示例没有返回错误,只是将日志记录到标准输出。可以返回一个错误数组,并且应该捕获恐慌,因为这在反射包中是可能的。
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()
    }

    // 遍历map,尝试将键与字段匹配
    for k, v := range m {
        if f := val.FieldByName(k); f.IsValid() {
            // 是否可赋值?
            if f.CanSet() {

                // 将map的值分配给该字段,转换为正确的数据类型。
                switch f.Type().Kind() {
                // 只有几种类型,只是为了展示基本思想...
                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 {
            // 键在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)
}
英文:

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 在golang中,将HTTP表单数据加载到结构体的通用函数

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

答案3

得分: 7

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

例如:

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通过实现以下方式来实现Loadable

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])
    // 等等。
    }
}

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

英文:

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.

huangapple
  • 本文由 发表于 2012年10月16日 07:56:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/12905661.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定