英文:
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:
答案2
得分: 9
对于好玩,我试了一下。请注意,我稍微作弊了一点(见注释),但你应该能够理解。使用反射与静态类型赋值(如nemo的答案)相比通常会有一些成本,所以在做决策时要权衡一下(尽管我还没有对其进行基准测试)。
另外,明显的免责声明,我没有测试所有边缘情况等等。不要只是将这段代码复制粘贴到生产代码中
所以开始吧:
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
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论