Gin响应渲染

51 阅读4分钟

Gin 内置 13+ 种响应渲染,统一通过 Render 接口实现。

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

type Render interface {
    Render(http.ResponseWriter) error
    WriteContentType(w http.ResponseWriter)
}

deepseek_mermaid_20260819_977b2a.png

1.1 JSON 系列

API用途
c.JSON(code, obj)标准 JSON,HTML 字符会被转义(&→&)
c.PureJSON(code, obj)不转义,适合返回 HTML 字符串
c.IndentedJSON(code, obj)带缩进(便于调试,性能略差)
c.SecureJSON(code, obj)加 while(1); 前缀,防 JSON 劫持
c.AsciiJSON(code, obj)非 ASCII 字符转 \uXXXX
c.JsonpJSON(code, obj)JSONP(配合 ?callback=xxx)

1.1.1 基本使用

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

r.GET("/u", func(c *gin.Context) {
    c.JSON(200, User{ID: 1, Name: "Alice"})
})
// {"id":1,"name":"Alice"}

1.1.2 gin.H 的本质

// gin.go 源码
type H map[string]any

所以以下两种写法等价:

c.JSON(200, gin.H{"id": 1, "name": "Alice"})
c.JSON(200, map[string]any{"id": 1, "name": "Alice"})

1.1.3 JSONP

r.GET("/jsonp", func(c *gin.Context) {
    data := gin.H{"msg": "hello"}
    callback := c.Query("callback")
    if callback == "" {
        c.JSON(200, data)
        return
    }
    c.JSONP(200, data) // 自动用 callback 包裹
})
// 访问 /jsonp?callback=cb → cb({"msg":"hello"});

1.1.4 高性能 JSON

源码位置:go.mod 引入 github.com/bytedance/sonic

Gin 在支持的平台(amd64 / arm64)上自动使用 sonic,
性能比标准库 encoding/json 高数倍。无需手动开启。


1.2 XML / YAML / TOML

r.GET("/xml", func(c *gin.Context) {
    c.XML(200, gin.H{"user": gin.H{"id": 1, "name": "Alice"}})
})

r.GET("/yaml", func(c *gin.Context) {
    c.YAML(200, gin.H{"id": 1, "name": "Alice"})
})

r.GET("/toml", func(c *gin.Context) {
    c.TOML(200, gin.H{"id": 1, "name": "Alice"})
})

XML struct 标签:xml:"<tag>",如:

type User struct {
    XMLName xml.Name `xml:"user"`
    ID      int      `xml:"id,attr"`
    Name    string   `xml:"name"`
}

1.3 String

r.GET("/", func(c *gin.Context) {
    c.String(200, "hello %s, you are %d", "alice", 18)
})

1.4 HTML 模板

1.4.1 准备模板文件

templates/index.tmpl:

<!doctype html>
<html>
<head><title>{{.title}}</title></head>

<body>
  <h1>{{.title}}</h1>

  <p>User: {{.user.name}} ({{.user.id}})</p>

  <ul>
    {{range .items}}
      <li>{{.}}</li>

    {{end}}
  </ul>

</body>

</html>

1.4.2 加载与渲染

源码位置:gin.go:270-309

r := gin.Default()
r.LoadHTMLGlob("templates/*")
// 或多个目录
// r.LoadHTMLGlob("templates/*/*")
// 或指定文件
// r.LoadHTMLFiles("templates/index.tmpl", "templates/about.tmpl")

r.GET("/", func(c *gin.Context) {
    c.HTML(200, "index.tmpl", gin.H{
        "title": "首页",
        "user":  gin.H{"id": 1, "name": "Alice"},
        "items": []string{"Apple", "Banana", "Cherry"},
    })
})

1.4.3 不同目录同名模板

r.LoadHTMLGlob("templates/**/*")
// templates/blog/index.tmpl
// templates/admin/index.tmpl

// 需要改名注册
r.GET("/blog", func(c *gin.Context) {
    c.HTML(200, "blog/index.tmpl", ...)
})

1.4.4 自定义模板函数

r.SetFuncMap(template.FuncMap{
    "upper": strings.ToUpper,
    "ts":    func(t time.Time) string { return t.Format("2006-01-02 15:04:05") },
})
r.LoadHTMLGlob("templates/*")

模板内使用:{{ .name | upper }}、{{ .now | ts }}。

⚠️SetFuncMap 必须在 LoadHTMLGlob之前调用,否则不会生效。

1.4.5 嵌入二进制(推荐生产用)

import "embed"

//go:embed templates/*
var tmplFS embed.FS

func main() {
    r := gin.Default()
    tmpl, _ := template.ParseFS(tmplFS, "templates/*.tmpl")
    r.SetHTMLTemplate(tmpl)
    // ...
}

1.5 文件下载

1.5.1 直接返回文件

r.GET("/file", func(c *gin.Context) {
    c.File("./public/report.pdf")
})

1.5.2 带 Content-Disposition(下载)

