Gin 路由器:路径段与现有通配符冲突

Xep*_*eph 5 go go-gin

我想让我的应用程序服务于以下事物。

  • a.com => 将 /www 提供给浏览器,以便浏览器可以查找 /www/index.html)
  • a.com/js/mylib.js => 将 /www/js/mylib.js 提供给浏览器
  • a.com/api/v1/disk => 返回 JSON 的典型 REST API
  • a.com/api/v1/memory => 另一个 API

我做了如下代码:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()

    r.Static("/", "/www")

    apiv1 := r.Group("api/v1")
    {
        apiv1.GET("/disk", diskSpaceHandler)
        apiv1.GET("/memory", memoryHandler)
        apiv1.GET("/cpu", cpuHandler)
    }

    r.Run(":80")
}
Run Code Online (Sandbox Code Playgroud)

当我运行代码时,它恐慌:

panic: path segment '/api/v1/disk' conflicts with existing wildcard '/*filepath' in path '/api/v1/disk'
Run Code Online (Sandbox Code Playgroud)

我明白为什么它会恐慌,但我不知道如何解决。

我脑子里只有两件事:

  1. 使用 NoRoute() 函数,它将处理 /api/v1 组路径以外的其他内容(不知道我是如何实现的)
  2. 使用中间件。有静态中间件https://github.com/gin-gonic/contrib但代码在 Windows 上不起作用(https://github.com/gin-gonic/contrib/issues/91

先感谢您。

bla*_*een 21

这是Gin 底层路由器实现的预期功能。路由在前缀上进行匹配,因此与另一个现有路径段处于同一位置的任何路径参数或通配符都会导致冲突。

在这个特定的问答中,该方法RouterGroup.Static将通配符添加/*filepath到您提供服务的路由中。如果该路由是根路由/则通配符将与您声明的所有其他路由发生冲突

那该怎么办呢?

您必须承认没有直接的解决方案,因为这就是 Gin 的路由器实现的工作原理。如果您不能接受解决方法,那么您可能必须更改 HTTP 框架。评论中提到 Echo 作为替代方案。

1. 如果您可以更改路线映射

最好的解决方法不是没有解决方法,而是拥抱 Gin 的设计。然后你可以简单地在静态文件路径中添加一个唯一的前缀:r.Static("/static", "/www")。这并不意味着您更改本地目录结构,仅更改映射到它的路径前缀。请求 URL 必须更改。

2. 通配符与一条或几条其他路由冲突

假设您的路由器只有这两条路由:

/*any
/api/foo
Run Code Online (Sandbox Code Playgroud)

在这种情况下,您可能会使用中间处理程序并手动检查路径参数:

r.GET("/*any", func(c *gin.Context) {
    path := c.Param("any")
    if strings.HasPrefix(path, "/api") {
        apiHandler(c) // your regular api handler
    } else {
        // handle *any
    }
})
Run Code Online (Sandbox Code Playgroud)

3. 与许多其他路由的通配符冲突

哪一种最好取决于您的具体情况和目录结构。

3.1 使用r.NoRoute处理程序;这可能有效,但是是一个糟糕的黑客行为。

r.NoRoute(gin.WrapH(http.FileServer(gin.Dir("static", false))))
r.GET("/api", apiHandler)
Run Code Online (Sandbox Code Playgroud)

static这将从(或任何其他目录)提供文件,但它也会尝试为该组下的所有不存在的路由提供资产/api。例如/api/xyz将由处理程序处理NoRoute。这可能是可以接受的,直到不能接受为止。例如,如果您恰好有一个api在静态资产中命名的文件夹。

3.2 使用中间件

例如,您还可以找到gin-contrib/static

// declare before other routes
r.Use(static.Serve("/", static.LocalFile("www", false)))
Run Code Online (Sandbox Code Playgroud)

这个中间件稍微复杂一些,但它也有同样的限制。即:

  • 如果静态资源中有一个类似于 API 路由的目录,则会导致无限重定向(可以使用 缓解Engine#RedirectTrailingSlash = false
  • 即使没有无限重定向,中间件也会首先检查本地 FS,只有在没有发现任何内容的情况下才会继续处理链中的下一个处理程序。这意味着您在每次请求时都会进行系统调用来检查文件是否存在。(或者至少是这样gin-contrib/static的,如下所示)
    r := gin.New()
    r.Use(func(c *gin.Context) {
        fname := "static" + c.Request.URL.Path
        if _, err := os.Stat(fname); err == nil {
            c.File(fname)
            c.Abort() // file found, stop the handler chain
        }
        // else move on to the next handler in chain
    })
    r.GET("/api", apiHandler)
    r.Run(":5555")
Run Code Online (Sandbox Code Playgroud)

3.3 使用Gin子引擎;如果您有很多潜在的冲突,例如通配符/和带有组的复杂 API 路由等等,这可能是一个不错的选择。使用子引擎可以让你更好地控制它,但实现仍然感觉很麻烦。基于的示例Engine.HandleContext

func main() {
    apiEngine := gin.New()
    apiG := apiEngine.Group("/api")
    {
        apiG.GET("/foo", func(c *gin.Context) { c.JSON(200, gin.H{"foo": true})})
        apiG.GET("/bar", func(c *gin.Context) { c.JSON(200, gin.H{"bar": true})})
    }

    r := gin.New()
    r.GET("/*any", func(c *gin.Context) {
        path := c.Param("any")
        if strings.HasPrefix(path, "/api") {
            apiEngine.HandleContext(c)
        } else {
            assetHandler(c)
        }
    })
    r.Run(":9955")
}
Run Code Online (Sandbox Code Playgroud)

包起来

如果可以的话,重新设计你的路线。如果你不能,这个答案提供了三种可能的解决方法,但复杂性不断增加。与往常一样,这些限制可能适用也可能不适用于您的特定用例。YMMV。

如果这些都不适合您,也许可以留下评论来指出您的用例。


小智 -3

您应该使用静态中间件,请参阅其示例:

https://github.com/gin-contrib/static#canonical-example

r.Use(static.Serve("/", static.LocalFile("/tmp", false)))
Run Code Online (Sandbox Code Playgroud)