【问题标题】:Dynamically parse yaml field to one of a finite set of structs in Go将 yaml 字段动态解析为 Go 中的一组有限结构之一
【发布时间】:2021-03-19 14:24:54
【问题描述】:

我有一个yaml 文件,其中一个字段可以由一种可能的结构表示。为了简化代码和 yaml 文件,假设我有这些 yaml 文件:

kind: "foo"
spec:
  fooVal: 4
kind: "bar"
spec:
  barVal: 5

还有这些用于解析的结构体:

    type Spec struct {
        Kind string      `yaml:"kind"`
        Spec interface{} `yaml:"spec"`
    }
    type Foo struct {
        FooVal int `yaml:"fooVal"`
    }
    type Bar struct {
        BarVal int `yaml:"barVal"`
    }

我知道我可以将map[string]interface{} 用作Spec 字段的一种类型。但是实际的例子更复杂,涉及到更多可能的结构类型,不仅仅是FooBar,这就是为什么我不喜欢将spec解析到字段中。

我找到了一个解决方法:将 yaml 解组为中间结构,然后检查 kind 字段,并将 map[string]interface{} 字段编组到 yaml 中,并将其解组为具体类型:

    var spec Spec
    if err := yaml.Unmarshal([]byte(src), &spec); err != nil {
        panic(err)
    }
    tmp, _ := yaml.Marshal(spec.Spec)
    if spec.Kind == "foo" {
        var foo Foo
        yaml.Unmarshal(tmp, &foo)
        fmt.Printf("foo value is %d\n", foo.FooVal)
    }
    if spec.Kind == "bar" {
        tmp, _ := yaml.Marshal(spec.Spec)
        var bar Bar
        yaml.Unmarshal(tmp, &bar)
        fmt.Printf("bar value is %d\n", bar.BarVal)
    }

但它需要额外的步骤并消耗更多的内存(真正的 yaml 文件可能比示例中的大)。是否存在一些更优雅的方式来将 yaml 动态解组为一组有限的结构?

更新:我正在使用github.com/go-yaml/yaml v2.1.0 Yaml 解析器。

【问题讨论】:

  • 你使用的是哪个 yaml 包和包的版本?
  • @mkopriva 抱歉,更新了问题

标签: parsing go yaml marshalling unmarshalling


【解决方案1】:

要与yaml.v2 一起使用,您可以执行以下操作:

type yamlNode struct {
    unmarshal func(interface{}) error
}

func (n *yamlNode) UnmarshalYAML(unmarshal func(interface{}) error) error {
    n.unmarshal = unmarshal
    return nil
}

type Spec struct {
    Kind string      `yaml:"kind"`
    Spec interface{} `yaml:"-"`
}
func (s *Spec) UnmarshalYAML(unmarshal func(interface{}) error) error {
    type S Spec
    type T struct {
        S    `yaml:",inline"`
        Spec yamlNode `yaml:"spec"`
    }

    obj := &T{}
    if err := unmarshal(obj); err != nil {
        return err
    }
    *s = Spec(obj.S)

    switch s.Kind {
    case "foo":
        s.Spec = new(Foo)
    case "bar":
        s.Spec = new(Bar)
    default:
        panic("kind unknown")
    }
    return obj.Spec.unmarshal(s.Spec)
}

https://play.golang.org/p/Ov0cOaedb-x


要与yaml.v3 一起使用,您可以执行以下操作:

type Spec struct {
    Kind string      `yaml:"kind"`
    Spec interface{} `yaml:"-"`
}
func (s *Spec) UnmarshalYAML(n *yaml.Node) error {
    type S Spec
    type T struct {
        *S   `yaml:",inline"`
        Spec yaml.Node `yaml:"spec"`
    }

    obj := &T{S: (*S)(s)}
    if err := n.Decode(obj); err != nil {
        return err
    }

    switch s.Kind {
    case "foo":
        s.Spec = new(Foo)
    case "bar":
        s.Spec = new(Bar)
    default:
        panic("kind unknown")
    }
    return obj.Spec.Decode(s.Spec)
}

https://play.golang.org/p/ryEuHyU-M2Z

【讨论】:

    【解决方案2】:

    您可以通过实现自定义UnmarshalYAML 函数来做到这一点。但是,使用v2 版本的 API,您基本上会做和现在一样的事情,只是封装得更好一点。

    但是,如果您切换到使用 v3 API,您将获得更好的 UnmarshalYAML,它实际上可以让您在已解析的 YAML 节点上工作它被处理为本机 Go 类型。看起来是这样的:

    package main
    
    import (
        "errors"
        "fmt"
        "gopkg.in/yaml.v3"
    )
    
    type Spec struct {
        Kind string      `yaml:"kind"`
        Spec interface{} `yaml:"spec"`
    }
    type Foo struct {
        FooVal int `yaml:"fooVal"`
    }
    type Bar struct {
        BarVal int `yaml:"barVal"`
    }
    
    func (s *Spec) UnmarshalYAML(value *yaml.Node) error {
        s.Kind = ""
        for i := 0; i < len(value.Content)/2; i += 2 {
            if value.Content[i].Kind == yaml.ScalarNode &&
                value.Content[i].Value == "kind" {
                if value.Content[i+1].Kind != yaml.ScalarNode {
                    return errors.New("kind is not a scalar")
                }
                s.Kind = value.Content[i+1].Value
                break
            }
        }
        if s.Kind == "" {
            return errors.New("missing field `kind`")
        }
        switch s.Kind {
        case "foo":
            var foo Foo
            if err := value.Decode(&foo); err != nil {
                return err
            }
            s.Spec = foo
        case "bar":
            var bar Bar
            if err := value.Decode(&bar); err != nil {
                return err
            }
            s.Spec = bar
        default:
            return errors.New("unknown kind: " + s.Kind)
        }
        return nil
    }
    
    var input1 = []byte(`
    kind: "foo"
    spec:
      fooVal: 4
    `)
    
    var input2 = []byte(`
    kind: "bar"
    spec:
      barVal: 5
    `)
    
    func main() {
        var s1, s2 Spec
        if err := yaml.Unmarshal(input1, &s1); err != nil {
            panic(err)
        }
        fmt.Printf("Type of spec from input1: %T\n", s1.Spec)
        if err := yaml.Unmarshal(input2, &s2); err != nil {
            panic(err)
        }
        fmt.Printf("Type of spec from input2: %T\n", s2.Spec)
    }
    

    我建议研究使用 YAML 标签而不是您当前的结构在您的 YAML 中建模的可能性;标签正是为此目的而设计的。而不是当前的 YAML

    kind: "foo"
    spec:
      fooVal: 4
    

    你可以写

    --- !foo
    fooVal: 4
    

    现在您不再需要 kindspec 的描述结构了。加载它看起来会有些不同,因为您需要一个可以定义 UnmarshalYAML 的包装根类型,但如果这只是更大结构的一部分,它可能是可行的。您可以在yaml.NodeTag 字段中访问标签!foo

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-10-24
      • 2018-03-26
      • 1970-01-01
      • 2018-05-24
      • 1970-01-01
      • 2021-03-13
      • 1970-01-01
      相关资源
      最近更新 更多