合并两个不同的结构体并进行编组 – Golang

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

Merging two different structs and marshalling them - Golang

问题

假设我有以下的第一个结构体:

type Person struct {
	Name string                 `json:"person_name"`
	Age  int                    `json:"person_age"`
	Data map[string]interface{} `json:"data"`
}

我想要将一个包含上述结构体的数组进行编组。到目前为止,一切都正常,我收到的一个示例响应如下:

[
   {
      "person_name":"name",
      "person_age":12,"data":{}
   },
   {
      "person_name":"name2",
      "person_age":12,"data":{}
   }
]

现在,我需要在这里追加另一个结构体,并且最终的响应应该如下:

[
   {
      "person_name":"name",
      "person_age":12,"data":{}
   },
   {
      "person_name":"name2",
      "person_age":12,"data":{}
   }, 
   {
      "newData":"value"
   }
]

那么,有人可以帮忙解决这个问题吗?我该如何实现这个目标?

我尝试创建一个[]interface{},然后迭代person以附加每个数据,但是这种方法的问题是,如果Data是一个空字符串,它会将其变为null。我希望它仍然是一个空映射。

英文:

Let's say I have the first struct as

type Person struct {
	Name string                 `json:"person_name"`
	Age  int                    `json:"person_age"`
	Data map[string]interface{} `json:"data"`
}

and I am trying to marshal an array of the above struct
Things work well till here and a sample response I receive is

[
   {
      "person_name":"name",
      "person_age":12,"data":{}
   },
   {
      "person_name":"name2",
      "person_age":12,"data":{}
   }
]

Now, I need to append another struct over here and the final response should like

[
   {
      "person_name":"name",
      "person_age":12,"data":{}
   },
   {
      "person_name":"name2",
      "person_age":12,"data":{}
   }, 
   {
      "newData":"value"
   }
]

So can someone help on this and how i can achieve this ?

I tried by creating an []interface{} and then iterating over person to append each data, but the issue in this approach is that it makes the Data as null if in case it's an empty string.
I would need it be an empty map only.

答案1

得分: 2

让我先说一下,根据我的理解,你可能正在处理一个X-Y问题。我很难想象出很多合理的用例,其中一个已定义的数据类型必须与完全不同的、潜在的任意/自由形式的数据结构一起进行编组。虽然这是可能的,以下是你可以实现它的方法:

所以你只是想将一个完全不同的结构附加到数据集中,然后将其编组并以JSON格式返回结果?你需要为此创建一个新的切片:

personData := []Person{} // 这里是person 1和person 2
more := map[string]string{ // 或者其他某个结构体
    "newdata": "value",
}
allData := make([]interface{}, 0, len(personData)+1) // +1 是为了more,根据需要编组的对象数量设置容量
for _, p := range personData {
    allData = append(allData, p) // 将其复制到该切片中,因为[]Person与[]interface{}不兼容
}
allData = append(allData, more)
bJSON, err := json.Marshal(allData)
if err != nil {
    // 处理错误
}
fmt.Println(string(bJSON))

基本上,因为你试图编组一个包含多种不同类型的切片,所以你必须将所有对象添加到类型为interface{}的切片中,然后一次性进行编组。


更简洁的方法

有更简洁的方法可以允许你进行数据的_反编组_,前提是已知涉及的不同数据类型。考虑使用包装类型,如下所示:

type Person struct {} // 你已经有的那个

type NewData struct {
   NewData string `json:"newdata"`
}

type MixedData struct {
    *Person
    *NewData
}

在这个MixedData类型中,PersonNewData都被嵌入进来,所以MixedData本质上将充当所有嵌入类型的合并版本(具有相同名称的字段应该在此级别被覆盖)。使用这种类型,你可以相应地进行编组和反编组JSON:

allData := []MixedData{
    {
        Person: &person1,
    },
    {
        Person: &person2,
    },
    {
        NewData: &newData,
    },
}

类似地,当你有一个JSON []byte输入时,你可以像处理任何其他类型一样进行反编组:

