11.1 为什么选 Gin?

Go 的 Web 框架很多(Gin、Echo、Fiber、Chi、Beego...),但 Gin 具有如下特点:

  • 最流行:目前GitHub Star 最多,社区最大,遇到问题易找答案
  • 性能优秀:基于 httprouter,速度很快
  • API 简洁:和 Express 几乎一一对应
  • 生态丰富:和 GORM、swag(API 文档)等集成好

Gin 在「中间件机制」和「上下文对象设计」上更像 Koa2,但在「功能齐全程度」上更像 Express。 它是个混合体。比较适合从Nodejs开发者转过来的学习者


11.2 第一个 Gin 应用

安装

go get -u github.com/gin-gonic/gin

Hello World

package main

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

func main() {
    r := gin.Default()              // 创建路由引擎(含 Logger 和 Recovery 中间件)

    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello, Gin!"})   // gin.H 是 map[string]any 的简写
    })

    r.Run(":8080")                  // 启动,默认 8080
}

对比 Express:

// Express
const express = require('express')
const app = express()

app.get('/', (req, res) => {
    res.json({ message: 'Hello, Express!' })
})

app.listen(8080)

几乎一一对应:

  • express() → gin.Default()
  • app.get(path, handler) → r.GET(path, handler)
  • res.json(obj) → c.JSON(200, obj)
  • app.listen(port) → r.Run(":port")

gin.Context 是核心

Gin 的 handler 都接收 *gin.Context(简称 c)。它同时封装了请求和响应:

func handler(c *gin.Context) {
    // 读请求
    c.Query("name")              // 查询参数
    c.Param("id")                // 路径参数
    c.GetHeader("Authorization") // 请求头

    // 写响应
    c.JSON(200, data)
    c.String(200, "plain text")
    c.HTML(200, "tpl", data)
}

注意:这里的 gin.Context 和第 08 章 8.8 节的 context.Context 不是同一个东西,名字撞车了:

  • context.Context(标准库):贯穿调用链的「取消信号 + 超时 + 元数据」容器(详见第 08 章 8.8 节)
  • gin.Context(Gin 自己的类型):封装「当前这次 HTTP 请求」的请求/响应数据 + 一堆便捷方法

两者的关系:Gin Context 内部包含了一个标准库 context.Context(用 c.Request.Context() 获取)。所以一个 c 既能读 HTTP 请求,又能拿到请求的取消/超时信号——这是 Gin 的贴心封装。日常你大部分时间用 gin.Context 的方法(c.JSON、c.Query 等),需要超时控制时再取 c.Request.Context()。

热重载工具

每次我们改完代码都要重新启动才能生效,我们希望代码改完可以直接看到效果,但是 Go 确实没有原生的热重载(毕竟它是编译型语言,改完代码要先编译再运行),但社区有成熟的工具方案,比较主流的有 Air、gowatch、CompileDaemon等,这里采用的是 Air。

# 安装
go install github.com/air-verse/air@latest

# 在项目根目录初始化配置(会生成 .air.toml)
air init

# 启动(监听文件变化,自动重新编译+重启)
air

个人实际使用中,偶有修改后不生效的问题,需要手动重启


11.3 路由与路径参数

路径参数(Gin 的核心优势)

// GET /users/123
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")    // "123"
    c.JSON(200, gin.H{"id": id})
})

标准库做不到的事,Gin 一行搞定。

通配符参数

// 匹配 /files/xxx/yyy/zzz
r.GET("/files/*filepath", func(c *gin.Context) {
    fp := c.Param("filepath")   // "/xxx/yyy/zzz"
})

所有 HTTP 方法

r.GET("/users", listUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)
r.PATCH("/users/:id", patchUser)
r.HEAD("/users", headUsers)
r.OPTIONS("/users", optionsUsers)
r.Any("/ping", allMethods)    // 所有方法

11.4 Context(上下文)对象

查询参数与请求体绑定(请求数据)

就是我们如何从请求对象中获取数据

token := c.GetHeader("Authorization")

