This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/9-response-rendering.md
T

186 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [后端, Go, Gin, 响应渲染, JSON]
create time: 2026-04-28 00:00
---
# 响应渲染
## 概述
Gin 提供了丰富的响应渲染方法——不仅是基础的 JSON 返回,还包括防劫持的 SecureJSON、保留中文的 PureJSON、ASCII 转换、XML/YAML/ProtoBuf 输出等。理解各渲染方法的差异和适用场景,能写出更健壮、兼容的 API。
思考题:为什么 Gin 要同时提供 `JSON` 和 `PureJSON`?它们什么时候输出相同的结果,什么时候不同?
## 正文
### 1. 标准 JSON 渲染
```go
func handler(c *gin.Context) {
data := gin.H{
"message": "你好",
"items": []string{"a", "b", "c"},
}
// 等价于 c.Render(200, render.JSON{Data: data})
c.JSON(http.StatusOK, data)
}
```
输出:
```json
{"items":["a","b","c"],"message":"你好"}
```
**注意:** 标准 `c.JSON()` 会对非 ASCII 字符做 Unicode 转义(`\uXXXX`),这是 RFC 4627 的要求,但现代浏览器和客户端都无需此限制。
### 2. PureJSON — 保留原始 Unicode
```go
func handler(c *gin.Context) {
c.PureJSON(http.StatusOK, gin.H{
"message": "你好世界",
})
}
```
输出:
```json
{"message":"你好世界"}
```
**`PureJSON` vs `JSON` 对比:**
| 特性 | `c.JSON()` | `c.PureJSON()` |
|------|-----------|----------------|
| 中文编码 | `你好` | `你好` |
| HTML 转义 | `<script>` → `\<script\>` | 不转义 |
| 性能 | 略快(stdlib) | 略慢(gjson) |
| 安全性 | 更安全(防 XSS) | 需注意 XSS |
> **何时选哪个:** 如果是面向浏览器的 SPA 应用,用 `JSON`(防 XSS);如果是移动端 API 或后端服务间调用,用 `PureJSON`(可读性更好、带宽更小)。
### 3. SecureJSON — 防 JSON 劫持
针对老式浏览器的安全保护——在输出前加上 `)]}',\n` 前缀,阻止 JSON 被邪恶的 `<script src=...>` 跨域加载:
```go
func secureHandler(c *gin.Context) {
c.SecureJSON(200, gin.H{"name": "wonder"})
}
```
输出:
```
)];},{"name":"wonder"}
```
**适用场景:**
- API 需要被不受信任的第三方域名引用
- 遗留系统无法升级 CSP 头
- 安全合规要求严格的场景
**不适用场景:**
- 现代 SPA(前后端分离,不用 script 标签加载 JSON)
- 移动端 API
- 需要解析该输出的自动化测试
### 4. AsciiJSON — 中文转 Unicode
与 `PureJSON` 相反,强制将所有非 ASCII 字符转为 Unicode 逃逸序列:
```go
func asciiHandler(c *gin.Context) {
c.AsciiJSON(200, gin.H{
"name": "张三",
"data": []int{1, 2, 3},
})
}
```
输出:
```json
{"data":[1,2,3],"name":"\xe5\xbc\xa0\xe4\xb8\x89"}
```
> **注意:** `AsciiJSON` 使用的是 UTF-8 字节的十六进制转义(`\xe5\xbc\xa0...`),不是 `\uXXXX`。这是 Gin 的独特实现——客户端解码时需要特殊处理,一般不太常用。
### 5. XML / YAML / ProtoBuf 渲染
```go
func multiFormat(c *gin.Context) {
data := gin.H{"title": "Go Guide", "version": "1.0"}
switch c.ContentType() {
case "application/xml":
c.XML(200, data)
case "application/yaml":
c.YAML(200, data)
default:
c.JSON(200, data)
}
}
```
**XML 输出示例:**
```xml
<map><title>Go Guide</title><version>1.0</version></map>
```
**YAML 输出示例:**
```yaml
map:
title: Go Guide
version: "1.0"
```
> **提问:** Gin 的 XML 渲染用的是 `gin.H`(map[string]interface{}),输出的 XML 根节点总是 `<map>`。如果需要自定义 XML 标签,应该怎么改结构体?
答案:用结构体的 `xml` 标签:
```go
type Article struct {
Title string `xml:"title"`
Version string `xml:"version,attr"` // attr 表示属性
}
```
### 6. JSONP — JSON with Padding
通过脚本回调的方式实现跨域 GET 请求。由于涉及 eval 执行,安全风险高,现代项目中已很少使用:
```go
func jsonpHandler(c *gin.Context) {
c.JSONP(http.StatusOK, gin.H{"callback": "getData"})
}
```
浏览器端:
```html
<script>
function getData(data) { console.log(data); }
</script>
<script src="https://api.example.com/data?callback=getData"></script>
```
> **安全警告:** JSONP 要求回调函数名白名单校验,否则攻击者可构造 `callback=<script>alert(1)</script>` 注入 XSS。
### 7. 渲染方法速查
| 方法 | 内容类型 | 特点 |
|------|----------|------|
| `c.JSON(code, obj)` | `application/json` | 标准 JSON,ASCII 转义 |
| `c.PureJSON(code, obj)` | `application/json` | 保留原始 Unicode |
| `c.SecureJSON(code, obj)` | `application/json` | 加前缀防劫持 |
| `c.AsciiJSON(code, obj)` | `application/json` | 强制 ASCII 编码 |
| `c.XML(code, obj)` | `application/xml` | XML 序列化 |
| `c.YAML(code, obj)` | `application/x-yaml` | YAML 序列化 |
| `c.ProtoBuf(code, obj)` | `application/x-protobuf` | Protobuf 序列化 |
| `c.String(code, format, vals)` | `text/plain` | 字符串格式化 |
| `c.Data(code, data)` | 自定义 | 原始字节流 |
| `c.HTML(code, tmplName, obj)` | `text/html` | HTML 模板渲染 |
思考题:如果你的 API 同时需要提供 JSON 和 CSV 两种格式,Gin 本身没有 `c.CSV()`,你应该怎么做?
提示:`c.Data()` 和 `c.Writer.Write()` 的组合。