data := []MixedData{}
if err := json.Unmarshal(in, &data); err != nil {
    // 处理错误
}
fmt.Printf("%#v\n", data) // 它会在这里

最好为MixedData类型添加一些函数/获取器:

func (m MixedData) IsPerson() bool { return m.Person != nil }

func (m MixedData) GetPerson() *Person {
    if m.Person == nil {
        return nil
    }
    cpy := *m.Person // 创建一个副本以避免共享指针
    return &cpy // 返回指向副本的指针
}

对所有嵌入类型执行相同的操作,这样就可以完美运行了。

如前所述,如果嵌入类型包含具有相同名称的字段,那么你应该在MixedData类型中覆盖它们。假设你有一个PersonAddress类型,两者都有一个ID字段:

type MixedData struct {
    ID  string `json:"id"`
    *Person
    *Address
}

这将在MixedData类型上设置ID值,并在相应的嵌入结构体上设置所有其他(非共享)字段。然后你可以使用获取器在需要的地方设置ID,或者使用自定义的解组器,但我将把实现留给你。

英文:

Let me prefix this by saying this looks to me very much like you might be dealing with an X-Y problem. I can't really think of many valid use-cases where one would end up with a defined data-type that has to somehow be marshalled alongside a completely different, potentially arbitrary/freeform data structure. It's possible, though, and this is how you could do it:

So you just want to append a completely different struct to the data-set, then marshal it and return the result as JSON? You'll need to create a new slice for that:

personData := []Person{} // person 1 and 2 here
more := map[string]string{ // or some other struct
    "newdata": "value",
}
allData := make([]any, 0, len(personData) + 1) // the +1 is for the more, set cap to however many objects you need to marshal
for _, p := range personData {
    allData = append(allData, p) // copy over to this slice, because []Person is not compatible with []any
}
allData = append(allData, more)
bJSON, err := json.Marshal(allData)
if err != nil {
    // handle
}
fmt.Println(string(bJSON))

Essentially, because you're trying to marshal a slice containing multiple different types, you have to add all objects to a slice of type any (short for interface{}) before marshalling it all in one go


Cleaner approaches

There are much, much cleaner approaches that allow you to unmarshal the data, too, assuming the different data-types involved are known beforehand. Consider using a wrapper type like so:

type Person struct {} // the one you have

type NewData {
   NewData string `json:"newdata"`
}

type MixedData struct {
    *Person
    *NewData
}

In this MixedData type, both Person and NewData are embedded, so MixedData will essentially act as a merged version of all embedded types (fields with the same name should be overridden at this level). With this type, you can marshal and unmarshal the JSON accordingly:

allData := []MixedData{
    {
        Person: &person1,
    },
    {
        Person: &person2,
    },
    {
        NewData: &newData,
    },
}

Similarly, when you have a JSON []byte input, you can unmarshal it same as you would any other type:

data := []MixedData{}
if err := json.Unmarshal(&data, in); err != nil {
    // handle
}
fmt.Printf("%#v\n", data) // it'll be there

It pays to add some functions/getters to the MixedData type, though:

func (m MixedData) IsPerson() bool { return m.Person != nil }

func (m MixedData) Person() *Person {
    if m.Person == nil {
        return nil
    }
    cpy := *m.Person // create a copy to avoid shared pointers
    return &cpy // return pointer to the copy
}

Do the same for all embedded types and this works like a charm.

As mentioned before, should your embedded types contain fields with the same name, then you should override them in the MixedData type. Say you have a Person and Address type, and both have an ID field:

type MixedData struct {
    ID  string `json:"id"`
    *Person
    *Address
}

This will set the ID value on the MixedData type, and all other (non-shared) fields on the corresponding embedded struct. You can then use the getters to set the ID where needed, or use a custom unmarshaller, but I'll leave that to you to implement

huangapple
  • 本文由 发表于 2023年2月16日 17:12:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/75470007.html
匿名

发表评论

匿名网友

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

确定