runsisi's

technical notes

Go 读写 yaml 文档

2019-05-10 runsisigolang

Go 对 yaml 的读写没有标准库支持,第三方库中 go-yaml/yaml 的支持比较好。

go-yaml/yaml 的使用非常简单,下面举例说明。

yaml 文档

比如有以下的 yaml 文档:

table: filter

enabled: [mon,osd]
disabled: []

ipv4:
  mon:
    priority: 100
    tcp:
    - "-p tcp -m multiport --dport 6789,3300 -m state --state NEW -j ACCEPT"
    - "-p tcp -m multiport --dport 6789,3300 -m state --state ESTABLISHED -j ACCEPT"
  osd:
    priority: 200
    tcp:
    - "-p tcp -m multiport --dport 6800:7300 -m state --state NEW -j ACCEPT"
    - "-p tcp -m multiport --dport 6800:7300 -m state --state ESTABLISHED -j ACCEPT"

Go 数据结构

我们想要的 Go 数据结构如下:

type Config struct {
	Table *string `yaml:"table,omitempty"`

	Enabled  []string `yaml:"enabled,flow,omitempty"`
	Disabled []string `yaml:"disabled,flow,omitempty"`

	RulesV4 map[string]Rule `yaml:"ipv4,omitempty"`
	RulesV6 map[string]Rule `yaml:"ipv6,omitempty"`
}

type Rule struct {
	Priority *int         `yaml:"priority,omitempty"`
	TCP      []string     `yaml:"tcp,omitempty"`
	UDP      []string     `yaml:"udp,omitempty"`
}

反序列化

如下的代码可以非常容易的从前面的 yaml 文档中解析出想要的数据结构:

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (raw *Config) UnmarshalYAML(node *yaml.Node) error {
	defaultTable := "filter"
	*raw = Config{
		Table: &defaultTable,
	}
	type plain Config
	if err := node.Decode((*plain)(raw)); err != nil {
		return errors.Wrapf(err, "yaml: failed to decode document: %s", err)
	}
	return nil
}

// Parse parses a config fragment in YAML format.
func Parse(data []byte) (raw Config, err error) {
	decoder := yaml.NewDecoder(bytes.NewBuffer(data))
	decoder.KnownFields(true)

	if err = decoder.Decode(&raw); err != nil {
		return Config{}, errors.Wrap(err, "config: failed to parse config file")
	}
	return
}

自定义 yaml.Unmarshaler 接口是可选的,可以在解析 yaml 文档时实现一些基本的校验,或者也可以记录当前解析的数据结构在文档节点中定义的行列位置,用于辅助错误提示等:

// The Unmarshaler interface may be implemented by types to customize their
// behavior when being unmarshaled from a YAML document.
type Unmarshaler interface {
	UnmarshalYAML(value *Node) error
}

序列化

至于序列化为 yaml 文档,也很简单:

//
// write to stdout
//
// https://github.com/go-yaml/yaml/issues/166
// https://github.com/go-yaml/yaml/pull/455
var buffer bytes.Buffer
encoder := yaml.NewEncoder(&buffer)
encoder.SetIndent(2)
err = encoder.Encode(raw)
if err != nil {
  return errors.Wrapf(err, "failed to encode config: %s", err)
}

_, err = os.Stdout.Write(buffer.Bytes())

同样的,可以通过类似上面自定义 yaml.Unmarshaler 的例子自定义 yaml.Marshaler 接口:

// The Marshaler interface may be implemented by types to customize their
// behavior when being marshaled into a YAML document. The returned value
// is marshaled in place of the original value implementing Marshaler.
//
// If an error is returned by MarshalYAML, the marshaling procedure stops
// and returns with the provided error.
type Marshaler interface {
	MarshalYAML() (interface{}, error)
}

注意事项

并不是只有结构体才能自定义 yaml.Unmarshaleryaml.Marshaler 接口,只需要将这些字段的类型使用 type 关键字定义成用户自定义类型,然后就可以为这些自定义类型实现 yaml.Unmarshaleryaml.Marshaler 接口。

上面的反序列化和序列化分别用到了 yaml.Decoderyaml.Encoder,如果不需要特殊设置参数,也可以直接使用 yaml.Unmarshalyaml.Marshal 接口。

需要注意的是,前面提到由于 Go 的基础类型没有 nil 值,所以 Go 数据结构中的基础类型要定义成指针类型,否则 yaml 文档中的 0 值与文档结构中字段未定义等价,从而导致不管加或者不加 omitempty 都存在歧义:

  1. omitempty 时,文档结构中未定义字段在反序列化时被错误的设置成 0 值;
  1. omitempty 时,文档结构中未定义字段在反序列化时被错误的设置成 0 值,同时 0 值在序列化成文档结构时被忽略变成未定义字段;

参考资料

A tour of YAML parsers for Go

http://sweetohm.net/article/go-yaml-parsers.en.html