Skip to content

Go Gin 框架 ​

基于 Gin 的 HTTP Web 服务开发指南,覆盖路由、绑定校验、中间件、JWT 与生产化。


一、Gin 概览与选型 ​

1.1 Gin 是什么 ​

Gin 是 Go 最流行的 Web 框架(GitHub 80k+ star),基于标准库 net/http 封装,核心卖点:

  • 路由树:基于 radix tree(压缩前缀树)的路由匹配,性能远超标准库的线性匹配。
  • 中间件链:HandlerFunc 链式模型,与 Java Servlet Filter / Python WSGI 中间件同构。
  • JSON 友好:内置 c.JSON 渲染与绑定,开发 API 服务体验极佳。
  • 依赖少、无反射包袱之外的魔法,源码可读。

1.2 与主流框架对照 ​

维度GinSpring MVCFlask / FastAPI
语言GoJavaPython
路由前缀树Ant 风格Werkzeug / Starlette
中间件HandlerFunc 链Filter / Interceptor / AOPbefore/after、依赖注入
依赖注入手动(函数参数)容器 + 注解Flask 手动 / FastAPI 自动
参数校验validator 标签Jakarta ValidationPydantic
错误处理error 返回 + 中间件@ControllerAdvice异常处理器
启动速度毫秒级秒级秒级

1.3 何时不用 Gin ​

  • 纯 REST 需要 OpenAPI 自动生成 + 强契约:可考虑 go-kratos / Hertz / 标准库。
  • 追求零第三方依赖的小服务:标准库 net/http(Go 1.22+ 的 ServeMux 已支持方法与通配路由)够用。
  • 已深度使用标准库或 grpc-gateway 的团队不必强上 Gin。

二、安装与项目骨架 ​

2.1 初始化项目 ​

bash
mkdir gin-demo && cd gin-demo
go mod init example.com/gin-demo
go get github.com/gin-gonic/gin@latest
mkdir gin-demo && cd gin-demo
go mod init example.com/gin-demo
go get github.com/gin-gonic/gin@latest

2.2 最小可运行服务 ​

go
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default() // Default 自带 Logger + Recovery 中间件
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run(":8080") // 等价 http.ListenAndServe(":8080", r)
}
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default() // Default 自带 Logger + Recovery 中间件
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run(":8080") // 等价 http.ListenAndServe(":8080", r)
}

r.Run 是 http.ListenAndServe(addr, r) 的语法糖,Gin 的 Engine 实现了 http.Handler 接口,可以无缝嵌入标准库 server(优雅关停必须这么用,见第八节)。

2.3 引擎配置要点 ​

go
func newEngine() *gin.Engine {
    gin.SetMode(gin.ReleaseMode) // 生产关掉 Debug 打印;也可用环境变量 GIN_MODE=release

    r := gin.New() // 裸引擎,中间件按需挂
    r.Use(gin.Logger(), gin.Recovery())

    // 生产必须设置可信代理,否则日志中的 ClientIP 可被 X-Forwarded-For 伪造
    // 提示 "you are trusting all proxies" 时按下面处理:
    _ = r.SetTrustedProxies(nil) // 无代理部署,直接取直连 IP

    r.MaxMultipartMemory = 8 << 20 // 上传限制 8MB
    return r
}
func newEngine() *gin.Engine {
    gin.SetMode(gin.ReleaseMode) // 生产关掉 Debug 打印;也可用环境变量 GIN_MODE=release

    r := gin.New() // 裸引擎,中间件按需挂
    r.Use(gin.Logger(), gin.Recovery())

    // 生产必须设置可信代理,否则日志中的 ClientIP 可被 X-Forwarded-For 伪造
    // 提示 "you are trusting all proxies" 时按下面处理:
    _ = r.SetTrustedProxies(nil) // 无代理部署,直接取直连 IP

    r.MaxMultipartMemory = 8 << 20 // 上传限制 8MB
    return r
}

三、路由 ​

3.1 基础路由 ​