Query 参数

// GET /search?keyword=go&page=1&size=10
r.GET("/search", func(c *gin.Context) {
    keyword := c.Query("keyword") // "go"
    size := c.Query("size")
    page := c.DefaultQuery("page", "1") // 默认 "1"

    // 或者用 ok 判断是否存在
    size2, exists := c.GetQuery("size")
    fmt.Println(exists)
    c.JSON(200, gin.H{
        "keyword": keyword,
        "page":    page,
        "size":    size,
        "size2":   size2,
    })
})

URL 路径参数(Path Param)

// 路由: /users/:id
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")  // 字符串类型
})

Form 表单数据(application/x-www-form-urlencoded 或 multipart)

r.POST("/login", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.DefaultPostForm("password", "")
})

文件上传

r.POST("/upload", func(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": "上传失败"})
        return
    }
    c.SaveUploadedFile(file, "./uploads/"+file.Filename)
})

JSON Body 绑定(最常用)

type CreateUserRequest struct {
    Name string `json:"name" binding:"required"`           // required 必填
    Age  int    `json:"age" binding:"required,gte=0,lte=150"` // 0-150
    Email string `json:"email" binding:"required,email"`   // email 格式
}

r.POST("/users", func(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // req.Name、req.Age、req.Email 已经验证过了
    c.JSON(201, gin.H{"id": 1, "name": req.Name})
})

binding 标签自动校验参数,省去一堆 if 检查。这些标签来自 validator 库。上面代码里几个缩写的含义:

标签 全称 含义
required — 必填,不能是零值
gte=N greater than or equal 大于等于 N(如 gte=0 = 不能负数)
lte=N less than or equal 小于等于 N
min=N / max=N — 字符串长度 / 切片元素数 的最小/最大
email — 必须是合法邮箱格式
url — 必须是合法 URL
oneof=a b c — 值必须是 a、b、c 其中之一

所以 binding:"required,gte=0,lte=150" 意思是:必填 + 大于等于 0 + 小于等于 150(年龄合理范围)。多个规则用逗号分隔。

💡 完整规则列表见 validator 文档。常见场景 required/email/oneof/min/max 够用了。

其他绑定方式

c.ShouldBindJSON(&req)    // 绑定 JSON body
c.ShouldBindQuery(&req)   // 绑定查询参数
c.ShouldBindUri(&req)     // 绑定路径参数(需要 tag:`uri:"id"`)
c.ShouldBind(&req)        // 自动根据 Content-Type 选择

对比 Express:Express 需要 body-parser + express-validator 两个库。Gin 内置全部。

ShouldBind 系列 vs MustBind 系列

c.ShouldBindJSON(&req)  // 出错返回 error,自己处理(推荐)
c.BindJSON(&req)        // 出错会自动写入 400 响应并 Abort,不用自己处理但灵活性差

响应处理

如何向请求者响应数据

JSON 响应(最常用)

c.JSON(200, gin.H{
    "code": 0,
    "message": "success",
    "data": user,
})

// gin.H 本质就是 map[string]interface{},等价于:
c.JSON(200, map[string]interface{}{"code": 0})

// 用 struct 更类型安全
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
}
c.JSON(200, Response{Code: 0, Message: "success", Data: user})

对比 Express:res.json({ code: 0, message: 'success', data: user }) —— 写法上的体验几乎一致,区别是 Go 多了一个状态码参数前置。

字符串响应

c.String(200, "Hello, %s", name)

对比 Express:res.send('Hello')

HTML 渲染(使用模板引擎服务端模板)

r.LoadHTMLGlob("templates/*")
c.HTML(200, "index.html", gin.H{"title": "首页"})
// 模板引擎语法可以查询 https://gin-gonic.com/en/docs/rendering/html-rendering/

对比 Express + EJS:res.render('index', { title: '首页' })

设置状态码(单独/组合方式)

c.Status(204)  // 只设置状态码,无 body

// 或者直接在 JSON/String 里带上
c.JSON(http.StatusCreated, gin.H{"id": 1})  // 201
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})  // 404

