登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go jsontext.Value 怎样保留 JSON 片段原始表示

来源:17golang原创

时间:2026-11-14 08:24:41 447浏览 收藏

如果你的任务是把 JSON 片段转发、审计或延迟处理,直接解码成 map[string]any 往往会丢掉数字写法、字符串转义和原始字段顺序。Go 的 encoding/json/jsontext 把一个完整 JSON 值表示成 jsontext.Value,本质上是保存原始文本的 []byte。

真正容易踩坑的是生命周期:Decoder.ReadValue 返回的值由 Decoder 的内部缓冲区支撑,下一次读取后就不能继续把它当作长期副本。要保留片段,应立即调用 Clone;要保留字符串里的转义写法,则在重新格式化时显式使用 PreserveRawStrings(true)。

一句话结论:ReadValue 负责拿到当前 JSON 值,Clone 负责把它保存下来,Format/Indent/Compact 负责有边界地改变排版;如果要改对象字段,应重新构造合法 Value,而不是把 Value 当成可随意编辑的 map。

先分清 Value 保留的是什么

Value 可以代表字符串、数字、布尔值、null,也可以代表整个对象或数组;Token 更偏向词法级的单个结构单元,不能独立表示完整对象或数组。两者都处理 JSON 语法,不等同于把数据解码成 Go 结构体。

因此,“保留原始表示”至少有三层含义:

  • 保留当前值的原始字节,而不是立刻变成 Go 值;
  • 保留副本的生命周期,不受 Decoder 后续读取影响;
  • 在格式化时保留字符串转义或数字写法,而不是主动规范化。
jsontext.Value 从 Decoder 缓冲区到 Clone 独立副本的静态结构说明图
图1:结构说明图,展示 Decoder、ReadValue、Value、Clone 与下游保存边界;这是静态说明图,不是运行截图。

ReadValue 读取的是完整 JSON 值

Decoder 可以把输入看成连续的顶层 JSON 值。调用 ReadValue 后,读取位置会向前推进;返回值会去掉首尾空白,但值内部的原始字节仍被保留。读取完成后要区分正常结束和语法错误,不能把所有错误都当成没有数据。

package main

import (
	"errors"
	"fmt"
	"io"
	"strings"

	"encoding/json/jsontext"
)

func readValues(input string) ([]jsontext.Value, error) {
	dec := jsontext.NewDecoder(strings.NewReader(input))
	var saved []jsontext.Value

	for {
		value, err := dec.ReadValue()
		if errors.Is(err, io.EOF) {
			break // 只有没有更多顶层值时才结束读取。
		}
		if err != nil {
			return nil, fmt.Errorf("读取 JSON 值失败: %w", err) // 保留语法错误上下文。
		}

		saved = append(saved, value.Clone()) // 立即复制,避免后续读取覆盖内部缓冲区。
	}
	return saved, nil
}

func main() {
	values, err := readValues(`{"id":7,"raw":"\\u5f00"}
 {"id":8,"raw":"plain"}`)
	if err != nil {
		panic(err) // 示例只把错误交给顶层;服务代码应返回或记录错误。
	}
	for _, value := range values {
		fmt.Println(string(value)) // 输出的是每个值的原始 JSON 片段。
	}
}

这里用两个顶层对象演示流式读取。第一个对象中的 \\u5f00 仍是输入里的转义文本;代码把每次读取结果先 Clone,所以保存到切片后仍可安全使用。若只在当前调用栈里立即消费,也可以不复制,但不要把未复制的 Value 放进缓存、队列或异步任务。

Clone 才是跨读取保存的边界

官方文档对 ReadValue 的限制很明确:返回结果只在下一次 Peek、Read 或 Skip 调用前有效,并且 Decoder 仍在使用时不能修改它。这个设计让读取可以复用缓冲区,但也意味着“拿到 Value 就存起来”并不安全。

使用方式很简单:

value, err := dec.ReadValue()
if err != nil {
	return err // 先处理 EOF 或语法错误,再使用 value。
}
stable := value.Clone() // 复制底层字节,stable 不再依赖 dec 的下一次读取。

queue = append(queue, stable) // 现在才适合交给缓存、队列或异步消费者。

不要用 append([]byte(nil), value...) 来替代表达意图不清的临时复制;虽然这种写法也能复制字节,但 Clone 直接表达了“把原始 JSON Value 变成独立副本”的语义。

