当前位置: 首页
编程语言
Go语言YAML转JSON踩坑与解决方法

Go语言YAML转JSON踩坑与解决方法

时间:2026-07-26
转载

YAML转JSON时,yaml v3将数字键1与字符串键 "1 "视为重复致解析错误;解析所得map[interface{}]interface{}无法直接JSON序列化,需dyno库转换键类型。yaml v3已停维护,可用go-yaml替代。

最近在搬砖过程中,遇到一个 Go 代码里 YAML 转 JSON 引发报错的小问题,顺手记录一下。场景是这样的:我实现了一个功能,支持用户上传 YAML 或 JSON 格式的文档,为了方便统一处理,我会先把所有文档转成 JSON 格式。说实话,平时操作 YAML 和 JSON 都很频繁,但二者互转的场景确实不怎么遇到,所以一时疏忽就踩了个坑。这个 Go YAML 转 JSON 的常见问题,希望能帮到遇到类似情况的开发者。

使用 yaml.v3 包处理 YAML

先看一段 YAML 文档示例:

object:
  a: 1
  1: 2
  "1": 3
  key: value
  array:
  - null_value: 
  - boolean: true
  - integer: 1

这是一个对象结构,里面还嵌套了一个数组。细心的读者可能已经注意到,对象中有两个 key 长得比较像:一个是数字类型的 1,另一个是字符串类型的 "1"。这个细节在 Go YAML 解析中非常关键,容易引发键冲突问题。

在 Go 语言里,我们通常用 yaml.v3 处理 YAML,用内置的 encoding/json 处理 JSON。下面这段代码试图把上面的 YAML 文档转成 JSON:

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/icza/dyno" // 用于递归转换 map[interface{}] => map[string]
	"gopkg.in/yaml.v3"     // YAML 解析库
)

