响应渲染 render(render/ 包)

render 包负责把 Go 对象序列化到 HTTP 响应 。和 binding 对称,这里也是接口 + 多实现的设计。


1.1 Render 接口

源码位置 :render/render.go:9-15

scss 复制代码
// Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
type Render interface {
    // Render writes data with custom ContentType.
    Render(http.ResponseWriter) error
    // WriteContentType writes custom ContentType.
    WriteContentType(w http.ResponseWriter)
}

只有两个方法:

  • Render(w):把数据写到 w
  • WriteContentType(w):写 Content-Type 头

1.1.1 内置实现

源码位置 :render/render.go:17-35

ini 复制代码
var (
    _ Render     = (*JSON)(nil)
    _ Render     = (*IndentedJSON)(nil)
    _ Render     = (*SecureJSON)(nil)
    _ Render     = (*JsonpJSON)(nil)
    _ Render     = (*XML)(nil)
    _ Render     = (*String)(nil)
    _ Render     = (*Redirect)(nil)
    _ Render     = (*Data)(nil)
    _ Render     = (*HTML)(nil)
    _ HTMLRender = (*HTMLDebug)(nil)
    _ HTMLRender = (*HTMLProduction)(nil)
    _ Render     = (*YAML)(nil)
    _ Render     = (*Reader)(nil)
    _ Render     = (*AsciiJSON)(nil)
    _ Render     = (*ProtoBuf)(nil)
    _ Render     = (*TOML)(nil)
    _ Render     = (*PDF)(nil)
)

编译期断言 :这些类型都实现了 Render 接口。如果你新增类型忘了实现,编译就过不去。

1.1.2 writeContentType 辅助

css 复制代码
// render/render.go:37-42
func writeContentType(w http.ResponseWriter, value []string) {
    header := w.Header()
    if val := header["Content-Type"]; len(val) == 0 {
        header["Content-Type"] = value
    }
}

关键判断 :如果用户已经手动设置了 Content-Type,就不覆盖。


1.2 Context 与 Render 的桥梁

源码位置 :context.go:1201-1216

scss 复制代码
// Render writes the response headers and calls render.Render to render data.
func (c *Context) Render(code int, r render.Render) {
    c.Status(code)                                       // ① 设置状态码

    if !bodyAllowedForStatus(code) {
        // ② 对于 204/304 等不允许 body 的状态码,只写 header
        r.WriteContentType(c.Writer)
        c.Writer.WriteHeaderNow()
        return
    }

    if err := r.Render(c.Writer); err != nil {           // ③ 真正渲染
        _ = c.Error(err)                                  // ④ 渲染失败,收集错误
        c.Abort()
    }
}

统一入口 :所有 c.JSON / c.XML / c.HTML / ... 都通过 c.Render(code, renderImpl) 实现。

1.2.1 各种 Render 方法

源码位置 :context.go:1255-1289(节选)

scss 复制代码
func (c *Context) JSON(code int, obj any) {
    c.Render(code, render.JSON{Data: obj})
}

func (c *Context) IndentedJSON(code int, obj any) {
    c.Render(code, render.IndentedJSON{Data: obj})
}

func (c *Context) SecureJSON(code int, obj any) {
    c.Render(code, render.SecureJSON{
        Prefix: c.engine.secureJSONPrefix,
        Data:   obj,
    })
}

func (c *Context) PureJSON(code int, obj any) {
    c.Render(code, render.PureJSON{Data: obj})
}

func (c *Context) XML(code int, obj any)  { c.Render(code, render.XML{Data: obj}) }
func (c *Context) YAML(code int, obj any) { c.Render(code, render.YAML{Data: obj}) }
func (c *Context) TOML(code int, obj any) { c.Render(code, render.TOML{Data: obj}) }

每个 API 都是一行包装,核心在具体 Render 实现里。


1.3 JSON 渲染详解

源码位置 :render/json.go

1.3.1 JSON(默认,HTML 转义)

go 复制代码
type JSON struct {
    Data any
}

func (r JSON) Render(w http.ResponseWriter) error {
    return WriteJSON(w, r.Data)
}

func WriteJSON(w http.ResponseWriter, obj any) error {
    writeContentType(w, jsonContentType)
    jsonBytes, err := json.API.Marshal(obj)   // ★ 用 codec/json 抽象
    if err != nil {
        return err
    }
    _, err = w.Write(jsonBytes)
    return err
}

1.3.2 PureJSON(不转义)

go 复制代码
type PureJSON struct {
    Data any
}

func (r PureJSON) Render(w http.ResponseWriter) error {
    r.WriteContentType(w)
    encoder := json.API.NewEncoder(w)
    encoder.SetEscapeHTML(false)              // ★ 关键:关闭 HTML 转义
    return encoder.Encode(r.Data)
}
区别 JSON PureJSON
< < <
> > >
& & &
用途 防止 XSS 注入到 HTML 返回原始 JSON

1.3.3 IndentedJSON(缩进)