文件下载/返回

c.File("./static/report.pdf")               // 直接返回文件内容
c.FileAttachment("./report.pdf", "报告.pdf")  // 以附件形式触发浏览器下载

对比 Express:res.sendFile() / res.download()

重定向

c.Redirect(http.StatusMovedPermanently, "/new-path")

对比 Express:res.redirect(301, '/new-path')

设置响应头

c.Header("X-Custom-Header", "value")
c.JSON(200, data)

流式响应 / SSE

c.Stream(func(w io.Writer) bool {
    w.Write([]byte("data\n"))
    return false  // false 表示结束
})

对比 Express 的 res.write() 持续写入。

Abort 系列(中断请求链并响应)

c.AbortWithStatus(403)
c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"})

自定义数据传递

c.Set() 和 c.Get() 是 Gin 框架中 Context(上下文)对象提供的两个核心方法,用于在同一个请求的处理链条中传递自定义数据。

你可以把 *gin.Context 想象成一个随请求一起旅行的“快递箱”。

  • c.Set(key, value):把数据放进去(存键值对)。
  • c.Get(key):把数据拿出来(根据键取值)。
r.GET("/x3", func(c *gin.Context) {
    c.Set("userID", 1000) // 存
    userID, exists := c.Get("userID")
    if !exists {
        fmt.Println("信息不存在")
    }
    fmt.Println(userID)
    c.JSON(200, map[string]any{"Data": "你好,x3"})
})

简单来说,可以这样理解,在 Context(上下文)对象里维护了一个类似 map[string]any 字段,Set 是对这个map设置key和值,Get 是根据 key 取值。

前面的例子我们是在一个处理函数中编写了 c.Set() 和 c.Get() 的使用例子,只是为了演示其用法,现实中不会这样用,他有一个比较典型的场景就是在中间件中传值:

// 中间件里:解析 token,把 userID 存进 context
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 实际上一般是解析token得到的
        userID := 1000
        c.Set("userID", userID)  // 存
        c.Next()
    }
}

然后在路由对应的处理函数中使用:

func GetProfile(c *gin.Context) {
    userID, exists := c.Get("userID")  // 取,exists 表示是否存在这个 key
    if !exists {
        c.JSON(401, gin.H{"error": "未登录"})
        return
    }
    // 用 userID 查数据库...
}

这套机制只在单次请求的生命周期内有效(每个请求都是新的 Context 实例),跟JS的 Koa 框架里 ctx.state.xxx = yyy 是完全一样的用途和心智模型。

两个关键的注意事项(必看!)

(1) 类型断言是必须的

c.Set 存进去的数据类型是 interface{}(空接口,可以存任意类型)。当用 c.Get 取出时,必须断言回原来的类型:

// 存
c.Set("age", 18)       // int 类型

// 取(必须断言)
age := value.(int)     // 正确
// name := value.(string) // 运行时会 panic!因为类型不对

如果你确定某个字段的数据类型,那么也有取特定类型的便捷方法,避免每次都做类型断言:

c.GetString("userID")
c.GetInt("age")
c.GetBool("isAdmin")
// ……

(2) 另一个便捷方法:c.MustGet()

如果你确定这个键一定存在,可以用 c.MustGet(key)。它只返回一个值,如果键不存在会直接 panic(程序崩溃)。

// 适用于必传参数,比如上面例子里的 userID
userID := c.MustGet("userID").(int)

11.5 路由分组(RouterGroup)

把相关路由组织在一起,共享前缀和中间件:

// API v1 分组
v1 := r.Group("/api/v1")
{
    v1.GET("/users", listUsers)
    v1.POST("/users", createUser)

    v1.GET("/posts", listPosts)
    v1.POST("/posts", createPost)
}
// 产生路由:/api/v1/users, /api/v1/posts ...

// 鉴权分组(带中间件)
auth := r.Group("/admin")
auth.Use(AuthMiddleware())    // 这个组的所有路由都要过鉴权
{
    auth.GET("/dashboard", dashboard)
    auth.DELETE("/users/:id", deleteUser)
}

