路由与请求处理
本章覆盖路由注册、HTTP 方法绑定、路径参数、查询字符串、表单处理、文件上传、路由分组和重定向。
HTTP 方法绑定
Gin 为每种 HTTP 方法提供了对应的方法:
| 方法 | 典型 REST 用途 |
|---|---|
r.GET() |
获取资源 |
r.POST() |
创建新资源 |
r.PUT() |
替换现有资源 |
r.PATCH() |
部分更新现有资源 |
r.DELETE() |
删除资源 |
r.HEAD() |
获取响应头(无响应体) |
r.OPTIONS() |
查询支持的通信选项 |
r.Any() |
匹配所有 HTTP 方法 |
func main() {
r := gin.Default()
r.GET("/get-something", func(c *gin.Context) {
c.JSON(200, gin.H{"method": "GET"})
})
r.POST("/submit-something", func(c *gin.Context) {
c.JSON(200, gin.H{"method": "POST"})
})
r.PUT("/update-something", func(c *gin.Context) {
c.JSON(200, gin.H{"method": "PUT"})
})
r.DELETE("/delete-something", func(c *gin.Context) {
c.JSON(200, gin.H{"method": "DELETE"})
})
// 匹配所有方法
r.Any("/any-method", func(c *gin.Context) {
c.JSON(200, gin.H{"method": c.Request.Method})
})
r.Run()
}路径参数
Gin 支持两种动态路径参数:
| 语法 | 匹配规则 | 示例 |
|---|---|---|
:name |
匹配单个路径段(不含 /) |
/user/:id → /user/123 中 id=123 |
*name |
匹配其后所有内容(包含 /) |
/static/*path → /static/css/main.css 中 path=/css/main.css |
func main() {
r := gin.Default()
// 单段参数
r.GET("/user/:id", func(c *gin.Context) {
id := c.Param("id") // 123
c.String(200, "User ID: %s", id)
})
// 通配参数
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath") // /css/main.css (注意带 /)
c.String(200, "File path: %s", filepath)
})
// 多参数
r.GET("/user/:id/post/:postID", func(c *gin.Context) {
id := c.Param("id")
postID := c.Param("postID")
c.JSON(200, gin.H{"user": id, "post": postID})
})
r.Run()
}🚨 陷阱:
*filepath捕获的值包含前缀/。例如/files/css/main.css→filepath = "/css/main.css"。
查询字符串参数
查询字符串即 URL 中 ? 后面的键值对:/search?q=gin&page=1&tags=a&tags=b
| 方法 | 行为 |
|---|---|
c.Query("key") |
获取值,不存在返回空字符串 |
c.DefaultQuery("key", "default") |
获取值,不存在返回默认值 |
c.GetQuery("key") |
获取值 + bool 表示是否存在 |
c.QueryArray("key") |
获取同名多值,返回 []string |
c.QueryMap("key") |
获取 key[subkey]=val 格式,返回 map[string]string |
r.GET("/search", func(c *gin.Context) {
q := c.DefaultQuery("q", "") // 单个值,有默认值
page := c.Query("page") // 单个值
tags := c.QueryArray("tags") // 多值: ?tags=a&tags=b
ids, ok := c.GetQuery("ids") // 带存在性检查
// QueryMap: /search?filters[status]=active&filters[role]=admin
filters := c.QueryMap("filters") // map[status:active role:admin]
c.JSON(200, gin.H{
"q": q,
"page": page,
"tags": tags,
"filters": filters,
})
})💡 最佳实践:对可选参数使用
c.DefaultQuery()提供回退值;对必选参数使用c.GetQuery()检查是否存在。
Multipart/Urlencoded 表单
表单提交的两种标准方式(application/x-www-form-urlencoded 和 multipart/form-data),Gin 都能处理:
| 方法 | 行为 |
|---|---|
c.PostForm("key") |
获取表单值,不存在返回空字符串 |
c.DefaultPostForm("key", "default") |
获取值,不存在返回默认值 |
c.GetPostForm("key") |
获取值 + bool 表示是否存在 |
c.PostFormArray("key") |
获取同名多值 |
c.PostFormMap("key") |
获取 key[subkey]=val 格式 |
r.POST("/form", func(c *gin.Context) {
name := c.DefaultPostForm("name", "anonymous")
age := c.PostForm("age")
hobbies := c.PostFormArray("hobbies")
c.JSON(200, gin.H{
"name": name,
"age": age,
"hobbies": hobbies,
})
})文件上传
单文件上传
r.POST("/upload", func(c *gin.Context) {
// 最大内存限制(超出的写入临时文件)
r.MaxMultipartMemory = 8 << 20 // 8 MiB
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 保存到磁盘 — 务必净化文件名
dst := filepath.Join("./uploads", filepath.Base(file.Filename))
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.String(200, "Uploaded: %s", file.Filename)
})多文件上传
r.POST("/upload-multi", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
files := form.File["files"] // 前端使用相同字段名
for _, file := range files {
dst := filepath.Join("./uploads", filepath.Base(file.Filename))
c.SaveUploadedFile(file, dst)
}
c.String(200, "%d files uploaded", len(files))
})严格限制上传大小
使用 http.MaxBytesReader 限制请求体总大小,防止 DoS 攻击:
const MaxUploadSize = 1 << 20 // 1 MB
r.POST("/upload-safe", func(c *gin.Context) {
// 包装请求体,硬限制大小
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, MaxUploadSize)
if err := c.Request.ParseMultipartForm(MaxUploadSize); err != nil {
if _, ok := err.(*http.MaxBytesError); ok {
c.JSON(413, gin.H{"error": "file too large"})
return
}
c.JSON(400, gin.H{"error": err.Error()})
return
}
file, _, err := c.Request.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "file form required"})
return
}
defer file.Close()
c.JSON(200, gin.H{"message": "upload successful"})
})🚨 陷阱:永远不要信任客户端传来的文件名。始终使用
filepath.Base()清理路径,防止路径穿越攻击。
💡
router.MaxMultipartMemory控制文件在内存中的缓存大小(默认 32 MiB)。http.MaxBytesReader是请求体大小的硬限制——二者职责不同,建议同时使用。
路由分组
路由组让你在共享 URL 前缀下组织路由,支持:
- API 版本管理:
/v1/...、/v2/... - 共享中间件:一组路由共用一个认证/日志中间件
- 嵌套分组:更精细的层级组织
基本分组
func main() {
r := gin.Default()
v1 := r.Group("/v1")
{
v1.POST("/login", loginHandler)
v1.POST("/submit", submitHandler)
}
v2 := r.Group("/v2")
{
v2.POST("/login", loginHandler)
v2.POST("/submit", submitHandler)
}
r.Run()
}分组中间件
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
}
func main() {
r := gin.Default()
// 公开路由:无需认证
public := r.Group("/api")
{
public.GET("/health", healthCheck)
}
// 私有路由:需要认证
private := r.Group("/api")
private.Use(AuthRequired())
{
private.GET("/profile", getProfile)
private.POST("/settings", updateSettings)
}
r.Run()
}嵌套分组
func main() {
r := gin.Default()
api := r.Group("/api")
{
// /api/v1
v1 := api.Group("/v1")
{
users := v1.Group("/users")
users.GET("/", listUsers)
users.GET("/:id", getUser)
posts := v1.Group("/posts")
posts.GET("/", listPosts)
}
// /api/v2
v2 := api.Group("/v2")
v2.GET("/users", listUsersV2)
}
r.Run()
}重定向
HTTP 重定向
Gin 通过 c.Redirect() 支持标准 HTTP 重定向:
| 状态码 | 名称 | 行为 |
|---|---|---|
301 MovedPermanently |
永久重定向 | 浏览器缓存新 URL,搜索引擎更新索引。允许 POST→GET |
302 Found |
临时重定向 | 浏览器不缓存,每次先请求旧地址。允许 POST→GET |
307 TemporaryRedirect |
临时重定向 | 类似 302,但必须保留原始 HTTP 方法 |
308 PermanentRedirect |
永久重定向 | 类似 301,但必须保留原始 HTTP 方法 |
r.GET("/old", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://example.com/new")
})
r.POST("/submit", func(c *gin.Context) {
// 使用 307 保留 POST 方法
c.Redirect(http.StatusTemporaryRedirect, "/result")
})💡 选择指南:SEO 相关用 301;临时跳转用 302 或 307;需要保留 POST 方法时用 307/308。
路由内部重定向
r.GET("/test", func(c *gin.Context) {
c.Request.URL.Path = "/final"
r.HandleContext(c) // 内部转发,无客户端往返
})
r.GET("/final", func(c *gin.Context) {
c.JSON(200, gin.H{"hello": "world"})
})💡 通过
r.HandleContext(c)进行内部重定向不会向客户端发送 3xx。请求在服务器内部重新路由,更快且对客户端透明。
路由冲突处理
Gin 不允许在同一 HTTP 方法下注册相同路径。注册冲突会在启动时触发 panic:
r.GET("/user/:id", handler1)
r.GET("/user/profile", handler2) // panic: conflict with /user/:id🚨 陷阱:
/user/:id和/user/profile之间有冲突,因为:id可以匹配 “profile”。解决方案:调整路由注册顺序或重构路径结构。