iter.json:在 Go 中迭代和操作 JSON 的强大而有效的方法
前言:
您是否曾经需要在 Go 中修改非结构化 JSON 数据?也许你不得不删除密码和所有列入黑名单的字段,将键从
camelCase 重命名为 snake_case,或者将所有数字 ID 转换为字符串,因为 JavaScript 不喜欢 int64?如果你的解决方案是使用 encoding/json 将所有内容解组到 map[string]any 中,然后将其封组回来......好吧,让我们面对现实吧,这远非高效!如果您可以遍历 JSON 数据,获取每个项目的路径,并动态决定如何处理它,那会怎么样?
是的!我有个好消息!借助 Go 1.23 中新的迭代器功能,有更好的方法来迭代和操作 JSON。认识 ezpkg.io/iter.json — 您在 Go 中使用 JSON 的强大而高效的伙伴。
世界您好!
ezpkg.io/iter.json 是一个 Go 包,它提供了一种简单而有效的方法来迭代 JSON 数据。它允许您遍历 JSON 对象、数组和值,并对它们执行各种操作,而无需完全解析数据。
让我们看一些如何使用 iter.json 在 Go 中迭代、构建、格式化、过滤和编辑 JSON 数据的示例。
1. 迭代 JSON
假设我们有一个 alice.json 文件:
代码将输出:
2. 构建 JSON
用于构建 JSON 数据。它接受用于缩进的可选参数。请参阅 examples/02.builder。
Builder- 使用 创建新 .
BuilderNewBuilder(prefix, indent string)
Builder.AddRaw(key RawToken, token RawToken)
将原始令牌添加到 JSON 数据中。
Builder.Add(key any, token any)
将键值对添加到 JSON 数据中。
Builder.Bytes()
以字节切片的形式返回 JSON 数据。
- 它接受各种类型,包括 、 等。
stringintstruct[]byte
这将输出带有缩进的 JSON:
3. 格式化 JSON
您可以通过将 JSON 数据的键和值发送到 .参见 examples/03.reformat。
Builder第一个示例缩小 JSON,而第二个示例在每行使用前缀 “👉” 对其进行格式化。
4. 添加行号
在此示例中,我们通过在调用前添加 a 来将行号添加到 JSON 输出中。参见 examples/04.line_number。
b.WriteNewline()fmt.Fprintf()这将输出:
5. 添加评论
通过在 和 之间放置 ,您可以在每行的末尾添加注释。参见 examples/05.comment。
fmt.Fprintf(comment)b.WriteComma()b.WriteNewline()这将输出:
6. 筛选 JSON 并提取值
有 和 来获取当前项目的路径。您可以使用它们来筛选 JSON 数据。参见 examples/06.filter_print。
item.GetPathString()item.GetRawPath()带有 和 的示例 :
item.GetPathString()regexp带有 和 的示例 :
item.GetRawPath()path.Match()这两个示例都将输出:
7. 筛选 JSON 并返回新的 JSON
通过将 与 选项和筛选逻辑结合使用,您可以筛选 JSON 数据并返回新的 JSON。查看示例/07.filter_json
BuilderSetSkipEmptyStructures(false)此示例将返回仅包含筛选字段的新 JSON:
8. 编辑值
这是编辑 JSON 数据中的值的示例。假设我们为 API 使用数字 ID。ID 太大,JavaScript 无法处理它们。我们需要将它们转换为字符串。请参阅 examples/08.number_id 和 order.json。
迭代 JSON 数据,找到所有字段并将数字 ID 转换为字符串:
_id这将向数字 ID 添加引号:
它如何解析 JSON 数据
这是得益于 Go 1.23 中强大的迭代器,ezpkg.io/iter.json 能够以最少的代码行和较低的开销处理 JSON 数据。
核心解析器逻辑包含在 2 个文件中:scanner.go 和 parser.go。以下是其工作原理的简要概述:
- NextToken() 从输入中提取下一个 RawToken。
- Parse() 是一个带有堆栈的状态机。它从 Importing 中提取下一个 token,然后根据当前状态对其进行处理。
- RawToken() 是具有 TokenType 和可选 raw 的标记联合。
[]byte
NextToken() 从输入中提取下一个 Token
以下是 NextToken() 函数 (scanner.go) 的核心逻辑:
Scan() 在单个循环中的所有标记
Scan() 函数本质上是一个循环,每次都从输入中提取下一个标记。
Parse() 是一个带有堆栈的状态机
这是解析器 (parse.go) 的核心逻辑。
- 它使用堆栈来跟踪 JSON 数据的当前状态(路径、级别)。
- 它从输入中提取下一个令牌,并根据当前状态对其进行处理:
- 如果是 或 ,则将当前状态推送到堆栈。
[{ - 如果是 或 ,则从堆栈中弹出状态。
]} - 否则,它会根据当前状态解析 “value” 或 “key: value”。
以下是它初始化堆栈的方式:
通过 PathItem 的实现:
和 , , 辅助函数:
push()pop()advance()核心状态机代码如下。老实说,在这种情况下使用非常有趣:
goto如何动态构建 JSON 数据
基本上只使用单个方法实现 Builder 生成有效的 JSON 也是一个有趣的挑战!
Add(key any, value any)从 RawToken 重建 JSON
首先,让我们看看如何在不使用 .这是 Reconstruct() 最简单的实现,它生成一个缩小的 JSON:
RawTokenBuilder- 它迭代结果以检索密钥和令牌。
Parse()
- 令牌可以是 、 或 值。请注意,和 不是由 返回的。
[{]},:Parse()
- 它将密钥和令牌写入缓冲区,并在必要时添加逗号。
- 要在令牌之间正确添加逗号,它需要跟踪最后一个令牌类型并调用 .
ShouldAddComma()
函数:
ShouldAddComma()- 跳过逗号,最后一个标记是 、 、 或 ,或者下一个标记是 或 。
[{,:]}
- 否则,请添加逗号。
如何支持缩进
为了支持缩进,我们需要跟踪当前级别并在每行前添加适当数量的空格。以下是我们如何修改函数以支持缩进:
Reconstruct()- Add 和 arguments 添加到函数中。
prefixindent
- 在每行之前添加 。
prefix
- 添加 for each level 的缩进。
indent
- 使用 from the result 确定缩进级别。
LevelParse()
或者,我们可以通过为每个
[,{ 和 ],} 递增和递减一个计数器来跟踪级别。我们还可以使用 stack 来跟踪级别和当前路径。下面是修改后的函数,支持缩进,如 Reformat():
Builder 的早期实施
所以你明白了 / 函数是如何工作的。现在,让我们看看 是如何实现的。
Reconstruct()Reformat()Builder它从向 JSON 数据添加原始令牌的方法开始。以下是早期的实现:
AddRaw()代码与 Code 基本相同,但有一些不同:
Reformat()- 它跟踪最后一个标记类型、当前级别和 和 的堆栈。
[{
- 要跟踪级别,它需要打开或关闭令牌以更新堆栈和级别。
- 而不是像 .
Key.IsValue()Reformat()
- 它将密钥和令牌写入缓冲区,并在必要时添加逗号。
扭曲代码以支持更多用例Builder
随着更多用例的添加,代码会随着时间的推移而发展并变得更加复杂。
BuilderWriteNewline() 被公开以控制 prefix 的位置。这是因为我们需要控制行号的位置:
- 所以行号可以在逗号和换行符后添加,
- 但在 key 和 token 之前。
WriteNewline()是可选的。- 如果您不包含它,则 next 将自动调用它。
b.Add()
- 如果你这样做了,它将被记住并跳过电话。
b.Add()
它也很聪明。如果 没有配置缩进,则不会添加任何换行符。它不会添加第一个换行符,也不会添加两个换行符。
Builder这样,API 变得更加灵活:易于使用,同时仍允许更高级的用例。
和
WriteComma() 进行注释。这同样适用于 .它在 添加注释 示例中用于控制注释的位置。
WriteComma()- 与 , 一样,是可选的且很智能。它只会在必要时添加逗号,以始终生成有效的 JSON。
WriteNewline()WriteComma()
- 因此,如果你调用 then ,它实际上不会添加任何逗号,否则 JSON 将变得无效。
Add("", TokenObjectOpen)WriteComma()
SetSkipEmptyStructures(true) 忽略空结构。在 Filtering JSON and returning a new JSON 示例中,我们用于忽略空结构。
SetSkipEmptyStructures(true)如果没有此选项,则会将空 or 添加到输出 JSON 中。尝试删除它,输出将变为:
Builder{}[]请注意该字段的空值。但它是如何工作的呢?
[]scores- 要使其正常工作,不应在收到令牌时立即写入 the,因为它不知道里面是否有任何项目。
Builder"scores": [[
- 相反,它会写入备用缓冲区。并保存当前状态的快照。
- 下次编辑新令牌时,如果它为空,则将清除备用缓冲区,恢复上一个快照,并跳过空字段。
Add]"scores"
- 否则,它会切换回主缓冲区,包括备用缓冲区的内容。
- 这样,就可以以最小的开销跳过空结构。
Builder
下面是使用 和 的方法的更新实现:
add()switchAltBuf()restore(snapshot)所有测试均通过
该软件包以良好的测试开始:
- 它包括对大多数核心功能和许多边缘情况的测试:scanner_test.go、parser_test.go、reconstruct_test.go。
- 测试数据来自各种来源:serde-rs、jsonchecker、fastjson、rapidjson。
这是第一个版本,因此仍有改进的空间,例如模糊测试或基准测试。但是这些测试是确保 package 的核心 logic 按预期工作的良好起点。
缺失的功能和未来的工作
这仅仅是个开始。还有更多功能和改进可以添加到包中:
- 查询复杂值:如对象数组、嵌套对象等。
- 支持读取器/写入器:处理大型 JSON 数据。
- 支持 JSONL:处理以行分隔的 JSON。
- 支持 ProtoBuf JSON:处理来自 ProtoBuf 的 JSON 数据。
- 易于使用的 API:用于处理筛选、转换等常见用例。
- 更多示例:演示如何在实际场景中使用该包。
- Optimize, benchmark, and fuzz: 确保软件包高效可靠。
- 还有更多...
结论
ezpkg.io/iter.json 软件包使 Go 开发人员能够精确高效地处理 JSON 数据。无论您是需要迭代复杂的 JSON 结构、动态构建新的 JSON 对象、格式化或缩小数据、筛选特定字段,还是转换值,iter.json 都能提供灵活而强大的解决方案。
来自:
Oliver Nguyeniter.json: A Powerful and Efficient Way to Iterate and Manipulate JSON in Go

iter.json: A Powerful and Efficient Way to Iterate and Manipulate JSON in Go
Have you ever needed to modify unstructured JSON data in Go? Maybe you’ve had to delete password and all blacklisted fields, rename keys from camelCase to snake_case, or convert all number ids to strings because JavaScript does not like int64? If your solution has been to unmarshal everything into a map[string]any using encoding/json and then marshal it back… well, let’s face it, that’s far from efficient! What if you could loop through the JSON data, grab the path of each item, and decide exactly what to do with it on the fly?
作者:A-Persimmons
声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
Previous
结构化提示词到底是什么?Deepseek V3.1
Next
三阶段学习即兴表达