r.GET("/download", func(c *gin.Context) {
    c.FileAttachment("./public/report.pdf", "monthly-report.pdf")
})

响应头会包含:

Content-Disposition: attachment; filename="monthly-report.pdf"

1.5.3 流式响应(Reader)

适合大文件、动态生成内容:

r.GET("/stream", func(c *gin.Context) {
    pr, pw := io.Pipe()
    go func() {
        defer pw.Close()
        for i := 0; i < 10; i++ {
            fmt.Fprintf(pw, "line %d\n", i)
            time.Sleep(200 * time.Millisecond)
        }
    }()
    c.Stream(func(w io.Writer) bool {
        _, err := io.Copy(w, pr)
        return err == nil
    })
})

或使用 c.Render:

c.Render(200, render.Reader{
    ContentType: "application/pdf",
    Reader:      someReader,
    Headers:     map[string]string{"Content-Disposition": `attachment; filename="x.pdf"`},
})

1.6 重定向

r.GET("/old", func(c *gin.Context) {
    c.Redirect(http.StatusMovedPermanently, "/new")
})

// 外部重定向
r.GET("/ext", func(c *gin.Context) {
    c.Redirect(http.StatusFound, "https://example.com")
})

// 路由命名重定向(Gin 没有命名路由概念,但可以拼)
r.GET("/new", newHandler)
r.GET("/shortcut", func(c *gin.Context) {
    c.Request.URL.Path = "/new"
    r.HandleContext(c) // 内部转发
})

1.7 二进制 / Data

r.GET("/bin", func(c *gin.Context) {
    c.Data(200, "application/octet-stream", []byte{0x00, 0x01, 0x02})
})

r.GET("/img", func(c *gin.Context) {
    b, _ := os.ReadFile("./a.png")
    c.Data(200, "image/png", b)
})

1.8 ProtoBuf / MsgPack / BSON

import (
    "github.com/golang/protobuf/proto"
)

r.GET("/pb", func(c *gin.Context) {
    msg := &mypb.User{Id: 1, Name: "Alice"}
    c.ProtoBuf(200, msg)
})

r.GET("/msgpack", func(c *gin.Context) {
    c.MsgPack(200, gin.H{"id": 1, "name": "Alice"})
})

1.9 Server-Sent Events(SSE)

适合向浏览器单向推送数据(聊天、通知、行情)。

r.GET("/events", func(c *gin.Context) {
    c.Header("Content-Type", "text/event-stream")
    c.Header("Cache-Control", "no-cache")
    c.Header("Connection", "keep-alive")

    c.Stream(func(w io.Writer) bool {
        if msg, ok := <-someChan; ok {
            c.SSEvent("message", msg)
            return true
        }
        return false
    })
})

客户端(浏览器 JS):

const es = new EventSource("/events");
es.onmessage = e => console.log(JSON.parse(e.data));

1.10 Cookie 与 SetCookie

r.GET("/set", func(c *gin.Context) {
    c.SetSameSite(http.SameSiteLaxMode)
    c.SetCookie("token", "abc123", 3600, "/", "example.com", true, true)
    c.String(200, "cookie set")
})

r.GET("/get", func(c *gin.Context) {
    token, err := c.Cookie("token")
    fmt.Println(token, err)
})

参数:

c.SetCookie(name, value string, maxAge int,
           path, domain string, secure, httpOnly bool)
参数说明
maxAge秒数,0 表示会话级,<0 立即删除
secure仅 HTTPS 传输
httpOnlyJS 不可读,防 XSS

1.11 自定义 Render

实现 render.Render 即可:

type CSV struct {
    Data []User
}

func (c CSV) ContentType() string { return "text/csv; charset=utf-8" }
// 实际接口只要求 Render 和 WriteContentType
func (c CSV) WriteContentType(w http.ResponseWriter) {
    w.Header().Set("Content-Type", c.ContentType())
}
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: []User{{1, "Alice"}, {2, "Bob"}}})
})

1.12 状态码速查

码含义常见场景
200 OK成功GET / PUT / PATCH / DELETE
201 Created已创建POST 创建资源
204 No Content成功无内容DELETE 成功
301 / 302重定向资源迁移
304 Not Modified资源未变配合 ETag / If-None-Match
400 Bad Request客户端参数错误校验失败
401 Unauthorized未登录缺失 / 失效 token
403 Forbidden无权限已登录但无权限
404 Not Found资源不存在
409 Conflict冲突唯一约束冲突
422 Unprocessable Entity语义错误字段格式正确但语义不通
429 Too Many Requests限流限流中间件触发
500 Internal Server Error服务端错误兜底
502 / 503 / 504网关错误反向代理 / 限流

1.13 小结

  • ✅ 熟练使用 JSON / XML / YAML / TOML / String
  • ✅ 知道 c.JSON 与 c.PureJSON 的差异
  • ✅ 能用 LoadHTMLGlob + c.HTML 渲染模板
  • ✅ 知道 c.File / c.FileAttachment / c.Stream 的差异
  • ✅ 学会自定义 Render(如 CSV)