Appearance
响应处理与路由分组
本篇是 Gin 系列教程的第四篇。前两篇我们解决了「请求进来」(路由、参数绑定、校验)的问题,本篇聚焦「响应出去」——如何返回 JSON、XML、字符串、文件、HTML,如何操作状态码、响应头、Cookie,以及如何用路由分组组织大型项目的 API。最后我们会综合前三篇知识,写一个完整的 RESTful 用户 CRUD 示例。
一、JSON 响应:c.JSON、gin.H、结构体响应
JSON 是现代 Web API 最主流的响应格式。Gin 提供了多种返回 JSON 的方式。
1. 使用 gin.H
gin.H 是 map[string]interface{} 的别名,写起来最简单,适合字段少或临时构造的场景。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/json1", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "success",
"data": gin.H{"id": 1, "name": "alice"},
})
})
r.Run(":8080")
}2. 使用结构体
当响应字段固定、需要复用、或希望享受类型检查时,用结构体更合适。结构体的 json 标签控制输出字段名。
go
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// UserResp 用户响应结构体
type UserResp struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // omitempty: 空值时省略
CreatedAt time.Time `json:"created_at"`
}
// APIResponse 统一响应结构
type APIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
func main() {
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
user := UserResp{
ID: 1,
Name: "alice",
Email: "alice@example.com",
CreatedAt: time.Now(),
}
c.JSON(http.StatusOK, APIResponse{
Code: 0,
Msg: "success",
Data: user,
})
})
r.Run(":8080")
}访问 /users/1 返回:
json
{
"code": 0,
"msg": "success",
"data": {
"id": 1,
"name": "alice",
"email": "alice@example.com",
"created_at": "2024-01-01T12:00:00.123456789Z"
}
}3. 直接序列化切片
c.JSON 第二个参数可以是任意可序列化的对象,包括切片、map。
go
r.GET("/users", func(c *gin.Context) {
users := []UserResp{
{ID: 1, Name: "alice"},
{ID: 2, Name: "bob"},
}
c.JSON(http.StatusOK, users)
})实际项目建议用统一的
APIResponse结构体包装响应,让所有接口返回格式一致,方便前端处理。gin.H适合快速原型或字段不固定的场景。
二、XML、YAML、ProtoBuf 响应
除 JSON 外,Gin 还内置支持多种序列化格式。
XML 响应
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Person struct {
XMLName struct{} `xml:"person"`
ID int `xml:"id"`
Name string `xml:"name"`
}
func main() {
r := gin.Default()
r.GET("/xml", func(c *gin.Context) {
p := Person{ID: 1, Name: "alice"}
c.XML(http.StatusOK, p)
})
r.Run(":8080")
}访问 /xml 返回:
xml
<person><id>1</id><name>alice</name></person>YAML 响应
go
r.GET("/yaml", func(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{
"id": 1,
"name": "alice",
})
})返回内容类型为 application/yaml; charset=utf-8。
ProtoBuf 响应
用于 gRPC 或高性能二进制场景,需要先定义 .proto 文件并生成 Go 代码,然后:
go
c.ProtoBuf(http.StatusOK, &pb.Person{Id: 1, Name: "alice"})日常 Web 开发 95% 以上用 JSON。XML 多用于对接老系统或 RSS,YAML 偶尔用于配置接口,ProtoBuf 主要在 gRPC 生态里使用。
三、String 响应:c.String
当需要返回纯文本(如健康检查、纯文本消息、CSV)时,用 c.String。
go
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 纯文本
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
// 带格式化的文本
r.GET("/greet", func(c *gin.Context) {
name := c.DefaultQuery("name", "Guest")
c.String(http.StatusOK, "Hello, %s!", name)
})
// 返回 CSV
r.GET("/export.csv", func(c *gin.Context) {
c.Header("Content-Disposition", "attachment; filename=users.csv")
c.Data(http.StatusOK, "text/csv", []byte("id,name\n1,alice\n2,bob\n"))
})
r.GET("/info", func(c *gin.Context) {
c.String(http.StatusOK, fmt.Sprintf("Method: %s\nPath: %s", c.Request.Method, c.Request.URL.Path))
})
r.Run(":8080")
}c.String 与 c.Data 的区别
c.String(code, format, args...):带fmt.Sprintf格式化,Content-Type 为text/plain。c.Data(code, contentType, data):直接写字节流,需要自己指定 Content-Type,更通用(可以返回 CSV、任意二进制)。
四、HTML 模板响应基础:c.HTML
Gin 支持渲染 HTML 模板(基于 Go 标准库 html/template)。完整模板用法会在后续章节展开,这里只做基础介绍。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 1. 加载模板文件(支持 glob 模式)
r.LoadHTMLGlob("templates/*")
// 或加载多个目录
// r.LoadHTMLFiles("templates/index.html", "templates/about.html")
// 2. 设置静态资源目录
r.Static("/assets", "./assets")
// 3. 渲染模板
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"title": "首页",
"user": gin.H{"name": "alice", "age": 25},
"items": []string{"Go", "Gin", "Web"},
})
})
r.Run(":8080")
}templates/index.html 示例:
html
<!DOCTYPE html>
<html>
<head><title>{{ .title }}</title></head>
<body>
<h1>欢迎,{{ .user.name }}({{ .user.age }} 岁)</h1>
<ul>
{{ range .items }}
<li>{{ . }}</li>
{{ end }}
</ul>
</body>
</html>这里仅作入门演示。模板语法、模板继承、自定义函数等高级用法会在后续「模板渲染」章节详细讲解。
五、文件响应:c.File、c.FileAttachment
c.File:直接展示文件
返回一个文件,浏览器会根据类型决定是展示(图片、PDF)还是下载。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 直接返回文件(图片、PDF 等会直接在浏览器展示)
r.GET("/file", func(c *gin.Context) {
c.File("./assets/logo.png")
})
// 静态文件目录(更常用)
// 访问 /static/xxx.png 实际读取 ./assets/xxx.png
r.Static("/static", "./assets")
r.Run(":8080")
}c.FileAttachment:强制下载
c.FileAttachment 会在响应头加上 Content-Disposition: attachment,并指定下载文件名,浏览器会强制下载而非展示。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 强制下载
r.GET("/download", func(c *gin.Context) {
// 第二个参数是下载时显示的文件名
c.FileAttachment("./assets/report.pdf", "月度报告.pdf")
})
// 动态生成 CSV 并下载
r.GET("/export", func(c *gin.Context) {
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="users.csv"`)
c.String(http.StatusOK, "id,name\n1,alice\n2,bob\n")
})
r.Run(":8080")
}Static / StaticFS / StaticFile 的区别
| 方法 | 用途 |
|---|---|
r.Static(relPath, root) | 把磁盘目录映射为 URL 前缀 |
r.StaticFS(relPath, http.FileSystem) | 用自定义 FileSystem(如 embed 内嵌资源) |
r.StaticFile(relPath, filepath) | 只暴露单个文件 |
六、状态码设置:c.Status、c.AbortWithStatus
c.Status:只设置状态码
go
r.POST("/items", func(c *gin.Context) {
// ... 创建逻辑
c.Status(http.StatusNoContent) // 204,无响应体
})常用状态码
| 码 | 含义 | 典型场景 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 成功但无响应体 |
| 400 | Bad Request | 参数错误 |
| 401 | Unauthorized | 未登录 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 500 | Internal Server Error | 服务器内部错误 |
c.AbortWithStatus:设置状态码并中止
Abort 会停止后续中间件/handler 的执行,常用于鉴权失败等场景。
go
r.GET("/secure", func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.JSON(http.StatusOK, gin.H{"data": "secret"})
})组合方法
Gin 还提供了一些常用的组合快捷方法:
go
// 设置状态码 + JSON 响应
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
// 设置状态码 + 字符串
c.AbortWithStatus(http.StatusForbidden)七、响应头操作:c.Header
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/api", func(c *gin.Context) {
// 设置响应头
c.Header("X-Request-Id", "abc-123-456")
c.Header("X-Powered-By", "Gin")
// CORS 跨域响应头
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
c.JSON(http.StatusOK, gin.H{"data": "ok"})
})
// 获取请求头
r.GET("/agent", func(c *gin.Context) {
ua := c.GetHeader("User-Agent")
c.JSON(http.StatusOK, gin.H{"user_agent": ua})
})
r.Run(":8080")
}测试响应头:
bash
curl -i http://localhost:8080/api
# HTTP/1.1 200 OK
# X-Request-Id: abc-123-456
# X-Powered-By: Gin
# ...注意
c.Header是「设置」而非「追加」。若要追加同名头(如多个 Set-Cookie),用c.Writer.Header().Add("Set-Cookie", ...)。
八、Cookie 操作:c.SetCookie、c.Cookie
Cookie 常用于会话、用户偏好等。Gin 封装了 SetCookie 方法。
设置 Cookie
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.POST("/login", func(c *gin.Context) {
// 业务校验省略
// 设置 Cookie
c.SetCookie("token", "abc123", 3600, "/", "localhost", false, true)
// 参数:
// name, value, maxAge(秒), path, domain, secure(仅https), httpOnly(禁JS访问)
c.JSON(http.StatusOK, gin.H{"msg": "登录成功"})
})
r.GET("/profile", func(c *gin.Context) {
// 读取 Cookie
token, err := c.Cookie("token")
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
})
r.POST("/logout", func(c *gin.Context) {
// 删除 Cookie:设置 maxAge 为 -1
c.SetCookie("token", "", -1, "/", "localhost", false, true)
c.JSON(http.StatusOK, gin.H{"msg": "已退出"})
})
r.Run(":8080")
}Cookie 参数详解
| 参数 | 含义 |
|---|---|
name | Cookie 名称 |
value | Cookie 值 |
maxAge | 有效期(秒);<0 立即删除,=0 会话级,>0 持久化 |
path | 生效路径,通常 / |
domain | 生效域名 |
secure | true 表示仅 HTTPS 传输 |
httpOnly | true 表示 JS 无法通过 document.cookie 读取(防 XSS) |
生产环境对鉴权 Cookie,建议
secure=true(HTTPS)+httpOnly=true+ 合理的 SameSite,避免 CSRF/XSS。
九、路由分组:r.Group
当项目有多个模块、多版本 API 时,路由会非常多。r.Group 允许把路由按前缀分组,避免重复书写前缀,也便于统一加中间件。
基础分组
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// /api/v1 前缀分组
v1 := r.Group("/api/v1")
{
v1.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/v1/users"})
})
v1.POST("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/v1/users"})
})
v1.GET("/orders", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/v1/orders"})
})
}
// /api/v2 前缀分组
v2 := r.Group("/api/v2")
{
v2.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/v2/users"})
})
}
r.Run(":8080")
}访问:
bash
curl http://localhost:8080/api/v1/users # {"path":"/api/v1/users"}
curl http://localhost:8080/api/v2/users # {"path":"/api/v2/users"}
{}只是为了视觉上把同一组的路由归拢,不影响作用域,可以不写,但写了更清晰。
十、分组中间件
路由分组最大的价值之一,是可以给整组路由统一加中间件,例如鉴权、日志。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 鉴权中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "Bearer secret" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未授权"})
return
}
c.Set("user_id", 42) // 后续 handler 可取
c.Next()
}
}
func main() {
r := gin.Default()
// 公开接口
r.GET("/api/public", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"msg": "公开接口"})
})
// 受保护接口:加鉴权中间件
auth := r.Group("/api", AuthMiddleware())
{
auth.GET("/profile", func(c *gin.Context) {
uid := c.GetInt("user_id") // 取中间件设置的值
c.JSON(http.StatusOK, gin.H{"user_id": uid})
})
auth.GET("/orders", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"orders": []int{1, 2, 3}})
})
}
r.Run(":8080")
}测试:
bash
# 没带 token,401
curl http://localhost:8080/api/profile
# {"error":"未授权"}
# 带 token,成功
curl -H "Authorization: Bearer secret" http://localhost:8080/api/profile
# {"user_id":42}c.Set(key, value) / c.Get(key) 是中间件与 handler 之间传递数据的标准方式。
十一、嵌套分组
分组可以嵌套,灵活组合多层前缀。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// 顶层分组
api := r.Group("/api")
// api.Use(全局日志中间件) // 可以给 api 加中间件,对所有子分组生效
// 一级嵌套:/api/admin
admin := api.Group("/admin")
admin.Use(func(c *gin.Context) {
// admin 鉴权
if c.GetHeader("X-Role") != "admin" {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
return
}
c.Next()
})
{
admin.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/admin/users"})
})
admin.DELETE("/users/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"deleted": c.Param("id")})
})
}
// 一级嵌套:/api/user
user := api.Group("/user")
{
user.GET("/profile", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"path": "/api/user/profile"})
})
}
r.Run(":8080")
}测试:
bash
# 普通用户接口
curl http://localhost:8080/api/user/profile
# 管理员接口,需要 X-Role 头
curl -H "X-Role: admin" http://localhost:8080/api/admin/users
curl -H "X-Role: admin" -X DELETE http://localhost:8080/api/admin/users/5嵌套分组的中间件会累加:子分组会继承父分组的所有中间件,并加上自己的。
十二、版本化 API 设计示例
结合分组 + 中间件,做一个典型的多版本 API 设计。
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 通用日志中间件
func LogMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 简单记录
// 实际项目可记录到文件/ES
c.Next()
}
}
// v1 版本的用户控制器
type UserV1Controller struct{}
func (uc *UserV1Controller) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"version": "v1",
"users": []gin.H{{"id": 1, "name": "alice"}},
})
}
func (uc *UserV1Controller) Create(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"version": "v1", "msg": "已创建"})
}
// v2 版本的用户控制器(字段更丰富)
type UserV2Controller struct{}
func (uc *UserV2Controller) List(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"version": "v2",
"users": []gin.H{{"id": 1, "name": "alice", "email": "alice@example.com"}},
"total": 1,
})
}
func main() {
r := gin.Default()
r.Use(LogMiddleware())
v1 := r.Group("/api/v1")
{
uc := &UserV1Controller{}
v1.GET("/users", uc.List)
v1.POST("/users", uc.Create)
}
v2 := r.Group("/api/v2")
{
uc := &UserV2Controller{}
v2.GET("/users", uc.List)
}
r.Run(":8080")
}这种结构让 v1、v2 互不干扰,便于渐进式升级:v2 上线后保留 v1 一段时间,待客户端全部迁移后再下线 v1。
十三、完整的 RESTful API 示例(用户 CRUD)
把前三篇学到的「路由、参数绑定、校验、响应、分组、中间件」全部串起来,实现一个用户 CRUD 接口。
设计
- 路由前缀:
/api/v1 - 用内存切片模拟数据库
- 接口:
GET /api/v1/users列表GET /api/v1/users/:id详情POST /api/v1/users创建PUT /api/v1/users/:id更新DELETE /api/v1/users/:id删除
- 请求参数用结构体绑定 + 校验
- 响应统一用
APIResponse
完整代码
go
package main
import (
"errors"
"net/http"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
// ===== 数据模型 =====
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
}
// ===== 请求结构体 =====
type CreateUserReq struct {
Name string `json:"name" binding:"required,min=2,max=20"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,gte=1,lte=150"`
}
type UpdateUserReq struct {
Name string `json:"name" binding:"omitempty,min=2,max=20"`
Email string `json:"email" binding:"omitempty,email"`
Age int `json:"age" binding:"omitempty,gte=1,lte=150"`
}
// ===== 统一响应 =====
type APIResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
// ===== 模拟数据库 =====
type UserStore struct {
mu sync.RWMutex
nextID int
users map[int]*User
}
func NewUserStore() *UserStore {
return &UserStore{
nextID: 1,
users: make(map[int]*User),
}
}
func (s *UserStore) Create(req *CreateUserReq) *User {
s.mu.Lock()
defer s.mu.Unlock()
u := &User{
ID: s.nextID,
Name: req.Name,
Email: req.Email,
Age: req.Age,
CreatedAt: time.Now(),
}
s.users[s.nextID] = u
s.nextID++
return u
}
func (s *UserStore) Get(id int) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
u, ok := s.users[id]
if !ok {
return nil, errors.New("user not found")
}
return u, nil
}
func (s *UserStore) List() []*User {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]*User, 0, len(s.users))
for _, u := range s.users {
list = append(list, u)
}
return list
}
func (s *UserStore) Update(id int, req *UpdateUserReq) (*User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.users[id]
if !ok {
return nil, errors.New("user not found")
}
// 只更新非零值字段(简单处理,实际可借助指针区分空值)
if req.Name != "" {
u.Name = req.Name
}
if req.Email != "" {
u.Email = req.Email
}
if req.Age != 0 {
u.Age = req.Age
}
return u, nil
}
func (s *UserStore) Delete(id int) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
return errors.New("user not found")
}
delete(s.users, id)
return nil
}
// ===== 控制器 =====
type UserController struct {
store *UserStore
}
func NewUserController(store *UserStore) *UserController {
return &UserController{store: store}
}
// 校验错误信息映射
var errMsg = map[string]string{
"Name": "姓名必填,2-20 字符",
"Email": "邮箱格式不正确",
"Age": "年龄需在 1-150 之间",
}
func parseValidateErr(err error) (field, msg string) {
var valErrs validator.ValidationErrors
if errors.As(err, &valErrs) {
for _, fe := range valErrs {
if m, ok := errMsg[fe.Field()]; ok {
return fe.Field(), m
}
}
}
return "", "参数错误"
}
func (uc *UserController) List(c *gin.Context) {
c.JSON(http.StatusOK, APIResponse{
Code: 0,
Msg: "success",
Data: uc.store.List(),
})
}
func (uc *UserController) Get(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, APIResponse{Code: 10001, Msg: "id 必须为数字"})
return
}
u, err := uc.store.Get(id)
if err != nil {
c.JSON(http.StatusNotFound, APIResponse{Code: 10002, Msg: "用户不存在"})
return
}
c.JSON(http.StatusOK, APIResponse{Code: 0, Msg: "success", Data: u})
}
func (uc *UserController) Create(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
_, msg := parseValidateErr(err)
c.JSON(http.StatusBadRequest, APIResponse{Code: 10001, Msg: msg})
return
}
u := uc.store.Create(&req)
c.JSON(http.StatusCreated, APIResponse{Code: 0, Msg: "创建成功", Data: u})
}
func (uc *UserController) Update(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, APIResponse{Code: 10001, Msg: "id 必须为数字"})
return
}
var req UpdateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
_, msg := parseValidateErr(err)
c.JSON(http.StatusBadRequest, APIResponse{Code: 10001, Msg: msg})
return
}
u, err := uc.store.Update(id, &req)
if err != nil {
c.JSON(http.StatusNotFound, APIResponse{Code: 10002, Msg: "用户不存在"})
return
}
c.JSON(http.StatusOK, APIResponse{Code: 0, Msg: "更新成功", Data: u})
}
func (uc *UserController) Delete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, APIResponse{Code: 10001, Msg: "id 必须为数字"})
return
}
if err := uc.store.Delete(id); err != nil {
c.JSON(http.StatusNotFound, APIResponse{Code: 10002, Msg: "用户不存在"})
return
}
c.JSON(http.StatusOK, APIResponse{Code: 0, Msg: "删除成功"})
}
// ===== 入口 =====
func main() {
r := gin.Default()
// 初始化存储与控制器
store := NewUserStore()
uc := NewUserController(store)
// 版本化 API 分组
v1 := r.Group("/api/v1")
{
v1.GET("/users", uc.List)
v1.GET("/users/:id", uc.Get)
v1.POST("/users", uc.Create)
v1.PUT("/users/:id", uc.Update)
v1.DELETE("/users/:id", uc.Delete)
}
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, APIResponse{Code: 0, Msg: "ok"})
})
r.Run(":8080")
}测试流程
bash
# 1. 创建用户
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"alice","email":"alice@example.com","age":25}'
# 返回创建的用户(id=1)
# 2. 再创建一个
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"bob","email":"bob@example.com","age":30}'
# 3. 列表
curl http://localhost:8080/api/v1/users
# 4. 详情
curl http://localhost:8080/api/v1/users/1
# 5. 更新
curl -X PUT http://localhost:8080/api/v1/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"alice2"}'
# 6. 删除
curl -X DELETE http://localhost:8080/api/v1/users/2
# 7. 校验失败示例:邮箱格式错
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name":"x","email":"bad","age":25}'
# {"code":10001,"msg":"邮箱格式不正确"}这段代码体现了哪些最佳实践
- 分层结构:Model / Req / Response / Store / Controller 各司其职,便于扩展。
- 并发安全:
UserStore用sync.RWMutex保护 map,避免并发写冲突。 - 统一响应:所有接口都返回
APIResponse,前端处理逻辑统一。 - 校验集中:参数校验交给
binding标签,错误信息统一映射。 - RESTful 语义:方法与操作对齐,状态码用得规范(201 创建、404 不存在、400 参数错)。
- 版本化分组:
/api/v1前缀为未来 v2 留出空间。
真实项目里,
UserStore会被替换成数据库访问层(如 GORM),UserController会拆到internal/controller目录,路由注册会放到internal/router。但骨架与本示例一致。
十四、小结
本篇把 Gin 的「响应」和「组织」两侧能力补齐,并完成了一个完整 CRUD 示例:
- JSON 响应:
c.JSON+gin.H或结构体;推荐用统一APIResponse结构体。 - 多格式响应:XML / YAML / ProtoBuf / String / Data,按需选用。
- HTML 模板:
LoadHTMLGlob+c.HTML,配合Static提供静态资源。 - 文件响应:
c.File(展示)/c.FileAttachment(下载)/c.Static(目录)。 - 状态码:
c.Status、c.AbortWithStatus、c.AbortWithStatusJSON,搭配合理的 HTTP 状态码。 - 响应头:
c.Header设置;同名追加用c.Writer.Header().Add。 - Cookie:
c.SetCookie/c.Cookie,注意 secure、httpOnly 等安全选项。 - 路由分组:
r.Group减少前缀重复,配合Use加中间件。 - 嵌套分组:父分组中间件会传递给子分组。
- 版本化 API:通过
/api/v1、/api/v2分组实现平滑升级。 - 综合实战:用户 CRUD 串联了参数绑定、校验、响应、分组、并发安全,可作为项目脚手架。
至此,Gin 入门篇的四篇教程就完成了。你已经具备用 Gin 独立开发一个中小型 Web API 服务的能力。后续进阶篇我们会讲解中间件原理与自定义、错误处理、模板渲染、JWT 鉴权、文件上传、SSE/WebSocket、与数据库集成、性能优化与部署等内容。
上一篇:03-请求处理:参数绑定与验证