c.json() 返回非标准json是因go的encoding/json默认对、&做unicode转义(如\u003c),属html安全策略,非json语法错误;新版gin已默认关闭该转义,若仍出现需检查是否手动启用setescapehtml(true)或响应头缺失charset=utf-8。

为什么 c.JSON() 返回的不是标准 JSON?
因为默认启用了 HTML 转义,比如 " 会变成 <code>"\u003c","&" 变成 "\u0026"——这确实是合法 JSON,但不符合多数 API 客户端预期的“可读标准 JSON”。这不是 bug,是 Gin 为防止 XSS 主动做的安全策略。
常见现象:返回的 JSON 字符串里一堆 \uXXXX,前端解析没问题,但日志、调试、Postman 查看时很别扭;或者对接某些严格校验 Unicode 转义的旧系统失败。
- 仅对字符串字段中的特殊字符转义,数字、布尔、null 不受影响
- 只影响
c.JSON()和c.Render()(使用json.Marshal的场景) - 不影响
c.Data()或手动序列化后调用c.Writer.Write()
如何让 c.JSON() 输出不转义 HTML 字符
直接禁用转义即可,Gin v1.9+ 提供了开关:
c.JSON(200, gin.H{
"msg": "用户 & 角色 <admin>",
})</admin>
改成:
c.Header("Content-Type", "application/json; charset=utf-8")
encoder := json.NewEncoder(c.Writer)
encoder.SetEscapeHTML(false) // 关键
_ = encoder.Encode(gin.H{
"msg": "用户 & 角色 <admin>",
})</admin>
注意:SetEscapeHTML(false) 必须在 Encode() 前调用;且不能复用同一个 json.Encoder 实例跨请求(因为 c.Writer 是 per-request 的)。
用于端到端视频本地化流程的轻量编排器,路由至四个专注子技能——/wjs-transcribing-audio、/wjs-translating-subtitles...
- 不要用全局复用的
json.Encoder,每次请求新建 - 如果用了自定义中间件或封装响应函数,确保
SetEscapeHTML(false)在 encode 前执行 - 仍需手动设
Content-Type,c.JSON()会自动设,但手动 encoder 不会
全局关闭 HTML 转义是否安全?
不推荐全局关——除非你 100% 确保所有写入 JSON 的字符串都来自可信源(如数据库字段已过滤、内部计算结果)。真实场景中,用户输入、第三方 API 数据、模板渲染片段都可能含恶意 HTML 片段。
更稳妥的做法是按需关闭:
- 对纯数据接口(如配置下发、枚举列表),用
SetEscapeHTML(false) - 对含用户生成内容的字段(如评论、昵称),保留转义,或提前做 HTML 清洗再塞进结构体
- 避免在结构体字段上用
json:",string"标签绕过转义——这会让数字变成字符串,破坏类型语义
和 json.Marshal() 直接对比有什么差异?
Gin 的 c.JSON() 底层就是调 json.Marshal(),但多包了一层:json.Marshal() 默认不转义 HTML,而 Gin 封装后强制开启 SetEscapeHTML(true)。
所以如果你看到:
b, _ := json.Marshal(gin.H{"x": "<script>"})
// 输出:{"x":"\u003cscript\u003e"}</script>
那是 Gin 自己加的逻辑,不是 Go 标准库行为。标准 json.Marshal() 默认输出 {"x":"<script>"}</script>。
- 想完全控制序列化行为,就绕过
c.JSON(),用json.Encoder+SetEscapeHTML() - 不要依赖
json.MarshalIndent()做生产响应——它会增加 CPU 开销,且 Gin 不自动处理缩进 - 若需兼容老客户端要求换行缩进,应明确用
json.Indent()并缓存结果,而非每次请求重算
\u003c 这类转义做了二次解码,导致乱码——这时得查中间链路是否把 JSON 当 HTML 处理了。