对比 Express:

const v1 = express.Router()
v1.get('/users', listUsers)
app.use('/api/v1', v1)

概念完全一样。


11.6 中间件(Middleware)

中间件是「在 handler 前后执行的函数」,用于日志、鉴权、CORS、限流等。

gin的中间件设计非常像koa,都是洋葱模型,只是实现机制不一样,实际效果一样。洋葱模型长这样:

请求进入 → 中间件A 前 → 中间件B 前 → 路由处理 → 中间件B 后 → 中间件A 后 → 响应返回

内置中间件

r := gin.New()                          // 不含默认中间件
r.Use(gin.Logger())                     // 日志
r.Use(gin.Recovery())                   // panic 恢复
r.Use(gin.BasicAuth(gin.Accounts{...})) // HTTP 基本认证

gin.Default() = gin.New() + Logger() + Recovery(),所以日常直接用 Default。

自定义中间件

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()

        c.Next()    // ← 关键:调用下一个 handler

        latency := time.Since(start)
        fmt.Printf("%s %s %v\n", c.Request.Method, c.Request.URL.Path, latency)
    }
}

r.Use(Logger())

c.Next() 是中间件的核心:调用后续 handler,执行完后回到当前中间件继续(类似 Koa 的 next())。

看一下中间件的执行过程

package main

import (
	"fmt"

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

func Middleware1() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("中间件1: 进入")
		c.Next() // 等价于 Koa 的 await next()
		fmt.Println("中间件1: 返回")
	}
}

func Middleware2() gin.HandlerFunc {
	return func(c *gin.Context) {
		fmt.Println("中间件2: 进入")
		c.Next()
		fmt.Println("中间件2: 返回")
	}
}

func main() {
	r := gin.Default() // 创建路由引擎(含 Logger 和 Recovery 中间件)

	// 注册中间件
	r.Use(Middleware1())
	r.Use(Middleware2())

	r.GET("/x3", func(c *gin.Context) {
		c.JSON(200, map[string]any{"Data": "你好,x3"})
	})

	r.Run(":8080") // 启动,默认 8080
}

接下来访问 http://localhost:8080/x3,可以看到打印输出:

中间件1: 进入
中间件2: 进入
中间件2: 返回
中间件1: 返回

核心注意事项:

  • 执行顺序:中间件严格按照注册的顺序执行。当一个中间件调用 c.Next() 后,控制权会移交给下一个中间件或路由处理函数。
  • 洋葱模型:请求处理像剥洋葱。先执行 c.Next() 前的代码(请求预处理),然后进入内层,最后执行 c.Next() 后的代码(响应后处理)。
  • 中断请求:如果中间件想终止后续处理(如鉴权失败),可以调用 c.Abort() 来中断整个处理链。
  • 传递数据:在中间件中,你可以使用 c.Set("key", value) 存储数据,在后续的处理函数中通过 c.Get("key") 获取。

果中间件想终止后续处理(如鉴权失败),必须调用 c.Abort() 来中断整个处理链。注意:仅仅不调用 c.Next() 是没用的,后续 handler 仍会执行——c.Next() 只决定「中间件能否在 handler 之后继续做事」,不决定「要不要执行 handler」。

注册中间件

全局中间件

作用于所有路由,适合日志记录、错误恢复等通用逻辑,使用 router.Use() 方法注册。比如:

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "time"
)

// 自定义中间件:记录请求耗时
func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next() // 执行后续处理
        latency := time.Since(start)
        log.Printf("请求 %s 耗时: %v", c.Request.URL.Path, latency)
    }
}

func main() {
    r := gin.Default()

    // 注册全局中间件(学习演示注册中间件用)
    r.Use(LoggerMiddleware())

    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run()
}

路由组中间件

只对特定路由组 下的所有路由生效,使用路由组实例的 Use() 方法注册

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.JSON(401, gin.H{"error": "未授权"})
            c.Abort() // 终止请求
            return
        }
        c.Next()
    }
}