go
r.GET("/ping", handler)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.PATCH("/users/:id", patchUser)
r.Any("/callback", anyHandler)     // 任意方法
r.Handle("OPTIONS", "/preflight", h) // 手动注册
r.NoRoute(func(c *gin.Context) {  // 未匹配路由的兜底
    c.JSON(404, gin.H{"code": 404, "msg": "接口不存在"})
})
r.GET("/ping", handler)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.PATCH("/users/:id", patchUser)
r.Any("/callback", anyHandler)     // 任意方法
r.Handle("OPTIONS", "/preflight", h) // 手动注册
r.NoRoute(func(c *gin.Context) {  // 未匹配路由的兜底
    c.JSON(404, gin.H{"code": 404, "msg": "接口不存在"})
})

3.2 路由分组 ​

路由分组 = URL 前缀 + 可独立挂中间件,是"版本化 / 按模块鉴权"的基础:

go
api := r.Group("/api/v1")
api.Use(AuthMiddleware()) // 组内所有路由都走鉴权
{
    api.GET("/users", listUsers)
    api.POST("/users", createUser)
}

admin := r.Group("/admin", AdminAuthMiddleware()) // 第二种写法:分组时挂
{
    admin.GET("/stats", stats)
}

// 子分组
pub := api.Group("/public") // 实际路径 /api/v1/public/xxx
api := r.Group("/api/v1")
api.Use(AuthMiddleware()) // 组内所有路由都走鉴权
{
    api.GET("/users", listUsers)
    api.POST("/users", createUser)
}

admin := r.Group("/admin", AdminAuthMiddleware()) // 第二种写法:分组时挂
{
    admin.GET("/stats", stats)
}

// 子分组
pub := api.Group("/public") // 实际路径 /api/v1/public/xxx

3.3 路径参数与通配 ​

go
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // 精确段参数,匹配 /users/1,不匹配 /users/1/orders
    c.String(200, "id=%s", id)
})

r.GET("/files/*path", func(c *gin.Context) {
    // 通配符 *path 匹配剩余全部路径:/files/a/b/c -> path=/a/b/c
    c.String(200, "path=%s", c.Param("path"))
})
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id") // 精确段参数,匹配 /users/1,不匹配 /users/1/orders
    c.String(200, "id=%s", id)
})

r.GET("/files/*path", func(c *gin.Context) {
    // 通配符 *path 匹配剩余全部路径:/files/a/b/c -> path=/a/b/c
    c.String(200, "path=%s", c.Param("path"))
})

注意 :name 与 *name 不能注册在同一位置产生冲突(Gin 会 panic:conflicts with existing wildcard),这是路由树设计使然。

3.4 静态资源与文件 ​

go
r.Static("/static", "./public")          // 静态目录
r.StaticFile("/favicon.ico", "./favicon.ico")
r.StaticFS("/assets", http.Dir("assets")) // 底层文件系统接口
r.GET("/download", func(c *gin.Context) {
    c.FileAttachment("report.pdf", "报表.pdf") // 下载并指定文件名
})
r.Static("/static", "./public")          // 静态目录
r.StaticFile("/favicon.ico", "./favicon.ico")
r.StaticFS("/assets", http.Dir("assets")) // 底层文件系统接口
r.GET("/download", func(c *gin.Context) {
    c.FileAttachment("report.pdf", "报表.pdf") // 下载并指定文件名
})

四、请求参数绑定与校验 ​

4.1 查询参数、表单与路径混合 ​

go
r.GET("/search", func(c *gin.Context) {
    q := c.Query("q")               // ?q=go
    page := c.DefaultQuery("page", "1") // 带默认值
    c.String(200, "q=%s page=%s", q, page)
})

r.POST("/form", func(c *gin.Context) {
    name := c.PostForm("name")      // 表单
    age, _ := c.GetPostForm("age")  // 双返回值判断是否存在
    _ = age
    c.String(200, "hello %s", name)
})
r.GET("/search", func(c *gin.Context) {
    q := c.Query("q")               // ?q=go
    page := c.DefaultQuery("page", "1") // 带默认值
    c.String(200, "q=%s page=%s", q, page)
})

r.POST("/form", func(c *gin.Context) {
    name := c.PostForm("name")      // 表单
    age, _ := c.GetPostForm("age")  // 双返回值判断是否存在
    _ = age
    c.String(200, "hello %s", name)
})

4.2 绑定 JSON:ShouldBind 系列 ​

go
type CreateUserReq struct {
    Name  string `json:"name"  binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age"   binding:"gte=0,lte=150"`
}

