【问题标题】:How to pass arguments to router handlers in Golang using Gin web framework?如何使用 Gin Web 框架将参数传递给 Golang 中的路由器处理程序?
【发布时间】:2016-03-06 21:33:17
【问题描述】:

我正在使用 Gin,https://gin-gonic.github.io/gin/,用 Golang 构建一个简单的 RESTful JSON API。

路线是这样设置的:

func testRouteHandler(c *gin.Context) {
    // do smth
}

func main() {
    router := gin.Default()
    router.GET("/test", testRouteHandler)
    router.Run(":8080")
}

我的问题是如何将参数传递给 testRouteHandler 函数?例如,一个公共数据库连接可能是一个希望在路由之间重用的东西。

将它放在全局变量中是最好的方法吗?或者 Go 中有什么方法可以将额外的变量传递给 testRouteHandler 函数? Go 中的函数是否有可选参数?

PS。我刚刚开始学习围棋,所以可能很明显我错过了一些东西:)

【问题讨论】:

标签: go go-gin


【解决方案1】:

我会避免将“应用程序范围”的依赖项(例如数据库连接池)填充到请求上下文中。您的两个“最简单”的选项是:

  1. 使其成为全球性的。这适用于较小的项目,*sql.DB 是线程安全的。
  2. 在闭包中显式传递它,以便返回类型满足gin.HandlerFunc

例如

// SomeHandler returns a `func(*gin.Context)` to satisfy Gin's router methods
// db could turn into an 'Env' struct that encapsulates all of your
// app dependencies - e.g. DB, logger, env vars, etc.
func SomeHandler(db *sql.DB) gin.HandlerFunc {
    fn := func(c *gin.Context) {
        // Your handler code goes in here - e.g.
        rows, err := db.Query(...)

        c.String(200, results)
    }

    return gin.HandlerFunc(fn)
}

func main() {
    db, err := sql.Open(...)
    // handle the error

    router := gin.Default()
    router.GET("/test", SomeHandler(db))
    router.Run(":8080")
}

【讨论】:

  • 不幸的是,在使用 UseGroup (github.com/gin-gonic/gin#grouping-routes) 之类的方法时,我在让这种闭包模式与 Gin 框架很好地协同工作时遇到了多个问题,我建议使用SetGet 方法,直到更好地设计Gin。我很少提倡更糟糕的设计,但最好在方法上保持一致并利用框架。很好的模式,但杜松子酒还没有。
【解决方案2】:

使用我在 cmets 上发布的链接,我创建了一个简单的示例。

package main

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

// ApiMiddleware will add the db connection to the context
func ApiMiddleware(db gorm.DB) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Set("databaseConn", db)
        c.Next()
    }
}

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

    // In this example, I'll open the db connection here...
    // In your code you would probably do it somewhere else
    db, err := gorm.Open("sqlite3", "./example.db")
    if err != nil {
        log.Fatal(err)
    }

    r.Use(ApiMiddleware(db))

    r.GET("/api", func(c *gin.Context) {
        // Don't forget type assertion when getting the connection from context.
        dbConn, ok := c.MustGet("databaseConn").(gorm.DB)
        if !ok {
            // handle error here...
        }

        // do your thing here...
    })

    r.Run(":8080")
}

这只是一个简单的 POC。但我相信这是一个开始。 希望对您有所帮助。

【讨论】:

  • 不幸的是,这是最好的答案。这并不理想,因为SetGet 创建了一个依赖关系,其中闭包模式可以简单地将参数从中间件传递到封闭的路由。然而,Gin 并没有自然地使用闭包模式,因此像UseGroup 这样的方法变得复杂。
  • 每次调用处理程序都会得到一个新的数据库副本,还是引用同一个?我需要像这样传递一个 http 客户端,但会将超时修改为下游的不同值
  • -1。我不建议使用这种方法,官方文档也建议不要这样做。 “仅将上下文值用于传输流程和 API 的请求范围数据,而不用于将可选参数传递给函数。”请参阅@wildneuro 答案以获得更好的依赖注入选项。
【解决方案3】:

聚会迟到了,到目前为止,这是我的建议。将方法封装到带有私有/公共变量的对象中:

package main

import (
    "log"

    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

type HandlerA struct {
    Db gorm.DB
}

func (this *HandlerA) Get(c *gin.Context) {

    log.Info("[%#f]", this.Db)
    // do your thing here...
}

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

    // Init, should be separate, but it's ok for this sample:
    db, err := gorm.Open("sqlite3", "./example.db")
    if err != nil {
        log.Fatal(err)
    }

    Obj := new(HandlerA)
    Obj.Db = db // Or init inside Object

    r := gin.New()

    Group := r.Group("api/v1/")
    {
        Group.GET("/storage", Obj.Get)
    }

    r.Run(":8080")
}

