我在给配置导出接口增加可读 JSON 时,最先踩到的坑不是编码失败,而是把 prefix 和 indent 当成了同一种空白。json.MarshalIndent 的输入是 Go 值:它先完成 JSON 编码,再把每个对象或数组元素按层级排版。prefix 是嵌套行的固定行首文本,indent 是随层级重复的缩进文本;两者职责不同,参数位置也不能互换。
官方文档地址:https://pkg.go.dev/encoding/json#MarshalIndent
实际使用时,接口响应和配置文件通常把prefix设为空字符串,只用两个空格或一个制表符作为indent;只有日志分段、嵌入片段或需要明显标记行首时,才给prefix加固定文本。格式化只解决可读性,不负责字段排序、HTML 转义策略或修复非法数据。
prefix:每个嵌套结构行前附加的固定文本,不按深度增长。indent:每深入一层就重复一次的缩进文本。- 返回值是新的
[]byte;编码失败时必须先处理 error,再使用结果。
先把两个格式化参数放回各自的职责
MarshalIndent(v, prefix, indent) 与 Marshal(v) 一样遍历 Go 值,只是额外调用缩进逻辑。把 prefix 想成“每一行统一贴上的标签”,把 indent 想成“表示树深度的标尺”,就不容易写反。最外层结果不会因为 prefix 非空就自动在第一行前面多出一段前缀,前缀主要出现在嵌套元素的行首。
用小对象观察嵌套层级如何展开
下面的代码选择空前缀和两个空格缩进,适合配置文件、调试日志或人工阅读的接口响应。示例只表达 API 的输出关系,不把代码块当成本机执行截图。
package main
import (
"encoding/json"
"fmt"
)
type Service struct {
Name string `json:"name"`
Ports []int `json:"ports"`
Labels []string `json:"labels"`
}
func main() {
value := Service{Name: "api", Ports: []int{8080, 8443}, Labels: []string{"internal", "stable"}}
// 空 prefix 让结果适合直接保存为 JSON 文件或返回接口。
data, err := json.MarshalIndent(value, "", " ")
if err != nil {
// 编码错误不能被空字节结果掩盖,调用方应保留错误上下文。
panic(err)
}
// indent 会随着对象和数组深度重复,帮助读者看出数据树结构。
fmt.Println(string(data))
}
这里第二个参数是 "",第三个参数是 " "。如果把两个参数调换,输出仍可能是合法 JSON,但视觉层级会变成固定文本重复、缩进不明显的样子。生产代码中不建议为了“看起来有层次”把长日志标签放进 indent,这会让输出体积随嵌套深度增长。
按场景选择 prefix,别把日志标记混进接口协议
如果输出要被另一个 JSON 解析器消费,推荐保持 prefix 为空;任何行首装饰都可能让整体文本不再是一个可直接解析的 JSON 文档。日志里需要区分多个片段时,可以把格式化结果作为日志字段的值,或在 JSON 外层统一加日志上下文,而不是把 | 、时间戳这类字符塞进结构内容。
配置导出同样优先使用短缩进。两个空格通常比制表符更容易跨编辑器保持一致;大对象则要权衡可读性和传输体积,接口响应可以使用 Marshal 输出紧凑 JSON,把 MarshalIndent 留给调试、导出和人工审查。
编码错误和相邻 API 的边界要单独处理
MarshalIndent 不能编码循环引用,某些值也可能触发 UnsupportedTypeError 或 UnsupportedValueError。不能只判断返回字节是否为空;应先检查 error,再把结果交给文件、HTTP 或日志层。若输出来自已有 JSON 字节,而不是 Go 值,应考虑 json.Indent;它负责重新排版输入文本,不负责把 Go 结构体转换成 JSON。
另一个常见误区是把缩进和 HTML 转义混为一谈。需要调整 <、>、& 的编码策略时,应使用 json.Encoder.SetEscapeHTML,不要通过修改 prefix 或 indent 解决。字段顺序也由输入类型和编码规则决定,不能把缩进输出当成排序 API。
| 目标 | 优先 API | 关键判断 |
|---|---|---|
| Go 值生成可读 JSON | json.MarshalIndent | 先编码,再按层级排版 |
| 已有 JSON 字节重新排版 | json.Indent | 输入必须先能被解析 |
| 控制 HTML 字符转义 | json.Encoder.SetEscapeHTML | 这是编码策略,不是缩进参数 |
Go json.MarshalIndent 参数常见问题
prefix 和 indent 哪个表示层级?
indent 表示层级,每深入一层就重复一次;prefix 是固定的行首文本,不承担深度计算。
接口返回 JSON 时可以设置 prefix 吗?
通常不建议。接口正文应保持一个可直接解析的 JSON 文档,固定行首字符更适合外部日志或人工查看场景。
MarshalIndent 能保证 map 的业务字段顺序吗?
不能把它当作业务排序工具。需要稳定输出时,应选用结构体或在编码前建立明确的数据模型,并单独验证字段顺序需求。
结语
把 prefix 看作行首标签、把 indent 看作树深度标尺,MarshalIndent 的参数就会清楚很多。接口优先紧凑合法,配置和调试优先可读;输入、转义、字段顺序和错误处理则交给对应 API,各自负责自己的边界。