格式化时怎样保留字符串和数字写法

Value.Format 默认会生成最小 JSON 表示,可能去掉空白并采用更短的字符串编码。它适合希望得到规范排版的场景,但不等价于逐字保留输入。只想调整缩进、又不想改写字符串和数字时,可以使用 Indent,或者给 Format 传入 PreserveRawStrings(true)。

func prettyRaw(input jsontext.Value) (jsontext.Value, error) {
	copy := input.Clone() // 不直接改动调用方持有的原始副本。
	if err := copy.Format(
		jsontext.PreserveRawStrings(true), // 保留字符串中的已有转义序列。
		jsontext.WithIndent("  "),          // 使用两个空格进行多行排版。
	); err != nil {
		return nil, fmt.Errorf("格式化 JSON 失败: %w", err) // 输出前仍处理语法错误。
	}
	return copy, nil
}

需要特别注意:PreserveRawStrings 只影响编码阶段,不能阻止你主动调用规范化选项;它也不能让对象成员顺序或数字精度永远保持不变。若业务必须保留数字的字面写法,应避免对它做数字规范化;若只需要删除空白,可考虑 Compact,它不会重新格式化字符串或数字。

jsontext.Value 的 PreserveRawStrings、Indent、Compact 与 Canonicalize 静态关系说明图
图2:关系说明图,对比 Clone 后的 Value 在保留原始表示、排版压缩与规范化之间的边界;这是静态说明图,不是运行结果截图。

保留原文和修改字段不是一回事

jsontext.Value 是原始 JSON 值,不是带字段索引的可变对象。你可以在 Value 上执行排版、压缩、合法性判断或规范化,但“把 name 改成另一个值”通常意味着重新构造 JSON,或者使用 Token/Encoder 重新写出结构。

如果修改来自用户输入,不要直接用字符串替换拼接 JSON。更稳妥的边界是:先把需要修改的字段解码到明确的 Go 类型,使用 jsontext.String 等构造合法 Token,或通过 Encoder 重新输出;不需要修改的片段仍可以作为 raw Value 写回,从而把结构化修改和原始转发分开。

func replaceWithNewRaw() jsontext.Value {
	// 新 Value 必须是完整且合法的 JSON 值;动态内容应使用构造器或编码器转义。
	return jsontext.Value(`{"id":7,"name":"Lin","price":1e2}`)
}

func keepOriginalNumber(v jsontext.Value) jsontext.Value {
	// 只转发原始副本,不调用 Canonicalize,避免改写 1e2 这类数字表示。
	return v.Clone()
}

落地时按这张表选择 API

目标建议需要留意
读取完整 JSON 值Decoder.ReadValue遇到 io.EOF 才表示没有更多值。
跨读取、异步或缓存保存Value.Clone复制应紧跟读取发生。
调整缩进Indent 或 Format + WithIndent这是重新写出,不是逐字复制。
删除空白Compact仍要区分“无空白”和“原始文本完全不变”。
签名、审计或严格转发保存 Clone 后的原始字节不要调用会规范化数字、字符串或对象顺序的 API。

兼容性与常见疑问

encoding/json/jsontext 随 Go 1.27 标准库提供。项目如果仍使用较旧的 Go 工具链,应先检查实际编译环境;不能因为本机安装了新版本就假设所有构建节点都能导入该包。

ReadValue 会保留输入首尾空格吗?不会,官方语义是去除首尾空白,但值内部的原始字节会保留。

Clone 会把 JSON 解析成结构体吗?不会,它只复制 Value 的字节,不改变数据层级和文本表示。

为什么格式化后转义形式变了?Format 默认追求最小 JSON 表示;需要保留字符串已有转义时,显式传入 PreserveRawStrings(true),并避免其它规范化选项覆盖你的目标。

Value 能直接修改对象字段吗?它不是 map。字段级修改应重新编码结构,或者用 Token 与 Encoder 组成新的合法值;原始片段转发则使用 Clone。

小结

把 ReadValue 看成“借用当前读取窗口”,把 Clone 看成“取得长期副本”,就能避开大多数生命周期问题。随后根据目标选择 Indent、Compact、PreserveRawStrings 或结构化重写;如果目标是签名和审计,最重要的是不要在保存前无意调用规范化 API。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>