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(上下文)对象
查询参数与请求体绑定(请求数据)
就是我们如何从请求对象中获取数据
Header
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 优势 |
💬 评论 0
还没有评论,快来抢沙发吧~