【讨论】:

    【解决方案4】:

    我喜欢 wildneuro 的示例,但会做一个单行来设置处理程序

    package main
    
    import (
        "log"
    
        "github.com/gin-gonic/gin"
        "github.com/jinzhu/gorm"
        _ "github.com/mattn/go-sqlite3"
    )
    
    type HandlerA struct {
        Db gorm.DB
    }
    
    func (this *HandlerA) Get(c *gin.Context) {
    
        log.Info("[%#f]", this.Db)
        // do your thing here...
    }
    
    func main() {
        r := gin.New()
    
        // Init, should be separate, but it's ok for this sample:
        db, err := gorm.Open("sqlite3", "./example.db")
        if err != nil {
            log.Fatal(err)
        }
     
        r := gin.New()
    
        Group := r.Group("api/v1/")
        {
            Group.GET("/storage", (&HandlerA{Db: db}).Get)
        }
    
        r.Run(":8080")
    }
    

    【讨论】:

      【解决方案5】:

      处理程序闭包是一个不错的选择,但仅在该处理程序中使用参数时效果最佳。

      如果您有路由组或较长的处理程序链,在多个地方需要相同的参数,您应该在 Gin 上下文中设置值。

      您可以使用函数字面量或返回 gin.HandlerFunc 的命名函数以简洁的方式执行此操作。

      将配置注入路由器组的示例:

      中间件包:

      func Configs(conf APIV1Config) gin.HandlerFunc {
          return func(c *gin.Context) {
              c.Set("configKey", conf) // key could be an unexported struct to ensure uniqueness
          }
      }
      

      路由器:

      conf := APIV1Config{/* some api configs */}
      
      // makes conf available to all routes in this group
      g := r.Group("/api/v1", middleware.Configs(conf))
      {
          // ... routes that all need API V1 configs
      }
      

      这也很容易进行单元测试。假设您测试单个处理程序,您可以将必要的值设置到模拟上下文中:

      w := httptest.NewRecorder()
      c, _ := gin.CreateTestContext(w)
      c.Set("configKey", /* mock configs */)
      
      apiV1FooHandler(c)
      

      现在对于应用程序范围的依赖项(数据库连接、远程客户端等),我同意将这些直接设置到 Gin 上下文中是一个糟糕的解决方案。

      那么您应该做的是,使用上述模式将 providers 注入到 Gin 上下文中:

      中间件包:

      // provider could be an interface for easy mocking
      func DBProvider(provider database.Provider) gin.HandlerFunc {
          return func(c *gin.Context) {
              c.Set("providerKey", provider)
          }
      }
      

      路由器:

      dbProvider := /* init provider with db connection */
      
      r.Use(DBProvider(dbProvider)) // global middleware
      // or
      g := r.Group("/users", DBProvider(dbProvider)) // users group only
      

      处理程序(您可以通过将这些上下文获取器放在一些辅助函数中来大大减少样板代码):

      // helper function
      func GetDB(c *gin.Context) *sql.DB {
         provider := c.MustGet("providerKey").(database.Provider)
         return provider.GetConn()
      }
      
      func createUserHandler(c *gin.Context) {
          db := GetDB(c) // same in all other handlers
          // ...
      }
      

      【讨论】:

        【解决方案6】:

        好的,我给你举了一个简单的例子。它应该工作。您可以根据需要扩展它

        func main() {
            router := gin.Default()
            router.GET("/test/:id/:name", testRouteHandler)
            router.Run(":8080")
        }
        
        func testRouteHandler(c *gin.Context) {
            id := c.Params.ByName("id")
            name := c.Params.ByName("name")
        }
        

        现在您必须调用您的处理程序,如下所示 http://localhost:8080/test/1/myname

        【讨论】:

          【解决方案7】:

          让我试着详细解释一下,以免你感到困惑。

          1. 根据传入路由,您想要调用控制器函数。假设您的传入路由是/books,而您的控制器是BooksController
          2. 您的BooksController 将尝试从数据库中获取图书并返回响应。

          现在,您希望在您的BooksController 中有一个处理程序,以便您可以访问数据库。

          我会做这样的事情。假设您使用的是 dynamoDB,并且 aws sdk 提供了*dynamodb.DynamoDB。根据您的数据库,更改此变量。

          1. 创建如下结构。
          type serviceConnection struct {
              db *dynamoDB.DynamoDB
              // You can have all services declared here 
              // which you want to use it in your controller
          }
          
          1. 在您的主函数中,获取数据库连接信息。假设您已经有一个函数initDatabaseConnection,它将一个处理程序返回给 db,如下所示。

          db := initDatabaseConnection() -> 返回*dynamodb.DynamoDB

          1. db 设置为结构变量。
          conn := new(serviceConnection)
          conn.db = db
          
          1. 使用接收器处理程序调用 gin 请求方法,如下所示。
          r := gin.Default()
          r.GET("/books", conn.BooksController)
          

          如您所见,gin 处理程序是一个控制器方法,它将您的结构实例作为接收器。

          1. 现在,使用serviceConnection struct 接收器创建一个控制器方法。
          func (conn *serviceConnection) BooksController(c *gin.Context) {
              books := getBooks(conn.db)
          }
          

          正如您在此处看到的,您可以访问所有 serviceConnection 结构变量,并且可以在控制器中使用它们。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-12-22
            • 2022-10-05
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-11-19
            • 2014-11-30
            • 1970-01-01
            相关资源
            最近更新 更多