css 复制代码
jsonBytes, err := json.API.MarshalIndent(r.Data, "", "    ")

是多了缩进。性能差,仅供调试

1.3.4 SecureJSON(防 JSON 劫持)

go 复制代码
type SecureJSON struct {
    Prefix string
    Data   any
}

func (r SecureJSON) Render(w http.ResponseWriter) error {
    r.WriteContentType(w)
    jsonBytes, _ := json.API.Marshal(r.Data)
    // 如果是数组,前面加 prefix(默认 "while(1);")
    if bytes.HasPrefix(jsonBytes, []byte("[")) && bytes.HasSuffix(jsonBytes, []byte("]")) {
        w.Write([]byte(r.Prefix))
    }
    _, err := w.Write(jsonBytes)
    return err
}

为什么这么做 ?防止 <script src="/api/data"> 这种JSON 劫持 攻击------

浏览器解析 while(1);[...] 会死循环,无法被恶意页面窃取数据。

1.3.5 AsciiJSON(非 ASCII 转义)

go 复制代码
for _, r := range bytesconv.BytesToString(ret) {
    if r > unicode.MaxASCII {
        escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r)
        buffer.Write(escapeBuf)
    } else {
        buffer.WriteByte(byte(r))
    }
}

把中文字符等转成 \uXXXX,适合老式客户端。

1.3.6 JsonpJSON(跨域回调)

scss 复制代码
func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
    r.WriteContentType(w)
    ret, err := json.API.Marshal(r.Data)
    if err != nil { return err }

    if r.Callback == "" {
        _, err = w.Write(ret)
        return err
    }

    callback := template.JSEscapeString(r.Callback)
    w.Write([]byte(callback))
    w.Write([]byte("("))
    w.Write(ret)
    w.Write([]byte(");"))
    return nil
}

输出:cb({"id":1,...});

💡 Context 上的 JSONP 会自动从 query 取 callback 参数,没有就退化成普通 JSON。


1.4 高性能 JSON:codec 抽象

源码位置 :codec/json/json.go

scss 复制代码
// 简化示意
type API interface {
    Marshal(v any) ([]byte, error)
    Unmarshal(data []byte, v any) error
    NewEncoder(w io.Writer) Encoder
    NewDecoder(r io.Reader) Decoder
    // ...
}

Gin 通过这个抽象层,根据平台选择最佳 JSON 库:

平台 默认实现
amd64 / arm64 bytedance/sonic(JIT 加速)
其他(如 386) encoding/json(标准库)

📌这就是为什么 Gin 在 benchmark 里 JSON 性能领先------它自动用上了最优实现。


1.5 HTML 渲染

源码位置 :render/html.go

1.5.1 两层接口

go 复制代码
// HTMLRender:工厂接口
type HTMLRender interface {
    Instance(name string, data any) Render
}

// HTMLProduction:生产环境(预解析模板)
type HTMLProduction struct {
    Template *template.Template
    Delims   Delims
}

// HTMLDebug:开发环境(每次请求都重新加载)
type HTMLDebug struct {
    Files      []string
    Glob       string
    FileSystem http.FileSystem
    Patterns   []string
    Delims     Delims
    FuncMap    template.FuncMap
}

// HTML:具体渲染实例
type HTML struct {
    Template *template.Template
    Name     string
    Data     any
}

1.5.2 Context 中的 HTML 方法

源码位置 :context.go:1221-1224

css 复制代码
func (c *Context) HTML(code int, name string, obj any) {    instance := c.engine.HTMLRender.Instance(name, obj)    c.Render(code, instance)}

c.engine.HTMLRenderLoadHTMLGlob / LoadHTMLFiles 时被设置:

  • 生产 :HTMLProduction,启动时一次解析,后续复用
  • 开发 (debug 模式):HTMLDebug,每次请求都重新加载模板(便于改模板即时生效)

1.5.3 HTML.Render

scss 复制代码
func (r HTML) Render(w http.ResponseWriter) error {
    r.WriteContentType(w)
    return r.Template.ExecuteTemplate(w, r.Name, r.Data)
}

直接复用标准库 html/template


1.6 Reader / Data / String

1.6.1 Reader(流式响应)

源码位置 :render/reader.go

go 复制代码
type Reader struct {
    ContentType   string
    ContentLength int64
    Reader        io.Reader
    Headers       map[string]string
}

func (r Reader) Render(w http.ResponseWriter) (err error) {
    r.WriteContentType(w)
    if r.ContentLength >= 0 {
        if r.Headers == nil {
            r.Headers = map[string]string{}
        }
        r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
    }
    r.writeHeaders(w)
    _, err = io.Copy(w, r.Reader)
    return
}

适用:大文件、动态生成的内容、转发其他 Reader。

Context 上的对应方法:

go 复制代码
func (c *Context) DataFromReader(code int, contentLength int64, contentType string,
    reader io.Reader, extraHeaders map[string]string) {
    c.Render(code, render.Reader{
        ContentType:   contentType,
        ContentLength: contentLength,
        Reader:        reader,
        Headers:       extraHeaders,
    })
}