func createUser(c *gin.Context) {
    var req CreateUserReq
    // ShouldBindJSON:出错返回 error,由调用方决定响应(比 MustBind 灵活)
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
        return
    }
    c.JSON(200, gin.H{"code": 0, "data": req})
}
type CreateUserReq struct {
    Name  string `json:"name"  binding:"required"`
    Email string `json:"email" binding:"required,email"`
    Age   int    `json:"age"   binding:"gte=0,lte=150"`
}

func createUser(c *gin.Context) {
    var req CreateUserReq
    // ShouldBindJSON:出错返回 error,由调用方决定响应(比 MustBind 灵活)
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"code": 400, "msg": "参数错误: " + err.Error()})
        return
    }
    c.JSON(200, gin.H{"code": 0, "data": req})
}

ShouldBind(自动按 Content-Type 选择绑定器)与 ShouldBindQuery、ShouldBindUri(路径参数)同理;对应 Must 前缀版本失败时直接 400 中断。

4.3 validator 标签速查 ​

标签含义示例
required必填(零值视为缺失)binding:"required"
omitempty空则跳过后续校验binding:"omitempty,min=1"
min / max数值 / 长度范围binding:"min=6,max=32"
len精确长度binding:"len=11"(手机号)
email / url / ip格式binding:"email"
oneof枚举binding:"oneof=male female"
datetime时间格式binding:"datetime=2006-01-02"
gt / gte / lt / lte大小比较binding:"gt=0"

注意 required 与数字零值的坑:Age int binding:"required" 传 0 会被判缺失,需要"0 合法"的字段用 omitempty + 范围校验。

4.4 自定义校验与错误翻译 ​

go
// 自定义校验器:手机号
var phoneValidator validator.Func = func(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    return len(phone) == 11 && phone[0] == '1'
}

func init() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        _ = v.RegisterValidation("phone", phoneValidator)
    }
}

type Req struct {
    Phone string `json:"phone" binding:"required,phone"`
}
// 自定义校验器:手机号
var phoneValidator validator.Func = func(fl validator.FieldLevel) bool {
    phone := fl.Field().String()
    return len(phone) == 11 && phone[0] == '1'
}

func init() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        _ = v.RegisterValidation("phone", phoneValidator)
    }
}

type Req struct {
    Phone string `json:"phone" binding:"required,phone"`
}

配合 go-playground/validator/v10/translations/zh 可以把英文错误翻译成中文文案,统一错误格式(见第六节封装)。


五、中间件机制 ​

5.1 原理:HandlerFunc 链 ​

中间件本质是 func(c *gin.Context),通过 c.Next() 把控制权交给链上后续处理:

go
func MyMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 前置逻辑(请求处理前)
        c.Next() // 调用链上后续 handler
        // 后置逻辑(响应发出前,可按 c.Writer.Status() 判断结果)
    }
}
func MyMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 前置逻辑(请求处理前)
        c.Next() // 调用链上后续 handler
        // 后置逻辑(响应发出前,可按 c.Writer.Status() 判断结果)
    }
}

等价关系:Gin 中间件 ≈ Servlet Filter ≈ Django middleware,只是没有 Spring AOP 的切面魔法。

5.2 实现一个请求计时日志中间件 ​

go
func Timing() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        dur := time.Since(start)
        if dur > 500*time.Millisecond { // 慢请求单独告警
            log.Printf("[SLOW] %s %s cost=%v", c.Request.Method, c.Request.URL.Path, dur)
        }
    }
}

r := gin.New()
r.Use(Timing(), gin.Logger(), gin.Recovery())
func Timing() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        dur := time.Since(start)
        if dur > 500*time.Millisecond { // 慢请求单独告警
            log.Printf("[SLOW] %s %s cost=%v", c.Request.Method, c.Request.URL.Path, dur)
        }
    }
}

r := gin.New()
r.Use(Timing(), gin.Logger(), gin.Recovery())

5.3 CORS 与常用开源中间件 ​

go
import (
    "github.com/gin-contrib/cors"
    "github.com/gin-contrib/pprof"
)

r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://admin.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true, // 带 Cookie 时 AllowOrigins 不能写 *
    MaxAge:           12 * time.Hour, // 预检结果缓存
}))
import (
    "github.com/gin-contrib/cors"
    "github.com/gin-contrib/pprof"
)

