4. 路由参数:路径参数(:param)、通配符参数(*param)、查询字符串参数(Query)的获取与使用
在 Gin 路由设计中,参数传递是客户端与服务器交互的核心手段。Gin 提供了三种参数绑定方式:路径参数(Path Parameters)、通配符参数(Wildcard Parameters)和查询字符串参数(Query Parameters)。理解它们的获取方式、作用域和适用场景,是构建清晰 API 边界的基础。
4.1 三种参数类型对比
| 参数类型 | 路由定义示例 | URL 示例 | 获取方式 | 适用场景 |
|---|---|---|---|---|
路径参数 :param |
/users/:id |
/users/42 |
c.Param("id") |
资源标识、实体 ID |
通配符参数 *param |
/files/*path |
/files/a/b/c.txt |
c.Param("path") |
路径匹配、静态文件服务 |
查询字符串 ?key=value |
/search?q=go |
/search?q=go&page=1 |
c.Query("q") |
过滤、分页、排序参数 |
4.2 路径参数(:param)—— 精确匹配资源
路径参数使用冒号 : 前缀定义,用于匹配 URL 路径中的单个片段。它不会匹配斜杠 /,因此适合定位单一资源。
核心规则:路径参数只匹配非空片段,且不包含斜杠。多个路径参数可以连续定义,如
/users/:uid/posts/:pid。
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
// 单个路径参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"user_id": id,
"type": "path_param",
})
})
// 多个路径参数
r.GET("/users/:uid/posts/:pid", func(c *gin.Context) {
uid := c.Param("uid")
pid := c.Param("pid")
c.JSON(http.StatusOK, gin.H{
"user_id": uid,
"post_id": pid,
})
})
r.Run(":8080")
}
最佳实践:路径参数应使用在 RESTful 资源路径中,如
/api/v1/products/:productId。避免在路径参数中包含特殊字符,如需传递编码后的值,应使用 URL 编码。
4.3 通配符参数(*param)—— 匹配剩余路径
通配符参数使用星号 * 前缀定义,它会匹配从当前位置到路径末尾的所有片段,包括斜杠。通配符参数必须定义在路由末尾,且一个路由中只能有一个通配符参数。
// 通配符参数示例
r.GET("/files/*filepath", func(c *gin.Context) {
filepath := c.Param("filepath")
// 请求 /files/notes/chapter1.txt → filepath = "/notes/chapter1.txt"
// 请求 /files/ → filepath = "/"
c.String(http.StatusOK, "Requested file: %s", filepath)
})
// 通配符参数与路径参数组合
r.GET("/projects/:project/assets/*assetPath", func(c *gin.Context) {
project := c.Param("project")
assetPath := c.Param("assetPath")
c.JSON(http.StatusOK, gin.H{
"project": project,
"asset_path": assetPath,
})
})
注意:通配符参数会包含前导斜杠。例如
/files/notes/readme.md 匹配后,c.Param("filepath") 返回 /notes/readme.md(包含开头的 /)。处理文件路径时需注意拼接根目录。
4.4 查询字符串参数(Query)—— 过滤与分页
查询字符串参数通过 URL 中 ? 后的 key=value 对传递。Gin 提供了多种获取方式,支持默认值和数组参数。
// 基础查询参数获取
r.GET("/search", func(c *gin.Context) {
// 获取单个参数,不存在返回空字符串
query := c.Query("q")
// 获取参数并设置默认值
page := c.DefaultQuery("page", "1")
size := c.DefaultQuery("size", "20")
// 获取数组参数:?tags=go&tags=gin&tags=api
tags := c.QueryArray("tags")
// 获取 Map 参数:?filter[name]=john&filter[age]=30
filter := c.QueryMap("filter")
c.JSON(http.StatusOK, gin.H{
"query": query,
"page": page,
"size": size,
"tags": tags,
"filter": filter,
})
})
| 方法 | 签名 | 说明 |
|---|---|---|
Query |
c.Query(key string) string |
获取参数值,不存在返回空字符串 |
DefaultQuery |
c.DefaultQuery(key, defaultValue string) string |
获取参数值,不存在返回默认值 |
GetQuery |
c.GetQuery(key string) (string, bool) |
获取参数值并判断是否存在 |
QueryArray |
c.QueryArray(key string) []string |
获取重复 key 的数组值 |
QueryMap |
c.QueryMap(key string) map[string]string |
获取嵌套 Map 参数 |
4.5 参数获取流程与边界
4.6 参数验证与类型转换
获取到的参数默认都是字符串类型,在实际业务中通常需要转换为目标类型并进行验证。
r.GET("/products/:id", func(c *gin.Context) {
// 1. 获取路径参数
idStr := c.Param("id")
// 2. 字符串转整数
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid product id",
})
return
}
// 3. 获取查询参数并转换
pageStr := c.DefaultQuery("page", "1")
page, _ := strconv.Atoi(pageStr)
// 4. 参数范围验证
if page < 1 {
page = 1
}
c.JSON(http.StatusOK, gin.H{
"product_id": id,
"page": page,
})
})
类型转换建议:使用
strconv 包进行基础类型转换。对于复杂验证逻辑,推荐使用 go-playground/validator 结合结构体绑定(将在后续章节详细讲解)。
4.7 参数优先级与冲突处理
当路径参数和查询参数同名时,Gin 不会自动覆盖或报错,需要开发者自行处理优先级逻辑。
r.GET("/items/:id", func(c *gin.Context) {
// 路径参数优先
id := c.Param("id")
// 查询参数作为可选覆盖
if queryID := c.Query("id"); queryID != "" {
// 业务逻辑:是否允许查询参数覆盖路径参数
id = queryID
}
c.JSON(http.StatusOK, gin.H{
"final_id": id,
})
})
| 场景 | 路由定义 | 请求 URL | 获取结果 | 建议 |
|---|---|---|---|---|
| 路径参数唯一 | /users/:id |
/users/100 |
id = "100" |
标准 RESTful 设计 |
| 查询参数补充 | /users/:id |
/users/100?fields=name,email |
id = "100", fields = "name,email" |
路径参数标识资源,查询参数控制返回 |
| 参数同名冲突 | /items/:id |
/items/100?id=200 |
c.Param("id") = "100", c.Query("id") = "200" |
明确优先级规则,避免混淆 |
4.8 路由参数与中间件作用域
路由参数在中间件中同样可访问,这为参数级别的权限校验提供了基础。
// 中间件中获取路径参数
func UserAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.Param("uid")
if userID == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "user id required",
})
return
}
// 将解析后的用户信息存入上下文
c.Set("current_user_id", userID)
c.Next()
}
}
r.GET("/users/:uid/profile", UserAuthMiddleware(), func(c *gin.Context) {
uid := c.GetString("current_user_id")
c.JSON(http.StatusOK, gin.H{
"user_id": uid,
"profile": "user profile data",
})
})
边界注意:通配符参数
*param 在中间件中获取时,如果路由尚未完全匹配,可能返回空字符串。确保在路由处理函数或其后执行的中间件中获取通配符参数。
4.9 参数获取的常见陷阱
- 空字符串与不存在:
c.Query("key")在参数不存在时返回空字符串,无法区分“参数值为空”和“参数不存在”。使用c.GetQuery("key")可判断是否存在。 - 路径参数编码:路径参数中的特殊字符(如
/、?)会被 URL 编码,Gin 自动解码。如需传递原始斜杠,应使用通配符参数。 - 参数类型安全:所有参数均为字符串,类型转换失败时应返回 400 错误,而非 panic。
- 路由冲突:
/users/:id和/users/search会冲突,因为:id会匹配search。静态路由应定义在参数路由之前。
4.10 完整示例:综合参数处理
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)
func main() {
r := gin.Default()
// 综合示例:文章列表 API
r.GET("/api/v1/articles/:category/*subpath", func(c *gin.Context) {
// 路径参数
category := c.Param("category")
// 通配符参数
subpath := c.Param("subpath")
// 查询参数
keyword := c.Query("keyword")
pageStr := c.DefaultQuery("page", "1")
page, _ := strconv.Atoi(pageStr)
tags := c.QueryArray("tag")
// 参数验证
if page < 1 {
page = 1
}
c.JSON(http.StatusOK, gin.H{
"category": category,
"subpath": subpath,
"keyword": keyword,
"page": page,
"tags": tags,
})
})
r.Run(":8080")
}
// 测试请求:
// GET /api/v1/articles/tech/backend/go?keyword=gin&page=2&tag=web&tag=api
// 返回:
// {
// "category": "tech",
// "subpath": "/backend/go",
// "keyword": "gin",
// "page": 2,
// "tags": ["web", "api"]
// }
设计原则总结:
- 路径参数用于标识资源层级和唯一实体。
- 通配符参数用于匹配动态路径片段,适合文件系统或嵌套资源。
- 查询参数用于过滤、分页、排序等非资源标识的附加信息。
- 三种参数类型可以组合使用,但需保持语义清晰,避免职责混淆。