func main() {
	yamlExample := `
object:
  a: 1
  1: 2
  "1": 3
  key: value
  array:
  - null_value: 
  - boolean: true
  - integer: 1
`

	// 解析 YAML
	var data interface{}
	if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
		log.Fatalf("YAML parse error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %#vn", data, data)

	fmt.Println("--------------------------")

	// 关键:递归转换 map 键类型(interface{} → string)
	convertedData := dyno.ConvertMapI2MapS(data)

	// 转换为 JSON
	jsonData, err := json.Marshal(convertedData)
	if err != nil {
		log.Fatalf("JSON convert error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}

运行一下,结果直接报错:

$ go run main.go

2025/07/31 23:22:49 YAML parse error: yaml: unmarshal errors:

line 5: mapping key "1" already defined at line 4

exit status 1

错误信息说 "1" 这个 key 已经存在了。那咱们先注释掉 "1": 3 试试:

yamlExample := `
object:
  a: 1
  1: 2
  # "1": 3
  key: value
  array:
  - null_value: 
  - boolean: true
  - integer: 1
`

再次运行,这次成功了:

$ go run main.go

Type: map[string]interface {}

Value: map[string]interface {}{"object":map[interface {}]interface {}{"a":1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":1}}, "key":"value", 1:2}}

--------------------------

Type: []uint8

Value: {"object":{"1":2,"a":1,"array":[{"null_value":null},{"boolean":true},{"integer":1}],"key":"value"}}

可以看到,在 yaml.v3 中,1 和 "1" 会被当作同一个 key 处理,导致冲突。另外,细心的读者可能注意到了,代码里 yaml.Unmarshal 出来的 data 并没有直接交给 json.Marshal,而是先经过 dyno.ConvertMapI2MapS(data) 转换了一遍。为什么需要这一步?这是 Go YAML 转 JSON 时必须注意的 map 类型转换细节。

如果去掉这个转换,直接写:

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"gopkg.in/yaml.v3" // YAML 解析库
)

func main() {
	yamlExample := `
object:
  a: 1
  1: 2
  # "1": 3
  key: value
  array:
  - null_value: 
  - boolean: true
  - integer: 1
`

	// 解析 YAML
	var data interface{}
	if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
		log.Fatalf("YAML parse error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %#vn", data, data)

	fmt.Println("--------------------------")

	// 转换为 JSON
	jsonData, err := json.Marshal(data)
	if err != nil {
		log.Fatalf("JSON convert error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}

运行结果:

$ go run main.go

Type: map[string]interface {}

Value: map[string]interface {}{"object":map[interface {}]interface {}{"a":1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":1}}, "key":"value", 1:2}}

--------------------------

2025/07/31 23:34:26 JSON convert error: json: unsupported type: map[interface {}]interface {}

exit status 1

这次报错换成了 json.Marshal 不支持 map[interface {}]interface {} 类型。把反序列化出来的对象整理一下,就能看清楚:

map[string]interface{}{
    "object": map[interface{}]interface{}{
        "a": 1,
        "array": []interface{}{
            map[string]interface{}{
                "null_value": interface{}(nil),
            },
            map[string]interface{}{
                "boolean": true,
            },
            map[string]interface{}{
                "integer": 1,
            },
        },
        "key": "value",
        1:     2,
    },
}

问题出在 map[interface {}]interface {} 这个类型上。Go 的 map 允许任何可比较类型作为 key,但 JSON 的 key 必须是字符串。所以需要一个转换函数,把 map[interface {}]interface {} 变成 map[string]interface {}。dyno.ConvertMapI2MapS 就是干这个的,它递归地将所有 interface{} 类型的键转换为字符串,是 Go YAML 转 JSON 时的必备工具:

// ConvertMapI2MapS walks the given dynamic object recursively, and
// converts maps with interface{} key type to maps with string key type.
// This function comes handy if you want to marshal a dynamic object into
// JSON where maps with interface{} key type are not allowed.
//
// Recursion is implemented into values of the following types:
//   -map[interface{}]interface{}
//   -map[string]interface{}
//   -[]interface{}
//
// When converting map[interface{}]interface{} to map[string]interface{},
// fmt.Sprint() with default formatting is used to convert the key to a string key.
func ConvertMapI2MapS(v interface{}) interface{} {
	switch x := v.(type) {
	case map[interface{}]interface{}: // 目标转换类型,需要把 key 为 interface{} 类型的转换成 string
		m := map[string]interface{}{}
		for k, v2 := range x {
			switch k2 := k.(type) {
			case string: // 如果 key 已经是 string 类型,则直接使用
				m[k2] = ConvertMapI2MapS(v2)
			default: // 如果 key 是其他类型则需要转换成 string
				m[fmt.Sprint(k)] = ConvertMapI2MapS(v2)
			}
		}
		v = m

	case []interface{}: // 递归处理数组元素
		for i, v2 := range x {
			x[i] = ConvertMapI2MapS(v2)
		}

	case map[string]interface{}: // key 已经是 string,仅递归处理 value
		for k, v2 := range x {
			x[k] = ConvertMapI2MapS(v2)
		}
	}

	return v
}

代码逻辑很清晰:递归遍历,把 map[interface{}]interface{} 的 key 转成字符串。这个工具在 Go YAML 解析和 JSON 序列化之间起到了关键的桥梁作用。

那么,为什么 yaml.Unmarshal 不直接返回 map[string]interface{} 呢?其实在 yaml.v3 的 issues 里有人问过,作者的回答是:

Unfortunately not.. Unlike json, yaml can take keys of arbitrary types.

因为 YAML 规范允许任意类型的值作为对象的 key,所以 1: 2 和 "1": 3 同时存在在 YAML 中是合法的。因此,在 Go 里做 YAML 转 JSON 时,保险起见一定要加上 dyno.ConvertMapI2MapS,避免直接把 map[interface{}]interface{} 丢给 json.Marshal。这是 Go 语言 YAML 转 JSON 的经典踩坑点。

Go语言中YAML转JSON的踩坑笔记与解决方法

顺便提一下,gopkg.in/yaml.v3 是 Canonical 公司(Ubuntu 背后的公司)为支持 Juju 项目开发的社区库,也是 Go 社区最常用的 YAML 解析库。不过,这个项目在 2025 年 4 月 2 日已被作者标记为不再维护。为此,我特意测试了另一个 Go YAML 包——go-yaml,看看效果如何。

使用 go-yaml 包处理 YAML

go-yaml 的用法和 yaml.v3 几乎一模一样,只需要改一下 import:

package main

import (
	"encoding/json"
	"fmt"
	"log"

	"github.com/goccy/go-yaml" // YAML 解析库
)

func main() {
	yamlExample := `
object:
  a: 1
  1: 2
  "1": 3
  key: value
  array:
  - null_value: 
  - boolean: true
  - integer: 1
`

	// 解析 YAML
	var data interface{}
	if err := yaml.Unmarshal([]byte(yamlExample), &data); err != nil {
		log.Fatalf("YAML parse error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %#vn", data, data)

	fmt.Println("--------------------------")

	// 转换为 JSON
	jsonData, err := json.Marshal(data)
	if err != nil {
		log.Fatalf("JSON convert error: %v", err)
	}
	fmt.Printf("Type: %TnValue: %sn", jsonData, jsonData)
}

运行结果:

$ go run goccy-go-yaml/main.go

2025/07/31 23:25:15 YAML parse error: [5:3] mapping key "1" already defined at [4:3]

2 | object:

3 | a: 1

4 | 1: 2

> 5 | "1": 3

^

6 | key: value

7 | array:

8 | - null_value:

exit status 1

和 yaml.v3 一样,它也无法处理同时存在 1: 2 和 "1": 3 的情况。不过报错信息更清晰,直接指出了具体行号。同样注释掉 "1": 3 后,就成功了:

$ go run goccy-go-yaml/main.go

Type: map[string]interface {}

Value: map[string]interface {}{"object":map[string]interface {}{"1":0x2, "a":0x1, "array":[]interface {}{map[string]interface {}{"null_value":interface {}(nil)}, map[string]interface {}{"boolean":true}, map[string]interface {}{"integer":0x1}}, "key":"value"}}

--------------------------

Type: []uint8

Value: {"object":{"1":2,"a":1,"array":[{"null_value":null},{"boolean":true},{"integer":1}],"key":"value"}}

有趣的是,go-yaml 直接返回了 map[string]interface{},不需要再借助 dyno.ConvertMapI2MapS 做二次转换。也就是说,go-yaml 一个包就能搞定 yaml.v3 + dyno 两个包才能完成的事。对于 Go YAML 转 JSON 的场景,go-yaml 提供了更简洁的解决方案。

到此,Go 里 YAML 转 JSON 的坑就讲完了。不过文章还没完——我用 Python 处理同样的 YAML 文档,看看为什么我当时会疏忽这个问题。

Python 中处理 YAML to JSON

Python 代码:

import json
import yaml
yaml_example = """
object:
  a: 1
  1: 2
  "1": 3
  key: value
  array:
  - null_value:
  - boolean: true
  - integer: 1
"""
def main():
    y = yaml.load(yaml_example, Loader=yaml.SafeLoader)
    print(type(y), y)
    print("--------------------------")
    j = json.dumps(y)
    print(type(j), j)
    print("--------------------------")
    d = json.loads(j)
    print(type(d), d)
if __name__ == '__main__':
    main()

运行结果:

$ python main.py

{'object': {'a': 1, 1: 2, '1': 3, 'key': 'value', 'array': [{'null_value': None}, {'boolean': True}, {'integer': 1}]}}

--------------------------

{"object": {"a": 1, "1": 2, "1": 3, "key": "value", "array": [{"null_value": null}, {"boolean": true}, {"integer": 1}]}}

--------------------------

{'object': {'a': 1, '1': 3, 'key': 'value', 'array': [{'null_value': None}, {'boolean': True}, {'integer': 1}]}}

Python 完美地处理了 1: 2 和 "1": 3 同时存在的场景。注意,json.dumps 序列化出来的字符串里同时出现了两个 "1",但再用 json.loads 反序列化时,只会保留最后一个 '1': 3,因为 JSON 规范要求 key 必须是字符串,重复的 key 会被覆盖。这也体现了 Python 处理 YAML 转 JSON 时的灵活性。

所以你看,同样的 YAML 文档,在 Python 里没问题,在 Go 里就踩坑了。这也是我喜欢 Python 的一点——足够灵活。顺便说一句,Python 虽然灵活,但它是动态强类型语言,不是弱类型,而 Go 是静态强类型语言。类型系统的差异直接导致了 YAML 转 JSON 时处理方式的不同。

原因分析

演示了这么多,YAML 转 JSON 报错的问题,归根结底是 YAML 和 JSON 格式的差异造成的。YAML 是 JSON 的超集,任何合法的 JSON 文档都是合法的 YAML,但反过来不成立——不是所有合法的 YAML 都是合法的 JSON。理解这一点对于正确处理 Go YAML 转 JSON 至关重要。

YAML 允许数字作为对象的 key,这点和 Python 的 dict 完美契合,但和 Go 的严格类型系统存在冲突。所以,同样的 YAML 文档,在 Python 里处理得顺风顺水,在 Go 里就报错了。

问题虽然解决了,但还想再扩展一下关于 YAML 和 JSON 规范的知识,毕竟知其然更要知其所以然。

YAML 规范

YAML 规范官方文档见 yaml.org。这里有一个挺有意思的点:

A question mark and space (“? ”) indicate a complex mapping key. Within a block collection, key/value pairs can start immediately following the dash, colon or question mark.

简单说,问号加空格(? )在 YAML 里表示一个复杂的 mapping 键。举个例子:

? - Detroit Tigers
  - Chicago cubs
: - 2001-07-23

这是一个合法的 YAML 文档。? 后面的内容(列表)是 key,: 后面的内容是 value。

另一个例子:

? [ New York Yankees,
    Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]

key 和 value 都是数组。不过很遗憾,Go 和 Python 的 YAML 库都不支持这种复杂键。但 JavaScript 的 js-yaml 可以:

const yaml = require('js-yaml');

const yamlExample = `
? - Detroit Tigers
  - Chicago cubs
: - 2001-07-23

? [ New York Yankees,
    Atlanta Braves ]
: [ 2001-07-02, 2001-08-12,
    2001-08-14 ]
`;
try {
    const data = yaml.load(yamlExample);
    console.log(data);
} catch (e) {
    console.error(e);
}

解析结果:

{
  'Detroit Tigers,Chicago cubs': [ 2001-07-23T00:00:00.000Z ],
  'New York Yankees,Atlanta Braves': [
    2001-07-02T00:00:00.000Z,
    2001-08-12T00:00:00.000Z,
    2001-08-14T00:00:00.000Z
  ]
}

JavaScript 用了一种变通方式——把数组 key 转成逗号分隔的字符串。这也展示了不同语言对 YAML 规范的支持差异。

JSON 规范

JSON(JavaScript Object Notation)由道格拉斯·克罗克福特设计,是 JavaScript 的子集,也是目前主流编程语言之间数据交换的事实标准。维基百科对 JSON 对象的描述中有一句非常重要的话:

对象:若干无序的“键-值对”(key-value pairs),其中键只能是字符串[2]。建议但不强制要求对象中的键是独一无二的。

关键点就是“键只能是字符串”,这一点要牢记。当你进行 Go YAML 转 JSON 时,必须确保所有键都符合 JSON 规范。

总结

本文通过一个实际案例,记录了 Go 里 YAML 转 JSON 时遇到的坑,并给出了解决方案。同时,简单介绍了 YAML 和 JSON 规范,希望能让大家知其然并知其所以然。这个问题很小,甚至不值一提,几分钟就能搞定,但它背后折射出的是对官方标准规范的忽视——还是倡导大家多读官方文档。

回想编程刚入门的时候,其实很怕看官方文档,觉得晦涩难懂,但很多细节就藏在标准规范里,这是绕不过去的门槛,没有捷径可走。最后,单元测试真的很重要,充分测试你的 YAML 转 JSON 代码,有助于减少 Bug——虽然无法完全避免 :)

好了,本文就到这里。更多关于 YAML 和 JSON 规范的内容,可以到官网查看学习。希望这篇 Go YAML 转 JSON 踩坑笔记能帮你在实际开发中少走弯路。

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
Python 性能调优实战:用 line_profiler 精准定位代码瓶颈

Python 性能调优实战:用 line_profiler 精准定位代码瓶颈

本文以 Python 性能优化中的常见痛点为切入点,介绍如何使用 line_profiler 进行逐行级别的耗时分析。通过安装配置、解读报告数据、实施针对性优化以及建立可信验证流程四个步骤,帮助开发者从函数级统计深入到代码行级热点,避免盲目优化。文章强调通过对比优化前后的 Time 与 % Time

时间:2026-10-10 15:46
Linux性能监控:top与vmstat指标解读

Linux性能监控:top与vmstat指标解读

从系统负载、CPU、内存、进程与I O等核心维度入手,结合top和vmstat建立一套“发现异常—定位瓶颈—验证结论”的Linux性能监控方法,并梳理常见误判与排查避坑点。

时间:2026-10-10 15:41
SQL 变更安全指南:从规范自检到回滚闭环

SQL 变更安全指南:从规范自检到回滚闭环

本文聚焦数据库变更的安全落地,梳理从代码编写规范、工单审核标准到上线验证与回滚的完整链路。通过明确执行计划分析、分批处理与双重验证等关键动作,帮助团队建立可追溯、可验证且具备快速恢复能力的数据库变更机制,降低生产环境风险。

时间:2026-10-10 15:36
数据库可观测性:Zabbix与PMM的分工与协作

数据库可观测性:Zabbix与PMM的分工与协作

在数据库运维中,监控工具并非越多越好,关键在于厘清“基础设施可用性”与“数据库内核性能”的边界。本文从监控定位、能力差异、落地部署及避坑指南四个维度,对比Zabbix与Percona PMM的核心特性。通过明确两者的适用场景与协作方式,帮助团队避免重复建设与监控盲区,构建分层、高效的数据库可观测体系

时间:2026-10-10 15:31
Docker健康检查:HEALTHCHECK与curl探测

Docker健康检查:HEALTHCHECK与curl探测

从容器为什么需要健康检查入手,结合 Dockerfile 中的 HEALTHCHECK 与 curl HTTP 探测,完成配置、运行验证,并总结常见误区与生产实践。

时间:2026-10-10 15:26
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