r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://admin.example.com"},
    AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true, // 带 Cookie 时 AllowOrigins 不能写 *
    MaxAge:           12 * time.Hour, // 预检结果缓存
}))

生态中 gin-contrib 官方扩展已覆盖:pprof、cors、sessions、static、gzip、secure(安全响应头)、ratelimit 等。

5.4 中断链路:Abort ​

go
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "未登录"})
            return // Abort 后必须 return,否则还会继续执行本函数剩余逻辑
        }
        c.Set("userId", parseUser(token)) // 向下游传递解析结果
        c.Next()
    }
}
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "未登录"})
            return // Abort 后必须 return,否则还会继续执行本函数剩余逻辑
        }
        c.Set("userId", parseUser(token)) // 向下游传递解析结果
        c.Next()
    }
}

Abort() 只阻止链上后续 handler,已执行的前置代码不会回滚;配合 AbortWithStatusJSON 一步到位。中间件向 handler 传值用 c.Set / c.Get(内部是 map,注意并发安全由框架保证——每个请求一个 Context)。

5.5 限流中间件(令牌桶示例) ​

go
func RateLimit(rate int) gin.HandlerFunc {
    limiter := rate.NewLimiter(rate.Limit(rate), rate) // golang.org/x/time/rate
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.AbortWithStatusJSON(429, gin.H{"code": 429, "msg": "请求过于频繁"})
            return
        }
        c.Next()
    }
}
func RateLimit(rate int) gin.HandlerFunc {
    limiter := rate.NewLimiter(rate.Limit(rate), rate) // golang.org/x/time/rate
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.AbortWithStatusJSON(429, gin.H{"code": 429, "msg": "请求过于频繁"})
            return
        }
        c.Next()
    }
}

单机限流可直接用 x/time/rate;多实例限流需中心化存储(Redis 滑动窗口 / 令牌桶 Lua 脚本),见实践篇。


六、错误处理与响应封装 ​

6.1 统一响应结构 ​

前后端约定一套响应壳,所有接口输出一致:

go
type Response struct {
    Code int    `json:"code"` // 0 成功,非 0 业务错误码
    Msg  string `json:"msg"`
    Data any    `json:"data,omitempty"`
}

func OK(c *gin.Context, data any) {
    c.JSON(200, Response{Code: 0, Msg: "ok", Data: data})
}

func Fail(c *gin.Context, httpStatus, code int, msg string) {
    c.JSON(httpStatus, Response{Code: code, Msg: msg})
}
type Response struct {
    Code int    `json:"code"` // 0 成功,非 0 业务错误码
    Msg  string `json:"msg"`
    Data any    `json:"data,omitempty"`
}

func OK(c *gin.Context, data any) {
    c.JSON(200, Response{Code: 0, Msg: "ok", Data: data})
}

func Fail(c *gin.Context, httpStatus, code int, msg string) {
    c.JSON(httpStatus, Response{Code: code, Msg: msg})
}

统一壳之后前端只需判断 code == 0,HTTP 状态码只表达传输层语义(200/400/401/500)。

6.2 业务错误类型化 ​

go
type BizError struct {
    HTTP int
    Code int
    Msg  string
}

func (e *BizError) Error() string { return e.Msg }

var ErrUserNotFound = &BizError{HTTP: 404, Code: 1001, Msg: "用户不存在"}

func get(id int) (*User, error) {
    u, err := repo.FindByID(id)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrUserNotFound
    }
    return u, nil
}

// handler 内统一翻译
if err != nil {
    var be *BizError
    if errors.As(err, &be) {
        Fail(c, be.HTTP, be.Code, be.Msg)
        return
    }
    Fail(c, 500, 500, "服务器内部错误")
    log.Error(err) // 内部细节只进日志,不进响应
    return
}
type BizError struct {
    HTTP int
    Code int
    Msg  string
}

func (e *BizError) Error() string { return e.Msg }

var ErrUserNotFound = &BizError{HTTP: 404, Code: 1001, Msg: "用户不存在"}

func get(id int) (*User, error) {
    u, err := repo.FindByID(id)
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, ErrUserNotFound
    }
    return u, nil
}

