从 v3 迁到 go-yaml v4:改导入路径只是第一步,缩进和 Node 钩子会先暴露问题
v4.0.0 是稳定版,由 YAML 组织维护(原来的 gopkg.in/yaml.v3 仓库已经归档)。版本与导入路径先摆清楚:
| 版本 | 导入路径 | 状态 |
|---|---|---|
| v2 | go.yaml.in/yaml/v2 | 稳定,API 冻结,只收安全修复 |
| v3 | go.yaml.in/yaml/v3 | 稳定,API 冻结,只收安全修复 |
| v4 | go.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: