【Go 入门】Gin 框架实战:路由、中间件和项目架构
上一篇我们学会了用标准库
net/http起一个网站。但真实项目里,大家更常用框架,Go 里最流行的就是 Gin。这篇博客从零讲清 Gin 的核心概念:路由、分组、中间件、参数绑定,最后用一个待办事项 API 演示真实的项目架构怎么搭。
建议先看完系列前两篇(Go 项目结构、Go 常用标准库包)再来看这篇,衔接更顺。
Gin 是什么?
Gin 是基于 Go 标准库 net/http 封装的 Web 框架,帮你把注册路由、取参数、返回 JSON、加中间件这些事变得非常简单。
对比一下同样一个接口:
go
// 标准库写法:要自己判断路径、自己写 JSON
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"message":"pong"}`)
})
// Gin 写法:路由清晰,JSON 一行搞定
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
1. 第一个 Gin 程序
新建 main.go:
go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() // 创建引擎,自带日志和错误恢复
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong"})
})
r.Run(":8080") // 启动服务,监听 8080 端口
}
运行:
bash
go run .
浏览器打开 http://localhost:8080/ping,就能看到:
json
{"message":"pong"}
这 4 行代码里藏着 Gin 的全部核心概念:
| 代码 | 干什么 |
|---|---|
| gin.Default() | 创建引擎,相当于服务器本体 |
| r.GET(路径, 函数) | 注册路由:访问这个路径就调用后面的函数 |
| c 参数 | 上下文:从请求里拿东西、往响应里写东西,全靠它 |
| c.JSON(...) | 返回 JSON 响应 |
| gin.H{...} | 一个什么都能装的字典,用来拼 JSON 很方便 |
2. 路由:三种最常见的取参数方式
2.1 路径参数:/todos/:id
路径里的可变部分用冒号标记:
go
r.GET("/todos/:id", func(c *gin.Context) {
id := c.Param("id") // 拿到的是字符串
c.JSON(200, gin.H{"id": id})
})
访问 http://localhost:8080/todos/123,返回 {"id":"123"}。
注意:c.Param 拿到的一律是字符串。如果后面要当数字用,记得用 strconv.Atoi 转一下(系列第三篇讲过)。
2.2 查询参数:/search?q=xxx
URL 里问号后面的叫查询参数:
go
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // 拿不到就返回空字符串
c.JSON(200, gin.H{"query": q})
})
访问 http://localhost:8080/search?q=gin,返回 {"query":"gin"}。
2.3 测试方法
bash
curl http://localhost:8080/todos/123
curl "http://localhost:8080/search?q=gin"
或者用 Apifox / Postman 这类图形化工具,更适合新手。
3. 分组路由:同前缀的接口归一组
一个真实项目会有很多接口,比如所有 /api 开头的。用分组把它们归到一起,代码更整齐:
go
api := r.Group("/api")
{
api.GET("/todos", ListTodos) // 实际路径:/api/todos
api.POST("/todos", CreateTodo) // 实际路径:/api/todos
api.GET("/todos/:id", GetTodo) // 实际路径:/api/todos/:id
}
分组的两个好处:
- 不用重复写前缀,/api 只写一次
- 可以整组加中间件(下一节的主角),比如这组接口都要登录才能访问
4. 中间件:Gin 架构的灵魂
4.1 中间件是什么?
把中间件想象成安检通道:你的处理函数是候机厅,每个请求都要先过安检(中间件),才能进候机厅。
text
请求 → 中间件1 → 中间件2 → 处理函数 → 响应
中间件的典型用途:
| 用途 | 例子 |
|---|---|
| 打日志 | 记录每个请求的方法、路径、耗时 |
| 登录校验 | 没带 token 的请求直接拦下 |
| 跨域处理 | 给响应加 CORS 头,允许前端访问 |
4.2 写一个日志中间件
go
func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
// 请求进入时先执行
log.Printf("%s %s", c.Request.Method, c.Request.URL.Path)
c.Next() // 关键:放行,让请求继续往后走
// 处理函数跑完后,回到这里
}
}
注册方式:
go
r.Use(RequestLogger()) // 全局:所有请求都过
// 或 api.Use(RequestLogger()) // 只对某组生效
4.3 写一个登录校验中间件(拦截)
go
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("token")
if token != "secret123" {
// 直接拒绝:返回 401,请求到此为止
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "没有权限"})
return
}
c.Next() // 校验通过,放行
}
}
使用:
go
api.Use(Auth()) // 这组接口全部需要 token 才能访问
中间件三句话:写在处理函数前的是请求进来先干的事;c.Next() 是放行口令,不调它请求就断在这;c.AbortWithStatusJSON 是直接拒绝并返回错误。
5. 参数绑定:JSON 自动变成结构体
前端 POST 一段 JSON,我们想直接得到一个结构体,这是 Gin 最方便的功能之一。
go
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
r.POST("/todos", func(c *gin.Context) {
var t Todo
// 把请求体里的 JSON 自动填进结构体
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": t})
})
测试:
bash
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title":"写博客","done":false}'
返回:
json
{"data":{"id":0,"title":"写博客","done":false}}
两个注意点:json:"title" 这种标签控制字段名(系列第三篇讲过);必须传变量地址(用取地址符),忘了传又是老坑。
6. 项目架构:从单文件到分层
前面为了看懂概念,所有代码写在一个 main.go 里。但真实项目要分层,这正是系列第一篇讲的 cmd / internal 结构,现在派上用场了。
推荐的最小分层(新手够用):
text
mytodo/
├── go.mod
├── cmd/
│ └── server/
│ └── main.go # 入口:创建引擎、注册路由和中间件
└── internal/
├── handler/ # 处理层:接收请求、调用逻辑、返回响应
│ └── todo.go
└── model/ # 数据层:定义结构体
└── todo.go
请求的流动方向:
text
请求 → main.go(路由和中间件)→ handler(处理函数)→ model(数据结构)
为什么这么分:
- main.go 只负责接线:哪个路径找哪个函数、过哪些中间件
- handler 只负责接客:拿参数、回响应,不关心数据长什么样
- model 只负责定义:数据结构长什么样,全项目共用
这样分工,项目大了以后,改接口不影响数据,改数据不影响接口。
业务再复杂一点,可以在 handler 和 model 之间加一层 service(业务逻辑层)。新手先掌握三层就够,多了反而乱。
7. 实战:完整的待办事项 API
把上面所有知识串起来,做一个真实的待办事项 API。包含:列表、新增、查单个、日志中间件。
7.1 初始化项目
bash
mkdir mytodo && cd mytodo
go mod init example.com/mytodo
go get github.com/gin-gonic/gin
7.2 数据模型:internal/model/todo.go
go
package model
// Todo 待办事项
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
7.3 处理层:internal/handler/todo.go
go
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"example.com/mytodo/internal/model"
)
// 演示用内存存储,程序重启数据就没了(真实项目换成数据库)
var todos = []model.Todo{}
var nextID = 1
// ListTodos GET /api/todos 待办列表
func ListTodos(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": todos})
}
// CreateTodo POST /api/todos 新增待办
func CreateTodo(c *gin.Context) {
var t model.Todo
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
t.ID = nextID
nextID++
todos = append(todos, t)
c.JSON(http.StatusOK, gin.H{"data": t})
}
// GetTodo GET /api/todos/:id 查单个待办
func GetTodo(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id")) // 字符串转整数
for _, t := range todos {
if t.ID == id {
c.JSON(http.StatusOK, gin.H{"data": t})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "没有这条待办"})
}
7.4 入口:cmd/server/main.go
go
package main
import (
"log"
"github.com/gin-gonic/gin"
"example.com/mytodo/internal/handler"
)
// RequestLogger 日志中间件
func RequestLogger() gin.HandlerFunc {
return func(c *gin.Context) {
log.Printf("%s %s", c.Request.Method, c.Request.URL.Path)
c.Next()
}
}
func main() {
r := gin.Default()
r.Use(RequestLogger()) // 全局日志中间件
api := r.Group("/api") // 分组:所有接口带 /api 前缀
{
api.GET("/todos", handler.ListTodos)
api.POST("/todos", handler.CreateTodo)
api.GET("/todos/:id", handler.GetTodo)
}
log.Println("服务启动:http://localhost:8080")
r.Run(":8080")
}
7.5 运行并测试
bash
go run ./cmd/server
另开一个终端测试三个接口:
bash
# 1. 查列表(初始为空)
curl http://localhost:8080/api/todos
# 返回:{"data":[]}
# 2. 新增一条(注意转义,或用 Apifox/Postman 更方便)
curl -X POST http://localhost:8080/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"写 Gin 博客","done":false}'
# 返回:{"data":{"id":1,"title":"写 Gin 博客","done":false}}
# 3. 再查列表,能看到刚才新增的那条
curl http://localhost:8080/api/todos
同时服务端终端会打印中间件日志:
text
[GIN] 2026/09/07 - 20:00:00 | 200 | GET /api/todos
[GIN] 2026/09/07 - 20:00:01 | 200 | POST /api/todos
到此为止,一个结构规范、可运行的 Gin API 项目就完成了。和系列第一篇的项目结构完全对上了:入口在 cmd,业务代码在 internal,只是从工具库变成了 Web 分层。
8. 总结:一张表记住 Gin
| 我想做 | 用这个 | 例子 |
|---|---|---|
| 注册路由 | r.GET 或 r.POST 等 | r.GET("/ping", 函数) |
| 取路径参数 | c.Param(名字) | c.Param("id") |
| 取查询参数 | c.Query(名字) | c.Query("q") |
| 分组管理 | r.Group("/api") | 同前缀接口归组 |
| 加中间件 | r.Use(函数) | 日志、登录校验 |
| JSON 转结构体 | c.ShouldBindJSON(变量地址) | POST 请求体自动填充 |
| 返回 JSON | c.JSON(状态码, gin.H{...}) | c.JSON(200, gin.H{"ok": true}) |
新手常踩的三个坑:
- 绑定忘写取地址符:c.ShouldBindJSON 要传变量地址,才能把数据填进去
- 路径参数是字符串:c.Param("id") 拿到的是 "123",当数字用要先转成整数
- 中间件忘了 c.Next():不调用,请求就卡死在中间件里
Gin 的官方文档很全(中文版),遇到问题先查它:https://gin-gonic.com/zh-cn/docs/
下一篇可以讲讲 Gin 怎么连接数据库(GORM),把待办事项真正存起来,想看的话评论区告诉我。
参考资料
- Gin 官方文档(中文):https://gin-gonic.com/zh-cn/docs/
- Gin GitHub 仓库:https://github.com/gin-gonic/gin