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

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

yaml 文档

比如有以下的 yaml 文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 文档中解析出想要的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 文档时实现一些基本的校验,或者也可以记录当前解析的数据结构在文档节点中定义的行列位置,用于辅助错误提示等:

1
2
3
4
5
// 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 文档,也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//
// 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 接口:

1
2
3
4
5
6
7
8
9
// 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