将嵌套的interface{}解析为用户提供的结构体指针

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

Unmarshall nested interface{} into user supplied struct pointer

问题

我有一个Set函数,它将用户对象(或变量)封装在我自己的sessions结构体中。它将其分配给sessions结构体的Value字段。然后,Set函数将此结构体进行编组,并将字符串分配到存储的某个位置。

我的问题是,我不确定如何实现我的Get函数,以仅返回存储在Value字段中的未编组结构体,而不是整个sessions封装结构体。

我制作了一个非常简单的示例,演示了我所说的内容。

我无法在Get函数的赋值中使用类型断言,因为我事先不知道用户将使用什么类型。

我怀疑可能有一种使用反射来实现这一点的方法。

**编辑:**到目前为止,提供的两个答案都不是我要找的。我不知道用户将使用什么类型,它可以是任何类型,因此通过硬编码他们的类型或尝试“猜测”其可能包含的内容来编写代码是行不通的。

英文:

I've got a Set function that wraps a users object (or variable) in my own struct called sessions. It assigns it to the Value field of my sessions struct. The Set function then marshalls this struct and assigns the string somewhere in storage.

My problem is that I'm not sure how to implement my Get function to only return the unmarshalled struct stored in the Value field, opposed to the entire sessions wrapper struct.

I've made a very simple example demonstrating what I'm talking about.

I can't use a type assertion in the assignment in my Get func because I don't know what type the user is going to use in advance.

I suspect there may be a way using reflection to accomplish this?

Edit: The two provided answers so far are not what I'm looking for. I do not know what type the user will be using, it could be anything, so coding around that by hard coding their type or trying to "guess" what it may contain is not going to work.

答案1

得分: 1

好的,以下是翻译好的内容:

好的,我明白你想要做什么。我找到了这个答案https://stackoverflow.com/questions/26744873/converting-map-to-struct,并对其进行了一些修改,以使其适用于你的特定用例。注意:这个代码没有经过全面测试,可能有些不稳定,请自行承担风险:

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"reflect"
)

type session struct {
	Value interface{}
	Flash map[string]string
}

type Person struct {
	Name string
	Age  int
}

func Get(pointer interface{}) {
	marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`

	var sess session

	d := json.NewDecoder(bytes.NewBuffer([]byte(marshalledString)))
	d.UseNumber()
	if err := d.Decode(&sess); err != nil {
		panic(err)
	}

	fmt.Printf("%#v", sess)

	switch sess.Value.(type) {
	case map[string]interface{}:
		err := FillStruct(sess.Value.(map[string]interface{}), pointer)
		if err != nil {
			log.Fatal(err)
		}
	default:
		return // You may want to return an error here...
	}
}

func main() {
	var personObj Person

	Get(&personObj)

	// 希望在这里看到personObj的Name为"bob",Age为3
	fmt.Printf("%#v", personObj)
}

func SetField(obj interface{}, name string, value interface{}) error {
	structValue := reflect.ValueOf(obj).Elem()
	structFieldValue := structValue.FieldByName(name)

	if !structFieldValue.IsValid() {
		return fmt.Errorf("No such field: %s in obj", name)
	}

	if !structFieldValue.CanSet() {
		return fmt.Errorf("Cannot set %s field value", name)
	}

	structFieldType := structFieldValue.Type()
	val := reflect.ValueOf(value)

	if _, ok := value.(json.Number); ok {
		if f, err := value.(json.Number).Int64(); err == nil {
			structFieldValue.SetInt(f)
			return nil
		}
		if f, err := value.(json.Number).Float64(); err == nil {
			structFieldValue.SetFloat(f)
			return nil
		}
	}

	if structFieldType != val.Type() {
		return errors.New(fmt.Sprintf("Provided value type [%s] didn't match obj field type [%s]", val.Type().String(), structFieldType.String()))
	}

	structFieldValue.Set(val)
	return nil
}

func FillStruct(m map[string]interface{}, s interface{}) error {
	for k, v := range m {
		err := SetField(s, k, v)
		if err != nil {
			return err
		}
	}
	return nil
}

希望能帮到你!

英文:

OK, I think I know what you're wanting to do. I found this answer https://stackoverflow.com/questions/26744873/converting-map-to-struct and made some tweaks to get it working for your particular use case. Note: this hasn't been tested thoroughly and may be a little shaky, use at your own risk:

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"reflect"
)
type session struct {
Value interface{}
Flash map[string]string
}
type Person struct {
Name string
Age  int
}
func Get(pointer interface{}) {
marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`
var sess session
d := json.NewDecoder(bytes.NewBuffer([]byte(marshalledString)))
d.UseNumber()
if err := d.Decode(&sess); err != nil {
panic(err)
}
fmt.Printf("%#v", sess)
switch sess.Value.(type) {
case map[string]interface{}:
err := FillStruct(sess.Value.(map[string]interface{}), pointer)
if err != nil {
log.Fatal(err)
}
default:
return // You may want to return an error here...
}
}
func main() {
var personObj Person
Get(&personObj)
// Wanting to see personObj here have Name "bob" and Age 3
fmt.Printf("%#v", personObj)
}
func SetField(obj interface{}, name string, value interface{}) error {
structValue := reflect.ValueOf(obj).Elem()
structFieldValue := structValue.FieldByName(name)
if !structFieldValue.IsValid() {
return fmt.Errorf("No such field: %s in obj", name)
}
if !structFieldValue.CanSet() {
return fmt.Errorf("Cannot set %s field value", name)
}
structFieldType := structFieldValue.Type()
val := reflect.ValueOf(value)
if _, ok := value.(json.Number); ok {
if f, err := value.(json.Number).Int64(); err == nil {
structFieldValue.SetInt(f)
return nil
}
if f, err := value.(json.Number).Float64(); err == nil {
structFieldValue.SetFloat(f)
return nil
}
}
if structFieldType != val.Type() {
return errors.New(fmt.Sprintf("Provided value type [%s] didn't match obj field type [%s]", val.Type().String(), structFieldType.String()))
}
structFieldValue.Set(val)
return nil
}
func FillStruct(m map[string]interface{}, s interface{}) error {
for k, v := range m {
err := SetField(s, k, v)
if err != nil {
return err
}
}
return nil
}

