想必很多人在使用 Gin 编写路由函数的时候遇到过以下问题:

panic: wildcard route ‘:articleID’ conflicts with existing children in path ‘/articles/:articleID’

如何解决这个问题呢?

原因

出现这个问题主要是因为 golang 很多框架使用 httprouter 作为他们的路由模块。httprouter 为了性能优化使用了显式匹配,这就导致了很多通配符与具体路由产生冲突。

如下列路由就会冲突:

1
2
GET /articles/info
GET /articles/:articleID

解决方法

1. 修改路由规则

对于路由规则不是很复杂的项目,可以使用规避传统路由规则的方法,如:

1
2
GET /articles/info
GET /article/:articleId

但是这种方法违背了我们常用的 RESTful API 规则,非常不舒服。在项目比较大比较负责的情况下就会非常混乱。

2. 特定路由参数分支

一种简单的解决方法就是在通配符外面包裹一层判断语句。

1
2
3
4
5
6
7
router.GET("/v1/users/:userID", func(c *gin.Context) {
    if c.Param("userID") == "info" {
        ...
    } else {
        ...
    }
})

如果觉得这种写法不易阅读,还可以使用如下判断语句:

1
2
3
4
5
6
7
router.GET("/v1/users/:userID", func(c *gin.Context) {
    if strings.HasPrefix(c.Request.RequestURI, "/v1/users/info") {
        ...
        return
    }
    ...
})

3. 多级路由分支

httprouter 中通配符接通配符是可以被正常识别的,所以在多级路由中可以使用以下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
router.GET("/v1/images/:path1", GetHandler)           //      /v1/images/detail
router.GET("/v1/images/:path1/:path2", GetHandler)    //      /v1/images/<id>/history

func GetHandler(c *gin.Context) {
    path1 := c.Param("path1")
    path2 := c.Param("path2")

    if path1 == "detail" && path2 == "" {
        Detail(c)
    } else if path1 != "" && path2 == "history" {
        imageId := path1
        History(c, imageId)
    } else {
        HandleHttpError(c, NewHttpError(404, "Page not found"))
    }
}

4. 使用 * 号进行通配

httprouter 提供 * 号匹配,匹配效果如下:

1
2
3
4
5
/users/*action

    /users/                     action = ""
    /users/somefile.go          action = "somefile.go"
    /users/subdir/somefile.go   action = "subdir/somefile.go"

总结

httprouter 在提供高性能的同时也确实带来了诸多不便,以上几种方法不能说完美,但应该可以满足大部分 RESTful API 的规范要求。