3

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
}
user2990276
  • 117
  • 1
  • 6
  • 9
    The best way to implement this would be by not implementing it, this is not effective go. What's your use case for this? There is probably a better solution. – Chad Dec 21 '18 at 01:42
  • 1
    In addition to what @luminoslty said I'd like to request clarification because you say you want a's attributes to take precedence but in your book example you deviated from that. – perennial_noob Dec 21 '18 at 02:26
  • 1
    Possible duplicate of https://stackoverflow.com/questions/43912734/update-field-if-value-is-nil-0-false-in-struct-golang – Cerise Limón Dec 21 '18 at 03:32
  • 1
    I believe you should take a look at using [reflection](https://blog.golang.org/laws-of-reflection) with the [reflect](https://golang.org/pkg/reflect/) core library. – Koala Yeung Dec 21 '18 at 05:24
  • 1
    "I want the first value to take precedence." -- your code examples state the exact opposite. – Joel Cornett Dec 21 '18 at 05:54
  • 1
    Oh, that gets complicated really fast. 1. You must use reflection. 2. You "can reflect" only on exported fields so _none_ of your structs have _any_ _chance_ to work with that stuff. 3. Asking for best way is OT for SO. 4. Use a existing library e.g. github.com/imdario/mergo – Volker Dec 21 '18 at 08:46
  • Ok I fixed it so the example is consistent, everyone can calm down now. – user2990276 Dec 21 '18 at 16:02
  • @luminoslty I am receiving many values (all of the same struct type) from the payload of many HTTP requests, some of the values contain fields the others don't, and some both contain the same fields. I want to have the most updated fields from all of the values into one resulting value that I will use to hit another endpoint. e.g, I need to update user data. Struct A could contain a FirstName and ID, and Struct B could contain a FirstName, LastName, and Address. I want to make one Struct C that contains A's ID and Firstname and B's LastName and Address. – user2990276 Dec 21 '18 at 16:16
  • At some point, you may want to start using maps instead of structs if fields are essentially dynamic by nature. – SirDarius Dec 21 '18 at 16:33
  • @SirDarius there is a set number of fields – user2990276 Dec 21 '18 at 16:56
  • Are your payloads coming in as JSON? If so, it may be simpler to manipulate them as JSON directly, before de-serializing them. Otherwise, I would also recommend github.com/imdario/mergo if you really need to do merging of arbitrary struct types. – odin243 Dec 21 '18 at 21:57
  • This might be related to [XY Problem](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – Alirus Dec 22 '18 at 10:54

2 Answers2

6

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

Carlos
  • 1,125
  • 1
  • 25
  • 33
Cong Nguyen
  • 1,880
  • 1
  • 16
  • 15
1

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

blobdon
  • 780
  • 4
  • 10