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));
1.10 Cookie 与 SetCookie
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.JSON与c.PureJSON的差异
- ✅ 能用
LoadHTMLGlob+c.HTML渲染模板
- ✅ 知道
c.File/c.FileAttachment/c.Stream的差异
- ✅ 学会自定义 Render(如 CSV)
