如何在Go中读写结构未知的YAML文档指定值?
实现未知结构YAML的读写操作
需要开发一个程序,支持读写结构未知的YAML文档中的指定值(无法通过预定义结构体加载),且访问的索引由用户动态指定(无法通过简单类型断言直接获取数据)。
目前使用yaml.v3将YAML加载为map[interface{}]interface{},已实现readYaml()函数可通过可变参数接收索引,支持字符串/整数类型,能识别映射与列表结构并读取值,但该函数通过迭代赋值next变量的方式仅能获取值,无法实现写入操作。
曾尝试将YAML加载为map[interface{}]*interface{},但仅第一层为指针类型,后续层级仍为普通interface{}或其他映射类型,无法获取值的指针来完成写入。
现有代码
package main import ( "fmt" "os" "log" "gopkg.in/yaml.v3" "errors" ) type YAML map[interface{}]interface{} // Throw appropriate error if the file does not exist func checkFile(path string) { _, err := os.Stat(path) if err != nil { log.SetPrefix("file op: ") log.Fatal(fmt.Sprintf("%s is an invalid file path", path)) } } // Temp function to help debugging func describe(i interface{}) { fmt.Printf("(%v, %T)\n", i, i) } // Report an error with a prefix and the error contents func reportError(prefix string, err error) { if err == nil { return } log.SetPrefix(fmt.Sprintf("ERROR %s", prefix)) log.Fatal(err) } // Read a value from a yaml document func (yamlIn YAML) readYaml(keys ...interface{}) (interface{}, error){ var next interface{} = yamlIn // Iterate over passed in keys/indices for i, key := range keys { //key value map (YAML or map[string]interface{} type) or list ([]interface{}) switch next.(type){ case YAML: // Choose whether the key is a string or int to assert the type and get the next structure switch key.(type) { case string: next = next.(YAML)[key.(string)] case int: next = next.(YAML)[key.(int)] default: return nil, errors.New("string or int indices only") } case map[string]interface{}: // Choose whether the key is a string or int to assert the type and get the next structure switch key.(type) { case string: next = next.(map[string]interface{})[key.(string)] default: return nil, errors.New("cannot use non string for a string key") } case []interface{}: // Choose whether the key is a string or int to assertate the type and get the next strucutre switch key.(type) { case string: return nil, errors.New("cannot use string to index list") case int: next = next.([]interface{})[key.(int)] default: return nil, errors.New("string or int indicies only") } default: return nil, errors.New("Unknown yaml structure") } if next == nil { return nil, errors.New("Invalid index") } // If this is the last key return the value if i == len(keys)-1 { return next, nil } } return nil, errors.New("internal failure") } // --------------- Main --------------- func main() { // Get and check files inputFiles := os.Args[1:] for _, path := range inputFiles { checkFile(path) } //fmt.Println("Hello there", strings.Join(inputFiles, " ")) // Read file var files [][]byte for _, path := range inputFiles { file, err := os.ReadFile(path) files = append(files, file) reportError("file op: ", err) } // Load yaml var yamls []YAML for _, file := range files { var yamlFile YAML err := yaml.Unmarshal(file, &yamlFile) yamls = append(yamls, yamlFile) reportError("yaml: ", err) } data, err := yamls[0].readYaml("INDEX") reportError("yaml: ", err) value, err := yamls[0].readYaml(data.([]interface{})...) reportError("yaml: ", err) fmt.Println(value) }
示例YAML文档
CRAZY_FIRST_LEVEL: SECOND_LEVEL: string here WOW: wow LOSING_MY_MIND: - 1 - 2 - 3 - 4 EXAMPLE: FIRST: 1: away UP: 1 SECOND: WHAT: 1 THE: 2 INDEX: - EXAMPLE - FIRST - 1
解决方案:实现读写能力
要实现写入,核心是找到目标值的父节点以及对应的键/索引,而不是仅获取目标值本身。以下是修改方案:
1. 新增获取引用的辅助函数
编写getYamlRef函数,遍历索引时记录父节点和最后一个键/索引,最终返回父节点、键/索引、当前值及错误:
// getYamlRef 返回目标值的父节点、对应的键/索引、当前值,用于后续修改 func (yamlIn YAML) getYamlRef(keys ...interface{}) (parent interface{}, key interface{}, current interface{}, err error) { if len(keys) == 0 { return nil, nil, yamlIn, errors.New("no keys provided") } var currentNode interface{} = yamlIn var parentNode interface{} var lastKey interface{} for i, k := range keys { parentNode = currentNode lastKey = k switch node := currentNode.(type) { case YAML: switch keyType := k.(type) { case string: currentNode = node[keyType] case int: currentNode = node[keyType] default: return nil, nil, nil, errors.New("string or int indices only for YAML map") } case map[string]interface{}: keyStr, ok := k.(string) if !ok { return nil, nil, nil, errors.New("non-string key used for string map") } currentNode = node[keyStr] case []interface{}: idx, ok := k.(int) if !ok { return nil, nil, nil, errors.New("non-int index used for list") } if idx < 0 || idx >= len(node) { return nil, nil, nil, errors.New("list index out of range") } currentNode = node[idx] default: return nil, nil, nil, errors.New("unknown yaml structure at key step") } if currentNode == nil { return nil, nil, nil, errors.New("invalid index: key not found") } // 如果是最后一个键,跳出循环并返回父节点和键 if i == len(keys)-1 { break } } return parentNode, lastKey, currentNode, nil }
2. 实现写入函数
利用getYamlRef返回的父节点和键,修改对应位置的值:
// writeYaml 将指定索引路径的值修改为newValue func (yamlIn YAML) writeYaml(newValue interface{}, keys ...interface{}) error { parent, key, _, err := yamlIn.getYamlRef(keys...) if err != nil { return err } switch p := parent.(type) { case YAML: p[key] = newValue case map[string]interface{}: keyStr, ok := key.(string) if !ok { return errors.New("invalid key type for string map") } p[keyStr] = newValue case []interface{}: idx, ok := key.(int) if !ok { return errors.New("invalid index type for list") } if idx < 0 || idx >= len(p) { return errors.New("list index out of range") } p[idx] = newValue default: return errors.New("unsupported parent structure for write") } return nil }
3. 测试读写功能
修改main函数,测试读取和写入:
func main() { // ... 原有文件读取、加载代码保持不变 ... // 读取INDEX中的路径 data, err := yamls[0].readYaml("INDEX") reportError("yaml: ", err) path := data.([]interface{}) // 读取原始值 value, err := yamls[0].readYaml(path...) reportError("yaml: ", err) fmt.Println("原始值:", value) // 输出: away // 写入新值 err = yamls[0].writeYaml("new_away_value", path...) reportError("yaml write: ", err) // 验证写入结果 updatedValue, err := yamls[0].readYaml(path...) reportError("yaml: ", err) fmt.Println("更新后的值:", updatedValue) // 输出: new_away_value // 将修改后的YAML写回文件 updatedYaml, err := yaml.Marshal(yamls[0]) reportError("yaml marshal: ", err) err = os.WriteFile(inputFiles[0], updatedYaml, 0644) reportError("file write: ", err) }
编码实践建议
- 统一映射类型:优先使用
map[string]interface{}替代map[interface{}]interface{},因为YAML规范中映射键默认是字符串,yaml.v3默认解析为map[string]interface{},可减少类型断言的复杂度;如果必须支持非字符串键,再保留map[interface{}]interface{}的处理逻辑。 - 错误处理优化:定义自定义错误类型(如
YAMLError),携带更多上下文信息(如当前索引路径),便于调试。 - 边界检查增强:在处理列表索引时,增加越界检查;处理映射键时,检查键是否存在,避免nil指针问题。
- 代码复用:将类型判断、键/索引验证的逻辑抽成独立辅助函数,减少
readYaml和getYamlRef中的重复代码。 - 测试覆盖:编写单元测试,覆盖映射、列表、混合结构、无效索引等多种场景,确保函数稳定性。
- 避免类型断言重复:使用类型断言的
ok模式(如keyStr, ok := k.(string)),避免直接断言导致panic。
内容的提问来源于stack exchange,提问作者Ben Campbell
相关产品推荐
相关产品推荐

