Gin 框架实战

【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
}

分组的两个好处:

  1. 不用重复写前缀,/api 只写一次
  2. 可以整组加中间件(下一节的主角),比如这组接口都要登录才能访问

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})

新手常踩的三个坑:

  1. 绑定忘写取地址符:c.ShouldBindJSON 要传变量地址,才能把数据填进去
  2. 路径参数是字符串:c.Param("id") 拿到的是 "123",当数字用要先转成整数
  3. 中间件忘了 c.Next():不调用,请求就卡死在中间件里

Gin 的官方文档很全(中文版),遇到问题先查它:https://gin-gonic.com/zh-cn/docs/

下一篇可以讲讲 Gin 怎么连接数据库(GORM),把待办事项真正存起来,想看的话评论区告诉我。


参考资料

相关推荐
techdashen19 小时前
Go Map 详解:键值对实际上是如何存储的
开发语言·后端·golang
ttwuai1 天前
Go 后台初始化 MySQL 脚本失败怎么办?先查 DDL 还是生成配置
开发语言·mysql·golang
名字还没想好☜1 天前
Go 的 TCP 粘包与拆包:用长度前缀协议 + bufio 正确读消息
后端·tcp/ip·golang·go·php
王的宝库1 天前
Go 项目结构:从单文件到标准工程布局
开发语言·后端·golang
花酒锄作田1 天前
Go - Gin中使用sessions
golang
不甘先生2 天前
Go 中 type、方法与指针接收者:从 str_name.Name() 看懂 Go 的类型系统
开发语言·后端·golang
Casbin开源社区2 天前
一个面板管住 29 个 AI 编程 Agent:Casbin Gateway 的配置统一、协议互转、用量统计与权限管控
人工智能·golang·开源·gateway·casbin
名字还没想好☜2 天前
Go 标准库 flag 包实战:参数解析、子命令、自定义 Value 类型与默认值
开发语言·后端·golang·go
zh73142 天前
Go 1.23 → 1.26 升级检查文档
开发语言·chrome·golang