如何合并两个相同结构类型的Go值?

12
我想创建一个名为merge()的函数,它接受两个相同结构体类型的值,但可以是任何结构体,并返回合并后的结构体值。
我希望第一个值具有优先权。例如,如果有两个结构体ab,在调用merge(a,b)之后,如果ab都包含某些字段,则我希望该字段具有a的给定字段的值。
实现这个最好的方法是什么? https://play.golang.org/p/7s9PWx26gfz
type cat struct {
name  string
color string
age   int
}

type book struct {
title  string
author string
}

func main() {
c1 := cat{
    name:  "Oscar",
    color: "",
    age:   3,
}

c2 := cat{
    name:  "",
    color: "orange",
    age:   2,
}

c3 := merge(c1, c2)

// want: c3 = cat{
//               name: "Oscar",
//               color: "orange",
//               age: 3,
//       }



// another case...
b1 := book{
    title: "Lord of the Rings",
    author: "John Smith",
}

b2 := book{
    title: "Harry Potter",
    author: "",
}

b3 := merge(b1, b2)

// want: b3 = book{
//               title: "Lord of the Rings",
//               author: "John Smith",
//       }
}

目前为止,这就是我所拥有的:

// merges two structs, where a's values take precendence over b's values (a's values will be kept over b's if each field has a value)
func merge(a, b interface{}) (*interface{}, error) {
    var result interface{}
    aFields := reflect.Fields(a)
    bFields := reflect.Fields(b)

    if !reflect.DeepEqual(aFields, bFields) {
        return &result, errors.New("cannot merge structs of different struct types")
    }

    aValOf := reflect.ValueOf(a)
    bValOf := reflect.ValueOf(b)
    resultValOf := reflect.ValueOf(result)
    aValues := make([]interface{}, aValOf.NumField())
    resultValues := make([]interface{}, resultValOf.NumField())

    for i := 0; i < aValOf.NumField(); i++ {
        if reflect.ValueOf(aValues[i]).IsNil() {
            resultValues[i] = bValOf.Field(i).Interface()
            break
        }
        resultValues[i] = aValOf.Field(i).Interface()
    }
    return &result, nil
}

9
最好的实现方法是不去实现它,这不符合 Go 语言的特点。你使用它的用例是什么?可能有更好的解决方案。 - thecoolestguyever123
1
除了@luminosity所说的,我想要请求澄清,因为你说你希望a的属性具有优先权,但是在你的书的例子中,你偏离了这一点。 - perennial_noob
1
可能是 https://dev59.com/DKHia4cB1Zd3GeqPWqfV 的重复问题。 - Charlie Tumahai
1
我认为你应该使用 reflectionreflect 核心库。 - Koala Yeung
1
哦,这很快就变得复杂了。1.你必须使用反射。2.你只能“反射”导出的字段,所以你的任何结构体都没有任何机会与那些东西一起工作。3.询问最佳方法不适用于SO。4.使用现有库,例如github.com/imdario/mergo。 - Volker
显示剩余7条评论
2个回答

15

请检查此程序包 https://github.com/imdario/mergo

样例代码:

package main

import (
    "fmt"
    "github.com/imdario/mergo"
)

type Foo struct {
    A string
    B int64
}

func main() {
    src := Foo{
        A: "one",
        B: 2,
    }
    dest := Foo{
        A: "two",
    }
    mergo.Merge(&dest, src)
    fmt.Println(dest)
    // Will print
    // {two 2}
}

在playground上查看:https://play.golang.org/p/9KWTK5mSZ6Q


包名已更改,目前正常工作。https://go.dev/play/p/0LEg7HsK8PH - undefined

1

在目标结构体中使用自定义类型来定义字段。

type firstString string
type firstInt int

type cat struct {
    Name  firstString
    Color firstString
    Age   firstInt
}

type book struct {
    Title  firstString
    Author firstString
}

为每个自定义类型实现UnMarshalJSON,使它们仅对空目标值进行反序列化。
func (fs *firstString) UnmarshalJSON(bytes []byte) error {
   if len(*fs) > 0 {
        return nil
    }
    var s string
    err := json.Unmarshal(bytes, &s)
    if err != nil {
        return err
    }
    *fs = firstString(s)
    return nil
}

func (fi *firstInt) UnmarshalJSON(bytes []byte) error {
    if *fi != 0 {
        return nil
    }
    var i int
    err := json.Unmarshal(bytes, &i)
    if err != nil {
        return err
    }
    *fi = firstInt(i)
    return nil
}

如果您的数据是通过JSON传输的,您可以避免使用合并函数;只需将传入的JSON解组到相同的结构体中即可。如果您的数据已经在不同的结构体中,则可以在合并函数中使用JSON作为中介来抽象化您在示例中所反映的所有内容。
// merges two structs, where a's values take precendence over b's values (a's values will be kept over b's if each field has a value)
func merge(a, b interface{}) interface{} {

    jb, err := json.Marshal(b)
    if err != nil {
        fmt.Println("Marshal error b:", err)
    }
    err = json.Unmarshal(jb, &a)
    if err != nil {
        fmt.Println("Unmarshal error b-a:", err)
    }

    return a
}

在一个工作示例中,所有内容如下:https://play.golang.org/p/5YO2HCi8f0N

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接