
本文介绍使用 gopkg.in/yaml.v2 解析 YAML 后,通过反射(reflect)递归遍历任意嵌套结构(map、slice、基本类型)中所有键与值的完整方法,并提供可运行示例与关键注意事项。
本文介绍使用 `gopkg.in/yaml.v2` 解析 yaml 后,通过反射(`reflect`)递归遍历任意嵌套结构(map、slice、基本类型)中所有键与值的完整方法,并提供可运行示例与关键注意事项。
YAML 是一种常用配置格式,其天然支持嵌套结构(如 map 和 list),但在 Go 中原生不支持动态遍历——因为解析后的数据通常为 map[string]interface{} 或 map[interface{}]interface{},其中 value 类型不确定。因此,必须借助 reflect 包识别运行时类型,并分情况处理:基础类型(string/int/bool 等)、切片([]interface{})、映射(map[interface{}]interface{} 或 map[string]interface{})。
以下是一个健壮、可扩展的递归遍历实现:
package main
import (
"fmt"
"gopkg.in/yaml.v2"
"reflect"
"strings"
)
var data = `
Data:
- name: "foo"
bar1: 0
k1: val1
k2:
- val2
- val3
bar2: 1
k3: val4
k4: val5
k5: val5
k6: val6
`
// printVal 递归打印任意嵌套值,按缩进体现层级
func printVal(v interface{}, depth int) {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
fmt.Printf("%s<nil>\n", strings.Repeat(" ", depth))
return
}
switch rv.Kind() {
case reflect.String, reflect.Int, reflect.Int8, reflect.Int16,
reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8,
reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64, reflect.Bool:
fmt.Printf("%s%v\n", strings.Repeat(" ", depth), v)
case reflect.Map:
if rv.Len() == 0 {
fmt.Printf("%s(map empty)\n", strings.Repeat(" ", depth))
return
}
// 统一转为 map[interface{}]interface{} 便于遍历(兼容 yaml.v2 默认行为)
m, ok := v.(map[interface{}]interface{})
if !ok {
// 尝试 string-keyed map(部分场景下可能为 map[string]interface{})
if m2, ok2 := v.(map[string]interface{}); ok2 {
printMapString(m2, depth+1)
return
}
fmt.Printf("%s<unhandled map type:>\n", strings.Repeat(" ", depth), rv.Type())
return
}
printMap(m, depth+1)
case reflect.Slice, reflect.Array:
slc := rv.Slice(0, rv.Len()).Interface()
if s, ok := slc.([]interface{}); ok {
printSlice(s, depth+1)
} else {
fmt.Printf("%s<non-interface slice:>\n", strings.Repeat(" ", depth), rv.Type())
}
default:
fmt.Printf("%s<unsupported kind:>\n", strings.Repeat(" ", depth), rv.Kind())
}
}
func printMap(m map[interface{}]interface{}, depth int) {
for k, v := range m {
keyStr := fmt.Sprintf("%v", k)
fmt.Printf("%sKey: %s\n", strings.Repeat(" ", depth), keyStr)
printVal(v, depth+1)
}
}
func printMapString(m map[string]interface{}, depth int) {
for k, v := range m {
fmt.Printf("%sKey: %s\n", strings.Repeat(" ", depth), k)
printVal(v, depth+1)
}
}
func printSlice(slc []interface{}, depth int) {
for i, v := range slc {
fmt.Printf("%s[%d]\n", strings.Repeat(" ", depth), i)
printVal(v, depth+1)
}
}
func main() {
var m map[string]interface{}
err := yaml.Unmarshal([]byte(data), &m)
if err != nil {
panic(fmt.Sprintf("YAML unmarshal error: %v", err))
}
for k, v := range m {
fmt.Printf("Root Key: %s\n", k)
printVal(v, 1)
}
}</unsupported></non-interface></unhandled></nil>
✅ 关键说明与注意事项:
- 类型一致性:yaml.v2 默认将 YAML 映射解码为 map[interface{}]interface{},但若你明确声明为 map[string]interface{} 且 YAML 键全为合法字符串,也可直接使用;本例兼顾两种常见情况。
- 空值与无效值:添加了 reflect.Value.IsValid() 检查,避免 panic;对空 map/slice 也做了友好提示。
- 切片元素类型:YAML 列表(-)被解码为 []interface{},需确保每个元素都可被 printVal 处理。
- 重复键处理:YAML 规范中不允许同一 map 内存在重复键(如原文中 k3: val4 出现两次),实际解析时后出现的键会覆盖前者,因此代码无需特殊处理重复键逻辑。
- 性能考虑:反射有一定开销,生产环境若结构固定,推荐定义 struct 并用 yaml.Unmarshal 直接绑定,仅在需泛型遍历时使用本方案。
该方法适用于调试 YAML 结构、提取所有配置路径、生成文档或做 schema 校验等场景,是 Go 生态中处理动态 YAML 数据的通用实践。