func main() {
    r := gin.Default()

    // 创建一个路由组
    admin := r.Group("/admin")
    // 为这个路由组注册中间件
    admin.Use(AuthMiddleware())

    // 只有 /admin/panel 会经过 AuthMiddleware
    admin.GET("/panel", func(c *gin.Context) {
        c.JSON(200, gin.H{"data": "管理面板"})
    })

    r.Run()
}

单个路由中间件

只作用于单独一个路由,在定义路由时,将中间件作为参数传入 router.GET() 等方法

func RateLimitMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 模拟限流逻辑...
        c.Next()
    }
}

func main() {
    r := gin.Default()

    // 仅为这一个路由应用限流中间件
    r.GET("/limited", RateLimitMiddleware(), func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "ok"})
    })

    r.Run()
}

示例:鉴权中间件实战

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "未提供 token"})
            // c.Abort 阻止后续 handler 执行。
            // AbortWithStatusJSON 内部已调用 Abort,这里 return 是为了退出当前中间件函数
            return  
        }
        // 验证 token(省略)
        userID := validateToken(token)
        c.Set("userID", userID)   // 在 context 里存值,后续 handler 可取
        c.Next()
    }
}

// 后续 handler 取值
r.GET("/profile", AuthMiddleware(), func(c *gin.Context) {
    userID := c.MustGet("userID").(int)
    c.JSON(200, gin.H{"userID": userID})
})

对比 Express:

// Express
const authMiddleware = (req, res, next) => {
    const token = req.headers.authorization
    if (!token) return res.status(401).json({ error: '...' })
    req.userID = 123
    next()
}

11.7 统一响应格式(生产实践)

实际项目通常统一响应格式:

{
    "code": 0,
    "message": "success",
    "data": { ... }
}

封装一个辅助函数:

type Response struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    any    `json:"data"`
}

func Success(c *gin.Context, data any) {
    c.JSON(200, Response{Code: 0, Message: "success", Data: data})
}

func Error(c *gin.Context, code int, msg string) {
    c.JSON(200, Response{Code: code, Message: msg, Data: nil})
}

// 使用
r.GET("/users", func(c *gin.Context) {
    Success(c, users)
})

11.8 Node.js ↔ Go 对照表

Gin 在上下文对象和中间件机制上更像 Koa2(单 context + 洋葱模型),在功能齐全度上更像 Express。下表把三者放一起对照,方便你从 Koa2/Express 迁移心智模型:

概念 Express Koa2 Gin (Go) 备注
创建应用 express() new Koa() gin.Default()
上下文对象 req + res(双对象) ctx(单对象) c *gin.Context(单对象) Gin 像 Koa2
路由 app.get/post 需 koa-router 插件 r.GET/POST Koa 不内置路由
路径参数 req.params.id ctx.params.id c.Param("id")
查询参数 req.query.x ctx.query.x c.Query("x")
请求体 req.body(需中间件) ctx.request.body(需 bodyparser) c.ShouldBindJSON Gin 内置绑定
请求头 req.headers ctx.headers c.GetHeader
返回 JSON res.json() ctx.body = {...} c.JSON(200, obj)
状态码 res.status(404) ctx.status = 404 c.Status(404)
路由分组 express.Router() router.prefix() r.Group()
中间件注册 app.use(mw) app.use(mw) r.Use(mw)
调用下一个 next() await next() c.Next()
中间件模型 线性回调(后置逻辑不可靠) 洋葱模型(await next()) 洋葱模型(c.Next() 同步阻塞) Gin 像 Koa2
中止请求 res.end() 无(return 即可) c.Abort()
存请求值 req.x = 1 ctx.state.x c.Set("x", 1)
校验 express-validator 需 joi 等插件 binding tag(内置) Gin 内置
异步处理 callback / Promise async/await 同步阻塞(goroutine) Gin 完全不同
CPU 密集任务 阻塞整个进程 阻塞整个进程 不影响其他请求 Gin 优势