Gin响应渲染

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

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

Go 复制代码
type Render interface {
    Render(http.ResponseWriter) error
    WriteContentType(w http.ResponseWriter)
}

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 基本使用

Go 复制代码
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 的本质

Go 复制代码
// gin.go 源码
type H map[string]any

所以以下两种写法等价:

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

1.1.3 JSONP

Go 复制代码
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

Go 复制代码
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>",如:

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

1.3 String

Go 复制代码
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:

Go 复制代码
<!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

Go 复制代码
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 不同目录同名模板

Go 复制代码
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 自定义模板函数

Go 复制代码
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 嵌入二进制(推荐生产用)

Go 复制代码
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 直接返回文件

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

1.5.2 带 Content-Disposition(下载)

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

响应头会包含:

Go 复制代码
Content-Disposition: attachment; filename="monthly-report.pdf"

1.5.3 流式响应(Reader)

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

Go 复制代码
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:

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

1.6 重定向

Go 复制代码
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

Go 复制代码
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

Go 复制代码
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)

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

Go 复制代码
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):

Go 复制代码
const es = new EventSource("/events");
es.onmessage = e => console.log(JSON.parse(e.data));
Go 复制代码
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)
})

参数:

Go 复制代码
c.SetCookie(name, value string, maxAge int,
           path, domain string, secure, httpOnly bool)

|------------|---------------------|
| 参数 | 说明 |
| maxAge | 秒数,0 表示会话级,<0 立即删除 |
| secure | 仅 HTTPS 传输 |
| httpOnly | JS 不可读,防 XSS |


1.11 自定义 Render

实现 render.Render 即可:

Go 复制代码
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.JSONc.PureJSON 的差异
  • ✅ 能用 LoadHTMLGlob + c.HTML 渲染模板
  • ✅ 知道 c.File / c.FileAttachment / c.Stream 的差异
  • ✅ 学会自定义 Render(如 CSV)
相关推荐
invicinble1 小时前
设计网站的底层思路(深刻版本)
前端
三8441 小时前
WordPress REST API 参数校验机制剖析:为什么 author__not_in 无法直接盲注?
服务器·前端·数据库
ITmaster07311 小时前
Vibe Coding 时代:Vue 消失了还是 React 太强?
前端·vue.js·react.js
WebInfra2 小时前
Rspack 2.2 发布:30+ 项性能优化,拥抱 Solid 2.0
前端·javascript·前端框架
额额额对了2 小时前
Linux 进程管理详解:从概念到实战
java·服务器·前端
fatcoder2 小时前
玩转 Redis · Set 篇
前端·redis·后端
爱喝麻油的小哆2 小时前
🐾 Day 5|桌面数字人-接入llm可以对话啦
前端·three.js
我叫黑大帅2 小时前
关于没有对生产者做校验的思考
前端·面试·架构
掘金者阿豪2 小时前
你的公网 IP 是专线还是动态变化的?一文讲透动态 IP 与固定 IP 的那些事
前端·后端