将 yaml 字段动态解析为 Go 中的一组有限结构之一
Dynamically parse yaml field to one of a finite set of structs in Go
我有一个 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
字段的一种类型。但是真实的例子比较复杂,涉及到更多可能的struct类型,不仅仅是Foo
和Bar
,这就是为什么我不喜欢把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 解析器。
您可以通过实现自定义 UnmarshalYAML
函数来做到这一点。但是,使用 API 的 v2
版本,您基本上可以做与现在相同的事情,只是封装得更好一些。
但是,如果您切换到使用 v3
API,您会得到一个更好的 UnmarshalYAML
,它实际上可以让您在解析的 YAML 节点 before 它被处理成原生的 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
现在您不再需要 kind
和 spec
的描述结构。加载它看起来会有点不同,因为您需要一个可以在其上定义 UnmarshalYAML
的包装根类型,但如果这只是更大结构的一部分,这可能是可行的。您可以在 yaml.Node
的 Tag
字段中访问标签 !foo
。
要与 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)
}
我有一个 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
字段的一种类型。但是真实的例子比较复杂,涉及到更多可能的struct类型,不仅仅是Foo
和Bar
,这就是为什么我不喜欢把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 解析器。
您可以通过实现自定义 UnmarshalYAML
函数来做到这一点。但是,使用 API 的 v2
版本,您基本上可以做与现在相同的事情,只是封装得更好一些。
但是,如果您切换到使用 v3
API,您会得到一个更好的 UnmarshalYAML
,它实际上可以让您在解析的 YAML 节点 before 它被处理成原生的 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
现在您不再需要 kind
和 spec
的描述结构。加载它看起来会有点不同,因为您需要一个可以在其上定义 UnmarshalYAML
的包装根类型,但如果这只是更大结构的一部分,这可能是可行的。您可以在 yaml.Node
的 Tag
字段中访问标签 !foo
。
要与 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)
}