Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to merge two Go values of same struct type?

I want to create a function called merge() that takes in two values of the same struct, but of any struct, and returns the merged values of the two structs.

I want the first value to take precedence. For example, if there are two structs a and b, after calling merge(a,b), if there are fields that both a and b contain, I want it to have a's value for that given field.

What would be the best way to implement this? 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",
//       }
}

This is what I have so far:

// 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
}
like image 576
user2990276 Avatar asked Jan 01 '23 12:01

user2990276


2 Answers

Check this package https://github.com/imdario/mergo

Sample code:

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

See it at playground: https://play.golang.org/p/9KWTK5mSZ6Q

like image 110
Cong Nguyen Avatar answered Jan 05 '23 15:01

Cong Nguyen


Use custom types for fields in your target structs.

type firstString string
type firstInt int

type cat struct {
    Name  firstString
    Color firstString
    Age   firstInt
}

type book struct {
    Title  firstString
    Author firstString
}

Implement UnMarshalJSON for each custom type such that they only unmarshal for target values that are empty.

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
}

If your data is coming via JSON, you can avoid the use of the merge function; just keep unMarshalling incoming JSON to the same struct. If your data is already in separate structs, you can use JSON as an intermediary in your merge function to abstract away all the reflect-ing you have in your example.

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

All together in a working example: https://play.golang.org/p/5YO2HCi8f0N

like image 41
blobdon Avatar answered Jan 05 '23 15:01

blobdon