You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 03:50:15