// handler 内统一翻译
if err != nil {
    var be *BizError
    if errors.As(err, &be) {
        Fail(c, be.HTTP, be.Code, be.Msg)
        return
    }
    Fail(c, 500, 500, "服务器内部错误")
    log.Error(err) // 内部细节只进日志,不进响应
    return
}

6.3 handler 薄 + service 厚的分层 ​

Gin handler 应只做三件事:绑定参数、调用 service、翻译响应。业务逻辑全部下沉 service 层(service 不 import gin,保持可测试性),这是下一章 Go Web 项目实践 的核心原则。


七、JWT 鉴权接入 ​

7.1 JWT 结构与选库 ​

JWT = 三段式 Header.Payload.Signature(各段 Base64Url 编码):

  • Header:算法(HS256 / RS256)与类型。
  • Payload:声明(sub、exp、iat 及业务字段如 uid)。
  • Signature:服务端密钥对前两段签名,防篡改。

注意 Payload 只做 Base64Url 编码,不加密——任何拿到 token 的人都能解码看内容,所以绝不放密码等敏感数据。选库推荐 github.com/golang-jwt/jwt/v5(维护活跃,社区事实标准)。

7.2 签发与校验(HS256 对称密钥) ​

go
import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("please-change-me") // 生产从配置/密钥管理读取

type Claims struct {
    UserID int64  `json:"uid"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

// 签发:登录成功后调用
func sign(userID int64, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "gin-demo",
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
        SignedString(jwtSecret)
}

// 校验解析:中间件中调用
func parse(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // 防算法混淆攻击
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return jwtSecret, nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }
    return claims, nil
}
import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("please-change-me") // 生产从配置/密钥管理读取

type Claims struct {
    UserID int64  `json:"uid"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

// 签发:登录成功后调用
func sign(userID int64, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "gin-demo",
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
        SignedString(jwtSecret)
}

// 校验解析:中间件中调用
func parse(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
        if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { // 防算法混淆攻击
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return jwtSecret, nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }
    return claims, nil
}

安全要点:解析时必须校验签名算法(否则攻击者可用 none 或伪造算法);生产建议 RS256(非对称)便于网关/多服务共享公钥验签。

7.3 鉴权中间件组合 ​

go
func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        h := c.GetHeader("Authorization")
        tokenStr, ok := strings.CutPrefix(h, "Bearer ") // 标准格式:Authorization: Bearer xxx
        if !ok || tokenStr == "" {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "缺少 token"})
            return
        }
        claims, err := parse(tokenStr)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "token 无效或已过期"})
            return
        }
        c.Set("uid", claims.UserID) // 后续 handler 取 c.GetInt64("uid")
        c.Set("role", claims.Role)
        c.Next()
    }
}

// 路由接入:需要登录的分组挂上
authed := r.Group("/api/v1")
authed.Use(JWTAuth())
{
    authed.GET("/profile", profile)
}
func JWTAuth() gin.HandlerFunc {
    return func(c *gin.Context) {
        h := c.GetHeader("Authorization")
        tokenStr, ok := strings.CutPrefix(h, "Bearer ") // 标准格式:Authorization: Bearer xxx
        if !ok || tokenStr == "" {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "缺少 token"})
            return
        }
        claims, err := parse(tokenStr)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "token 无效或已过期"})
            return
        }
        c.Set("uid", claims.UserID) // 后续 handler 取 c.GetInt64("uid")
        c.Set("role", claims.Role)
        c.Next()
    }
}

// 路由接入:需要登录的分组挂上
authed := r.Group("/api/v1")
authed.Use(JWTAuth())
{
    authed.GET("/profile", profile)
}

小贴士:需要"踢人下线 / 修改权限立即生效"等场景,JWT 无状态特性不满足,可引入 Redis 黑名单或改用 session;纯 JWT 只适合接受最长 2 小时过期时间的场景。


八、优雅关停与超时 ​

8.1 优雅关停:不再接新请求,等存量处理完 ​