答案2

得分: 0

用户可能能够传入任何值,但是你的代码可以通过将错误传递回给他们来处理无效的输入。如果你知道传入数据的期望格式,你可以直接解组它并单独处理任何无效的输入。这样就不需要使用难以处理的中间接口{}了:

package main

import (
	"encoding/json"
	"fmt"
)

type session struct {
	Value Person
	Flash map[string]string
}

type Person struct {
	Name string
	Age  int
}

func Get(marshaled string) (Person, error) {
	var sess session
	err := json.Unmarshal([]byte(marshaled), &sess)
	if err != nil {
		return Person{}, err
	}
	fmt.Println(sess) // {{bob 3} map[]}
	return sess.Value, nil
}

func main() {
	person, err := Get(`{"Value":{"Name":"bob","Age":3},"Flash":null}`)
	if err != nil {
		fmt.Println("Got err:", err)
	}
	fmt.Printf("%#v", person) // main.Person{Name:"bob", Age:3}
}

如果Value可以是多种类型,那么你将不得不在某个地方进行类型断言。在Go中,这并不是太痛苦:

switch v := anything.(type) {
case string:
    fmt.Println(v)
case int32, int64:
    fmt.Println(v)
case SomeCustomType:
    fmt.Println(v)
default:
    fmt.Println("unknown")
}
英文:

The user may be able to pass in any value, but your code can deal with invalid input by passing an error back to them. If you know the desired format of the incoming data you can directly unmarshal it and handle any invalid input separately. This removes the need to have the intermediate interface{} that's hard to deal with:

https://play.golang.org/p/VNCflbk3GL

package main
import (
"encoding/json"
"fmt"
)
type session struct {
Value Person
Flash map[string]string
}
type Person struct {
Name string
Age  int
}
func Get(marshaled string) (Person, error) {
var sess session
err := json.Unmarshal([]byte(marshaled), &sess)
if err != nil {
return Person{}, err
}
fmt.Println(sess) // {{bob 3} map[]}
return sess.Value, nil
}
func main() {
person, err := Get(`{"Value":{"Name":"bob","Age":3},"Flash":null}`)
if err != nil {
fmt.Println("Got err:", err)
}
fmt.Printf("%#v", person) // main.Person{Name:"bob", Age:3}
}

If it's valid for Value to be multiple types, then you will have to do a type assertion somewhere. In Go it's not all that painful though:

https://newfivefour.com/golang-interface-type-assertions-switch.html

switch v := anything.(type) {
case string:
fmt.Println(v)
case int32, int64:
fmt.Println(v)
case SomeCustomType:
fmt.Println(v)
default:
fmt.Println("unknown")
}

答案3

