编程 从 v3 迁到 go-yaml v4:改导入路径只是第一步,缩进和 Node 钩子会先暴露问题

2026-11-09 21:31:57

从 v3 迁到 go-yaml v4:改导入路径只是第一步,缩进和 Node 钩子会先暴露问题

v4.0.0 是稳定版,由 YAML 组织维护(原来的 gopkg.in/yaml.v3 仓库已经归档)。版本与导入路径先摆清楚:

版本导入路径状态
v2go.yaml.in/yaml/v2稳定,API 冻结,只收安全修复
v3go.yaml.in/yaml/v3稳定,API 冻结,只收安全修复
v4go.yaml.in/yaml/v4稳定,活跃开发

v3 到 v4 的导入路径是:gopkg.in/yaml.v3 或 go.yaml.in/yaml/v3 → go.yaml.in/yaml/v4。

go get go.yaml.in/yaml/v4

Unmarshal、Marshal、NewDecoder、NewEncoder 都还在,v4 另外提供了 Load 和 Dump,用来带选项、插件和更明确的配置。API 对照如下:

// v3
yaml.Unmarshal(data, &config)
yaml.Marshal(&config)

// v4
yaml.Load(data, &config)
yaml.Dump(&config)

流式处理也从解码器换成了 loader 和 dumper:

// v3
decoder := yaml.NewDecoder(reader)
decoder.Decode(&config)

// v4:构造函数返回 error,先检查再使用
loader, err := yaml.NewLoader(reader)
if err != nil { /* ... */ }
loader.Load(&config)

NewDumper 同理,err := yaml.NewDumper(writer) 之后才 Dump、Close。这里最容易犯的错是把两行写成一行,或者忽略构造函数返回的 error。

默认格式变了:2 空格缩进加紧凑序列

v3 的 Marshal 默认 4 空格缩进,序列保持常规缩进;v4 的 Load 和 Dump 默认 2 空格缩进加 compact sequence。同一份配置的输出差异直接体现在文本上:

v3:

items:
- name: foo
value: 1

v4:

items:
- name: foo
value: 1

如果下游有按缩进做文本比对、diff 或模板替换的流程,这个差异会立刻造成失败。要让 Dump 的输出等值于旧的 Marshal,需要同时加两个选项:

data, err := yaml.Dump(&config, yaml.WithV3Defaults(), yaml.WithLineWidth(-1))

只加 WithV3Defaults() 不够,行宽还得显式设成 -1。另外,presets 只选择默认值,不会回退 v4 的每个 bug 修复或 API 变更。

选项按传入顺序应用,后面的覆盖前面的:

output, err := yaml.Dump(config, yaml.WithV3Defaults(), yaml.WithIndent(2))

常用格式化选项包括 WithIndent(n)、WithCompactSeqIndent(bool)、WithLineWidth(n)、WithUnicode、WithCanonical、WithLineBreak、WithExplicitStart/WithExplicitEnd、WithFlowSimpleCollections。

Load 默认拒绝多文档

包级 yaml.Load 默认拒绝额外的文档。要一条流里逐个文档处理,用 yaml.NewLoader。WithSingleDocument 是跳过这项校验,不是只读第一个:它让后续文档保持未读取、未校验的状态。

err := yaml.Load(input, &config, yaml.WithKnownFields(), yaml.WithUniqueKeys())

WithKnownFields 对应严格字段检查,WithUniqueKeys 管重复键。这两个在迁移里经常需要补上,否则解析行为会悄悄变宽。

代码和 CLI 共用一份 OptsYAML

选项可以直接写在 YAML 文件里,Go 侧用 yaml.OptsYAML 解析并检查 error,再传给 Load/Dump:

indent: 4
compact-seq-indent: false
line-width: 100
go-yaml -Y -C options.yaml config.yaml

CLI 里 -C 加载可复用的选项文件,命令行 -o 覆盖非插件选项。

插件:用 loader-limits 卡嵌套深度

插件按需链接,CLI 本身不下载插件。loader-limits 管嵌套深度和 alias 限制:

import (
"go.yaml.in/yaml/v4"
"go.yaml.in/yaml/v4/plugin/loaderlimits"
)

err := yaml.Load(input, &config,
yaml.WithPlugin(loaderlimits.New(loaderlimits.DepthValue(50))))

核心模块自带内置 parser、loader limits、merge-key 与 alias-data 策略、错误格式化和 tab 缩进;JSON-comments 和 reference-parser 是独立模块。

自定义 UnmarshalYAML 会直接报迁移错误

这是升级时最需要先检查的一处。v3 里常见的钩子签名是:

func (c *Config) UnmarshalYAML(node *yaml.Node) error

这个钩子依赖定义 Node 的具体包。v4 检测到使用 gopkg.in/yaml.v3.Node 或 go.yaml.in/yaml/v3.Node 的钩子时,不会调用它,也不会静默跳过钩子继续解码,而是返回一个迁移错误。经典 API、版本 presets、Load 都一样。v4 实现要把 Node 换成 go.yaml.in/yaml/v4.Node。

旧的回调签名 UnmarshalYAML(func(any) error) error 在 v2/v3/v4 之间仍然可用,不受这条影响。

转换错误变成 *yaml.LoadErrors

错误处理也别直接对 *yaml.TypeError 做类型断言,用 errors.As:

var typeErr *yaml.TypeError
if errors.As(err, &typeErr) {
for _, m := range typeErr.Errors {
fmt.Println(m)
}
}

TypeError.Errors 仍是 []string;需要结构化细节时,用 errors.As 取 *yaml.LoadErrors。

CLI 安装与几个常用开关

go install go.yaml.in/yaml/v4/cmd/go-yaml@latest

标志要写在文件名前面。-n 输出紧凑节点树,包含 tag 和 style;-t 输出 scanner token;-e 输出 parser event;-J 输出 pretty JSON;-o indent=4 设缩进,-o 可重复,-o v3 用 v3 preset,go-yaml -o help 看全部。--plugin 选择已经链接进可执行文件的实现。

--unmarshal/--decode/--marshal/--encode 用来对比经典路径和 Load/Dump 管线。node-aware 输出会保留样式和注释,但不承诺逐字节一致。-f yaml 强制按 YAML 解析输入。

YAML 1.1 的遗留行为

v4 支持大部分 YAML 1.2,同时保留了一些 YAML 1.1 行为:旧式八进制写法,以及加载进 typed Go bool 时 yes/no 仍按布尔处理。迁移带历史配置的项目时,这几条要单独跑用例确认。

什么时候只改导入路径就够

如果代码只用到经典入口,没有自定义 *yaml.Node 钩子,也不依赖输出文本的缩进和行宽,那么第一步可以只改导入路径,跑 go test ./... 看结果。测试里出现格式 diff 的,再补 WithV3Defaults() 和 WithLineWidth(-1)。

必须主动选格式的情况:输出被下游按文本消费、CI 里有 golden file、或者用 go-yaml -n testfile.yaml 做节点级比对。这类项目建议直接定下 v4 格式,或者明确锁死 v3 preset,不要留在“看起来一样”的状态里。

迁移文档给的路径也是三步:最小改动(导入路径加经典入口)、改用新 API、按需上选项与结构化错误。先跑通第一步,再决定第二步走到哪。

链接

  • 仓库:
  • 迁移文档:
  • CLI 文档:
  • pkg.go.dev:
复制全文 生成海报 Go YAML go-yaml 迁移 序列化

推荐文章

程序员茄子在线接单