Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Merge" fields two structs of same type

Tags:

struct

go

Looking at this struct:

type Config struct {
  path string
  id   string
  key  string
  addr string
  size uint64
}

Now I have a DefaultConfig intialized with some values and one loaded from a file, let's say FileConfig. I want both structs to me merged, so that I get a Config with the content of both structs. FileConfig should override anything set in DefaultConfig, while FileConfig may not have all fields set. (Why this? Because a potential user may not know the default value, so removing that entry would be equivalent to setting the default - I think)

I thought I'd need reflection for this:

 func merge(default *Config, file *Config) (*Config) {
  b := reflect.ValueOf(default).Elem()
  o := reflect.ValueOf(file).Elem()

  for i := 0; i < b.NumField(); i++ {
    defaultField := b.Field(i)
    fileField := o.Field(i)
    if defaultField.Interface() != reflect.Zero(fileField.Type()).Interface() {
     defaultField.Set(reflect.ValueOf(fileField.Interface()))
    }
  }

  return default
 }

Here I am not sure:

  • If reflection is needed at all
  • There may be easier ways to do this

Another issue I see here is that checking for zero values may be tricky: what if the overriding struct intends to override with a zero value? Luckily, I don't think it applies to my case - but this becomes a function, it may become a problem later

like image 609
transient_loop Avatar asked Nov 20 '17 15:11

transient_loop


2 Answers

Foreword: The encoding/json package uses reflection (package reflect) to read/write values, including structs. Other libraries also using reflection (such as implementations of TOML and YAML) may operate in a similar (or even in the same way), and thus the principle presented here may apply to those libraries as well. You need to test it with the library you use.

For simplicity, the solution presented here uses the standard lib's encoding/json.


An elegant and "zero-effort" solution is to use the encoding/json package and unmarshal into a value of the "prepared", default configuration.

This handles everything you need:

  • missing values in config file: default applies
  • a value given in file overrides default config (whatever it was)
  • explicit overrides to zero values in the file takes precedence (overwrites non-zero default config)

To demonstrate, we'll use this config struct:

type Config struct {
    S1 string
    S2 string
    S3 string
    S4 string
    S5 string
}

And the default configuration:

var defConfig = &Config{
    S1: "", // Zero value
    S2: "", // Zero value
    S3: "abc",
    S4: "def",
    S5: "ghi",
}

And let's say the file contains the following configuration:

const fileContent = `{"S2":"file-s2","S3":"","S5":"file-s5"}`

The file config overrides S2, S3 and the S5 fields.

Code to load the configuration:

conf := new(Config) // New config
*conf = *defConfig  // Initialize with defaults

err := json.NewDecoder(strings.NewReader(fileContent)).Decode(&conf)
if err != nil {
    panic(err)
}

fmt.Printf("%+v", conf)

And the output (try it on the Go Playground):

&{S1: S2:file-s2 S3: S4:def S5:file-s5}

Analyzing the results:

  • S1 was zero in default, was missing from file, result is zero
  • S2 was zero in default, was given in file, result is the file value
  • S3 was given in config, was overriden to be zero in file, result is zero
  • S4 was given in config, was missing in file, result is the default value
  • S5 was given in config, was given in file, result is the file value
like image 94
icza Avatar answered Nov 09 '22 08:11

icza


Reflection is going to make your code slow.

For this struct I would implement a straight Merge() method as:

type Config struct {
  path string
  id   string
  key  string
  addr string
  size uint64
}

func (c *Config) Merge(c2 Config) {
  if c.path == "" {
    c.path = c2.path
  }
  if c.id == "" {
    c.id = c2.id
  }
  if c.path == "" {
    c.path = c2.path
  }
  if c.addr == "" {
    c.addr = c2.addr
  }
  if c.size == 0 {
    c.size = c2.size
  }
}

It's almost same amount of code, fast and easy to understand.

You can cover this method with uni tests that uses reflection to make sure new fields did not get left behind.

That's the point of Go - you write more to get fast & easy to read code.

Also you may want to look into go generate that will generate the method for you from struct definition. Maybe there event something already implemented and available on GitHub? Here is an example of code that do something similar: https://github.com/matryer/moq

Also there are some packages on GitHub that I believe are doing what you want in runtime, for example: https://github.com/imdario/mergo

like image 6
Alexander Trakhimenok Avatar answered Nov 09 '22 08:11

Alexander Trakhimenok