go
func main() {
    r := gin.New()
    // ... 注册路由

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
        ReadTimeout:  10 * time.Second, // 读请求体超时
        WriteTimeout: 15 * time.Second, // 写响应超时
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    // 监听退出信号(Ctrl+C / docker stop 的 SIGTERM)
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    <-ctx.Done()
    log.Println("收到退出信号,开始优雅关停...")

    // 给存量请求最多 10 秒完成,超时强制断开
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Println("强制关停:", err)
    }
    log.Println("服务已退出")
}
func main() {
    r := gin.New()
    // ... 注册路由

    srv := &http.Server{
        Addr:    ":8080",
        Handler: r,
        ReadTimeout:  10 * time.Second, // 读请求体超时
        WriteTimeout: 15 * time.Second, // 写响应超时
    }

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %v", err)
        }
    }()

    // 监听退出信号(Ctrl+C / docker stop 的 SIGTERM)
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    <-ctx.Done()
    log.Println("收到退出信号,开始优雅关停...")

    // 给存量请求最多 10 秒完成,超时强制断开
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Println("强制关停:", err)
    }
    log.Println("服务已退出")
}

必须使用 srv.Shutdown(它会等待活跃连接结束并触发各请求 ctx 取消),而不是 srv.Close(立即断开)。

8.2 三层超时认知 ​

层级配置作用
Server 读写srv.ReadTimeout/WriteTimeout防慢速攻击(Slowloris)
单请求处理context.WithTimeout(ctx, 3s) 包住下游调用防下游卡死拖垮本服务
上游网关Nginx/网关超时兜底

业务代码里每个 DB / Redis / HTTP 调用都要可取消:把 c.Request.Context() 一路传到 repository(GORM 支持 WithContext)。


九、性能与生产注意 ​

9.1 pprof 性能剖析接入 ​

go
import (
    "net/http/pprof" // 标准库自带
)

// 生产建议监听独立内网端口,而不是挂在业务路由上
func startPprof() {
    mux := http.NewServeMux()
    mux.HandleFunc("/debug/pprof/", pprof.Index)
    mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    mux.HandleFunc("/debug/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP)
    go http.ListenAndServe("127.0.0.1:6060", mux) // 只绑本机回环
}

// 采集与查看
// curl http://127.0.0.1:6060/debug/pprof/profile?seconds=30 -o cpu.out
// go tool pprof -http=:8081 cpu.out
// 或用 gin-contrib/pprof 在测试环境便捷接入
import (
    "net/http/pprof" // 标准库自带
)

// 生产建议监听独立内网端口,而不是挂在业务路由上
func startPprof() {
    mux := http.NewServeMux()
    mux.HandleFunc("/debug/pprof/", pprof.Index)
    mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
    mux.HandleFunc("/debug/pprof/goroutine", pprof.Handler("goroutine").ServeHTTP)
    go http.ListenAndServe("127.0.0.1:6060", mux) // 只绑本机回环
}

// 采集与查看
// curl http://127.0.0.1:6060/debug/pprof/profile?seconds=30 -o cpu.out
// go tool pprof -http=:8081 cpu.out
// 或用 gin-contrib/pprof 在测试环境便捷接入

9.2 pprof 端口安全(必读) ​

pprof 暴露了堆对象、goroutine 栈、甚至可触发 profile 的 CPU 采样与 trace,泄露在公网等于交出服务内部情报,还可能被用来打内存/CPU 放大攻击。三条铁律:

  1. 业务公网端口永远不挂 pprof。
  2. 单独端口 + 只绑定 127.0.0.1 或内网网卡,靠 K8s port-forward / 堡垒机访问。
  3. 或挂鉴权中间件(Basic Auth / mTLS)再暴露;线上建议通过指标系统(Prometheus)取数而非直连 pprof。

9.3 其它生产要点 ​

项目做法
运行模式GIN_MODE=release,关闭调试日志
错误日志zap 结构化日志 + 请求 ID 贯穿(见实践篇)
panic 兜底gin.Recovery() 必须有;自定义 Recovery 可告警
JSON 编码需要小写字段 / 不转义 HTML 用 Encoder 选项
内存大响应体避免整块读入,用 io.Copy 流式
静态资源交给 Nginx / CDN,别让 Go 进程扛
压测验收go-wrk / wrk / k6 压测,配合 pprof 找热点
依赖升级关注 gin 与 golang-jwt 的安全公告,及时升 patch

Gin 单机性能(简单 JSON 接口)通常在数万 QPS 量级,业务瓶颈九成在下游 DB / Redis,框架本身很少是短板。调优先 pprof 后动手,不要凭感觉。


框架层到这里结束。下一章 Go Web 项目实践 将把这些能力组装成分层、可测试、可部署的完整工程。