1.6.2 c.Stream

go 复制代码
// context.go:1378
func (c *Context) Stream(step func(w io.Writer) bool) bool {
    w := c.Writer
    clientGone := w.CloseNotify()
    for {
        select {
        case <-clientGone:
            return true
        default:
            keepOpen := step(w)
            w.Flush()                   // ★ 每次循环都 Flush
            if !keepOpen {
                return false
            }
        }
    }
}

💡CloseNotify 监听客户端断开。每步写完都 Flush,

是 SSE(Server-Sent Events)流式推送的关键。

1.6.3 Data / String

go 复制代码
// render/data.go
type Data struct {
    ContentType string
    Data        []byte
}

// render/text.go
type String struct {
    Format string
    Data   []any
}

1.7 Redirect

源码位置 :render/redirect.go

go 复制代码
type Redirect struct {
    Code     int
    Request  *http.Request
    Location string
}

func (r Redirect) Render(w http.ResponseWriter) error {
    if (r.Code < 300 || r.Code > 308) && r.Code != 201 {
        panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))
    }
    http.Redirect(w, r.Request, r.Location, r.Code)
    return nil
}

复用标准库 http.Redirect


1.8 XML / YAML / TOML / ProtoBuf / BSON

它们的结构几乎一样:实现 RenderWriteContentType。区别只在序列化库:

类型
XML encoding/xml
YAML goccy/go-yaml(高性能)
TOML pelletier/go-toml/v2
ProtoBuf google.golang.org/protobuf
MsgPack ugorji/go/codec
BSON mongo-driver/v2

每种都对应一个 MIME 常量(在 binding/binding.go 中)。


1.9 自定义 Render

实现 Render 接口即可。例如 CSV:

go 复制代码
type CSV struct {
    Data []User
}

func (c CSV) WriteContentType(w http.ResponseWriter) {
    w.Header().Set("Content-Type", "text/csv; charset=utf-8")
}

func (c CSV) Render(w http.ResponseWriter) error {
    c.WriteContentType(w)
    ww := csv.NewWriter(w)
    _ = ww.Write([]string{"id", "name"})
    for _, u := range c.Data {
        _ = ww.Write([]string{strconv.Itoa(u.ID), u.Name})
    }
    ww.Flush()
    return nil
}

// 使用
r.GET("/csv", func(c *gin.Context) {
    c.Render(200, CSV{Data: users})
})

1.10 整体流程:一次 c.JSON 调用

css 复制代码
c.JSON(200, gin.H{"msg": "ok"})
  │
  ↓ context.go:1255
c.Render(200, render.JSON{Data: gin.H{"msg":"ok"}})
  │
  ↓ context.go:1202
c.Status(200)              ← 设置 writermem.status
  │
  ↓
r.WriteContentType(w)      ← 设置 Content-Type: application/json
  │
  ↓ render/json.go:57
r.Render(w) = WriteJSON(w, data)
  │
  ↓ render/json.go:67
writeContentType(w, ...)   ← 实际写 header
jsonBytes := json.API.Marshal(data)
w.Write(jsonBytes)         ← 写 body
  │
  ↓ response_writer.go:84
w.WriteHeaderNow()         ← 自动写出 status line

1.11 小结

  • Render 接口 + 13 种内置实现(JSON/XML/HTML/YAML/TOML/ProtoBuf/...)
  • ✅ Context 上的所有响应 API 都是 c.Render(code, renderImpl) 的包装
  • ✅ JSON 通过 codec/json 抽象,自动用 sonic 或标准库
  • ✅ HTML 区分 Production / Debug,后者每次重新加载模板
  • ✅ Reader / Stream 支持流式响应,适合大文件和 SSE
  • ✅ 自定义 Render 只需实现接口
相关推荐
lichenyang4531 小时前
实时团队邀请的架构与数据流
前端·后端
Java内核笔记1 小时前
Spring Boot 4 空安全源码剖析:JSpecify 是怎么让全生态 API null-safe 的
spring boot·后端
用户852495071841 小时前
一条点赞,六张表:SQL 数据库设计实战
后端
MetaLite1 小时前
SpringBoot项目Maven-BOM统一版本就不会冲突吗
spring boot·后端·maven
benchmark_cc1 小时前
Claude Code + MCP + QuantDash:打造全自动量化研究流水线的终极指南
人工智能·后端·爬虫·算法·claude·mcp·quantdash
小刘是地理大王1 小时前
Nacos 注册与配置中心实战笔记
后端
Csvn2 小时前
🐍 Day 6: Python 异常处理 — 防御式编程的核心
后端·python
叫我少年2 小时前
Git SSH 配置:从生成密钥到远程连接
git·后端
IT_陈寒2 小时前
Python多进程池的坑:子进程竟然不会退出
前端·人工智能·后端