【问题标题】:"Merge" fields two structs of same type“合并”字段两个相同类型的结构
【发布时间】:2017-11-20 15:26:18
【问题描述】:

看着这个struct

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

现在我有一个用一些值初始化的DefaultConfig 和一个从文件中加载的值,比如说FileConfig。 我希望将两个结构合并,以便我得到一个包含两个结构内容的ConfigFileConfig 应该覆盖 DefaultConfig 中设置的任何内容,而 FileConfig 可能没有设置所有字段。 (为什么?因为潜在用户可能不知道默认值,所以删除该条目相当于设置默认值 - 我认为)

我认为我需要对此进行反思:

 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
 }

这里我不确定:

  • 如果需要反思
  • 可能有更简单的方法来做到这一点

我在这里看到的另一个问题是检查零值可能很棘手:如果覆盖结构 打算 用零值覆盖怎么办?幸运的是,我认为它不适用于我的情况 - 但这变成了一个函数,以后可能会成为一个问题

【问题讨论】:

  • 绕过零值的唯一方法是不要在配置结构中包含原始整数或字符串。或者至少对于默认情况下非零但可以设置为零的值不适用。对于这些,您必须使用 *int 等。

标签: go struct


【解决方案1】:

前言:encoding/json 包使用反射(包reflect)来读取/写入值,包括结构。其他也使用反射的库(例如 TOML 和 YAML 的实现)可能以类似的方式(甚至以相同的方式)运行,因此这里介绍的原则也可能适用于这些库。您需要使用您使用的库对其进行测试。

为简单起见,此处介绍的解决方案使用标准库的encoding/json


一个优雅且“零努力”的解决方案是使用 encoding/json 包并解组为“准备好的”默认配置的值

这可以处理您需要的一切:

  • 配置文件中缺少值:默认适用
  • 文件中给出的值会覆盖默认配置(无论它是什么)
  • 显式覆盖文件中的零值优先(覆盖非零默认配置)

为了演示,我们将使用这个配置结构:

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

以及默认配置:

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

假设文件包含以下配置:

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

文件配置覆盖S2S3S5 字段。

加载配置的代码:

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)

以及输出(在Go Playground 上试试):

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

分析结果:

  • S1 默认为零,从文件中丢失,结果为零
  • S2 默认为零,在文件中给出,结果为文件值
  • S3 在配置中给出,在文件中被覆盖为零,结果为零
  • S4 在配置中给出,在文件中丢失,结果是默认值
  • S5 在配置中给出,在文件中给出,结果是文件值

【讨论】:

  • 超酷,但对我的情况有一点警告......文件必须采用 TOML 语法,并且读取它的函数确实已经将其返回为 Config 实例......因此,一切都将被这个fileContent 实例覆盖...我没有特别提到这个案例,所以我可能想接受你的回答
  • 原来我可以像使用 JSON 阅读器一样使用 TOML 阅读器...太棒了!
【解决方案2】:

反射会让你的代码变慢。

对于这个结构,我将直接实现Merge() 方法:

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

几乎相同数量的代码,快速且易于理解。

您可以通过使用反射的 uni 测试来覆盖此方法,以确保不会留下新字段。

这就是 Go 的意义所在 - 您编写更多代码以获得快速且易于阅读的代码。

您可能还想查看go generate,它将根据结构定义为您生成方法。也许有一些已经在 GitHub 上实现和可用的事件?以下是执行类似操作的代码示例:https://github.com/matryer/moq

我相信 GitHub 上还有一些包可以在运行时做你想做的事情,例如:https://github.com/imdario/mergo

【讨论】:

  • 我喜欢它;但是需要更多地考虑它,因为我们可能需要它更通用....这是一个具有不同配置部分的复杂项目,因此这种类型的解决方案可能会变得很长...至于缓慢的部分,它在仅初始化,因此我们可能会使用更慢但更通用的解决方案
  • 您想查看go generate,它将根据结构定义为您生成方法。我将更新我的答案以添加这一点。
【解决方案3】:

我在这里看到的另一个问题是检查零值可能是 棘手:如果覆盖结构打算用零覆盖怎么办 价值?

如果您无法使用icza 指出的encoding/json 或其他行为类似的格式编码器,您可以使用两种不同的类型。

type Config struct {
    Path string
    Id   string
    Key  string
    Addr string
    Size uint64
}

type ConfigParams struct {
    Path *string
    Id   *string
    Key  *string
    Addr *string
    Size *uint64
}

现在有了这样的功能:

func merge(conf *Config, params *ConfigParams)

您可以检查params 中的非零字段并取消引用指针以设置conf 中相应字段中的值。这允许您使用 params 中的非零零值字段取消设置 conf 中的字段。

【讨论】:

  • 在我的示例中,当文件包含零值时,我展示了 encoding/json 正确处理,它将覆盖非零默认值,结果将是零值,正如预期的那样。
  • 是的,我知道@icza,您的解决方案很棒,我会推荐它。我的回答是如果他们不能使用你的解决方案,也许是因为他们没有从json文件或其他东西加载配置,他们可以以不同的方式处理它。
【解决方案4】:

此解决方案不适用于您的特定问题,但它可能会帮助遇到类似但不同问题的人:

您可以拥有一个“默认”结构,并为它创建一个修饰函数,而不是创建两个单独的结构来合并。所以在你的情况下:

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

var defcfg = Config {
  path: "/foo",
  id: "default",
  key: "key",
  addr: "1.2.3.4",
  size: 234,
}

还有你的修饰函数:

func myCfg(c *Config) {
  c.key = "different key"
}

这适用于我想要测试大部分未修改的结构的许多小的不同变体的测试:

func TestSomething(t *testing.T) {
  modifiers := []func (*Config){
    .... // modifier functions here
  }
  for _, f := range modifiers {
    tc := defcfg // copy
    f(&tc)
    // now you can use tc.
  }
}

不过,当您将修改后的配置从文件读入结构时,它就没有用了。从好的方面来说:这也适用于零值。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-02
    • 1970-01-01
    • 2020-03-31
    • 1970-01-01
    相关资源
    最近更新 更多