得分: 0

你的问题是,你的Value字段的传入数据类型是map[string]interface{},而在Go语言中没有直接将map转换为你的类型的方法(尽管肯定有相关的代码)。

好的。如果我们假设我们对Value字段的传入数据完全没有控制权,但是我们仍然可以通过其属性的组合来识别数据类型,对吗?因为根据定义,你应该知道可能的选项。我们可以创建一个通用的传入对象,而不是使用interface{}。AWS在他们的Go SDK中使用了类似的方法,至少对于DynamoDB服务,通过指针设置可选属性:https://github.com/aws/aws-sdk-go/blob/master/service/dynamodb/examples_test.go#L32

所以,方法是:你的UnknownObj结构体将具有可选的属性,这些属性可以在json.Unmarshal时填充(也可能不填充)。通过知道哪些字段通过switch语句传递,你可以猜测发送的数据。

package main

import (
	"encoding/json"
	"fmt"
)

type session struct {
	Value UnknownObj
	Flash map[string]string
}

type UnknownObj struct {
	Name           *string
	Age            *float64
	SomeOtherField *map[string]string
}

func Get() UnknownObj {
	marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`

	var sess session
	json.Unmarshal([]byte(marshalledString), &sess)
	return sess.Value
}

func main() {

	v := Get()

	switch {
	case v.Name != nil && v.Age != nil:
		fmt.Println("This is a Person")
	default:
		fmt.Println("Unknown data type")

	}

}

然而,如果你对根/Values字段有控制权,并且可以要求发送给你每种类型的特定字段,而不是将所有字段都放在Values下面,那么你可以这样做:

type session struct {
	Person   *Person
	Car      *Car
	Building *Building
	Etc      *Etc

	...
}

这样,你的解决方案将更加简单,你只需要检查哪个属性不为nil。

希望对你有所帮助。

祝好运。

更新于2016年12月15日
回答你关于框架的评论,你描述的是将用户的请求绑定到任意数据类型的过程。
好的。不幸的是,这里的代码太多了,但是这是一个起点的链接:
https://github.com/go-playground/validator/blob/v8.18.1/validator.go#L498

这是一个Gin框架在这里使用的绑定方法:https://github.com/gin-gonic/gin/blob/master/binding/json.go

祝好运!

英文:

You problem is that your incoming data type of Value is map[string]interface{}, and there's no direct/native way in Go to convert map into your type (while there's definitely code out there).

OK. If we assume that we totally have no control over incoming data in the Value field, but still, we can identify data type by a combination of its attributes, right? Because by definition, you should know possible options. We can create a universal incoming object instead of interface{}. AWS is using similar approach in their Go SDK, at least for DynamoDB service, setting optional attributes via pointers: https://github.com/aws/aws-sdk-go/blob/master/service/dynamodb/examples_test.go#L32

So, the approach is: your UnknownObj struct will have optional attributes that may be filled (and may be not) on json.Unmarshal. Knowing what fields were delivered via the switch, you can guess the data sent.

package main
import (
"encoding/json"
"fmt"
)
type session struct {
Value UnknownObj
Flash map[string]string
}
type UnknownObj struct {
Name           *string
Age            *float64
SomeOtherField *map[string]string
}
func Get() UnknownObj {
marshalledString := `{"Value":{"Name":"bob","Age":3},"Flash":null}`
var sess session
json.Unmarshal([]byte(marshalledString), &sess)
return sess.Value
}
func main() {
v := Get()
switch {
case v.Name != nil && v.Age != nil:
fmt.Println("This is a Person")
default:
fmt.Println("Unknown data type")
}
}

However, if you have control over the root/Values field and you can request to send you specific fields for each of the types instead of pushing all under Values, then you could have:

type session struct {
Person   *Person
Car      *Car
Building *Buidling
Etc      *Etc
...
}

This way, your solution will be even easier -> you'll just need to check what property is not nil.

Hope this helps.

Cheers.

Update Dec 15, 2016
To reply on your comment regarding the framework: what you are describing is a process of binding of user's request to an arbitrary data-type.
OK. Unfortunately, its too much code to post here, but here's a link as a starting point:
https://github.com/go-playground/validator/blob/v8.18.1/validator.go#L498

This is a package and approach Gin framework is using for binding here: https://github.com/gin-gonic/gin/blob/master/binding/json.go

Good luck!

huangapple
  • 本文由 发表于 2016年12月15日 13:49:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/41157259.html
匿名

发表评论

匿名网友

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

确定