Skip to content
Go
Gin
路由与请求处理

路由与请求处理

本章覆盖路由注册、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/123id=123
*name 匹配其后所有内容(包含 / /static/*path/static/css/main.csspath=/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.cssfilepath = "/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-urlencodedmultipart/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”。解决方案:调整路由注册顺序或重构路径结构。