How to pass arguments to router handlers in Golang using Gin web framework?
Asked Answered
N

8

49

I'm using Gin, https://gin-gonic.github.io/gin/, to build a simple RESTful JSON API with Golang.

The routes are setup with something like this:

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

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

My question is how can I pass down an argument to the testRouteHandler function? For example a common database connection could be something that one wants to reuse among routes.

Is the best way to have this in a global variable? Or is there some way in Go to pass along an extra variable to the testRouteHandler function? Are there optional arguments for functions in Go?

PS. I'm just getting started in learning Go, so could be something obvious that I'm missing :)

Nowhither answered 2/12, 2015 at 15:13 Comment(3)
I think you are looking for an HTTP middleware This is a good place to start nicolasmerouze.com/middlewares-golang-best-practices-examplesLookeron
As @Lookeron said, middlewares are the way to go... take a look at Gin's custom middlewares. github.com/gin-gonic/gin#custom-middlewareKimberly
Examples with middlewares here (a few posts below)Yanina
K
43

Using the link i posted on comments, I have created a simple example.

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")
}

This is just a simple POC. But i believe it's a start. Hope it helps.

Kimberly answered 2/12, 2015 at 21:11 Comment(4)
Unfortunately, this is the best answer. It's not ideal, because the Set and Get creates a dependency, where the closure pattern could simply pass the parameters from the middleware to the enclosed route. However, Gin does not naturally make use of the closure pattern and so some methods like Use and Group become complicated.Example
Will every call to the handler get a new copy of db, or would they reference the same one? I need to pass an http client like this but will modify the timeout to different values downstreamZulmazulu
-1. I don't recommend using this approach, and the official documentation advises against it too. "Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions." See @wildneuro answer for a better, dependency injection, option.Phytohormone
@Zulmazulu - the database connection is created once in the main function, and all db objects would reference that same connection. The same is true with your http client, and if you modify the timeout in any route, then all routes would be affected. So instead, create multiple http clients in your main function with the required settings. So for example, in main create a httpClient15 with a 15 second timeout and httpClient30 with a 30 second timeout, and then pass whichever client is needed to your downstream route.Example
U
59

I would avoid stuffing 'application scoped' dependencies (e.g. a DB connection pool) into a request context. Your two 'easiest' options are:

  1. Make it a global. This is OK for smaller projects, and *sql.DB is thread-safe.
  2. Pass it explicitly in a closure so that the return type satisfies gin.HandlerFunc

e.g.

// 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")
}
Unsociable answered 3/12, 2015 at 2:20 Comment(1)
Unfortunately, I've experienced multiple issues in getting this closure pattern to work nicely with the Gin framework when using methods like Use and Group (github.com/gin-gonic/gin#grouping-routes), and I would recommend to use the Set and Get methods until Gin is better designed. I rarely advocate worse design, but it's better to be consistent in approach and leverage the framework. Great pattern, but Gin is not there yet.Example
K
43

Using the link i posted on comments, I have created a simple example.

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")
}

This is just a simple POC. But i believe it's a start. Hope it helps.

Kimberly answered 2/12, 2015 at 21:11 Comment(4)
Unfortunately, this is the best answer. It's not ideal, because the Set and Get creates a dependency, where the closure pattern could simply pass the parameters from the middleware to the enclosed route. However, Gin does not naturally make use of the closure pattern and so some methods like Use and Group become complicated.Example
Will every call to the handler get a new copy of db, or would they reference the same one? I need to pass an http client like this but will modify the timeout to different values downstreamZulmazulu
-1. I don't recommend using this approach, and the official documentation advises against it too. "Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions." See @wildneuro answer for a better, dependency injection, option.Phytohormone
@Zulmazulu - the database connection is created once in the main function, and all db objects would reference that same connection. The same is true with your http client, and if you modify the timeout in any route, then all routes would be affected. So instead, create multiple http clients in your main function with the required settings. So for example, in main create a httpClient15 with a 15 second timeout and httpClient30 with a 30 second timeout, and then pass whichever client is needed to your downstream route.Example
E
9

Late to the party, so far here is my proposal. Encapsulate methods into the object with private/public vars in it:

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")
}
Eyestalk answered 27/4, 2017 at 1:29 Comment(1)
yo this is good and fancy. using it 😉Aileneaileron
Y
3

Handler closures are a good option, but that works best when the argument is used in that handler alone.

If you have route groups, or long handler chains, where the same argument is needed in multiple places, you should set values into the Gin context.

You can use function literals, or named functions that return gin.HandlerFunc to do that in a clean way.

Example injecting configs into a router group:

Middleware package:

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

Router:

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
}

This is also easily unit-testable. Assuming that you test the single handlers, you can set the necessary values into the mock context:

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

apiV1FooHandler(c)

Now in the case of application-scoped dependencies (db connections, remote clients, ...), I agree that setting these directly into the Gin context is a poor solution.

What you should do then, is to inject providers into the Gin context, using the pattern outlined above:

Middleware package:

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

Router:

dbProvider := /* init provider with db connection */

r.Use(DBProvider(dbProvider)) // global middleware
// or
g := r.Group("/users", DBProvider(dbProvider)) // users group only

Handler (you can greatly reduce the boilerplate code by putting these context getters in some helper function):

// 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
    // ...
}
Yanina answered 26/9, 2021 at 21:5 Comment(0)
M
1

I like wildneuro's example but would do a one liner to setup the handler

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")
}
Maduro answered 28/4, 2021 at 9:41 Comment(0)
A
0

Alright, I have given you a simple example. It should work. You can extend it as per your need

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")
}

Now you will have to call your handler as below http://localhost:8080/test/1/myname

Assonance answered 27/2, 2016 at 15:13 Comment(0)
W
0

Let me try to explain in detail so that you won't get confused.

  1. Depending on the incoming route, you want to call a controller function. Lets say your incoming route is /books and your controller is BooksController
  2. Your BooksController will try to fetch the books from the database and returns a response.

Now, you want this a handler within your BooksController so that you can access database.

I would do something like this. Let's assume that you are using dynamoDB and the aws sdk provides *dynamodb.DynamoDB. Depending on your db, change this variable.

  1. Create a struct as below.
type serviceConnection struct {
    db *dynamoDB.DynamoDB
    // You can have all services declared here 
    // which you want to use it in your controller
}
  1. In your main function, get the db connection information. Let's say you already have a function initDatabaseConnection which returns a handler to db, something like below.

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

  1. Set db to a struct variable.
conn := new(serviceConnection)
conn.db = db
  1. Call the gin request method with a receiver handler as below.
r := gin.Default()
r.GET("/books", conn.BooksController)

As you see, the gin handler is a controller method which has your struct instance as a receiver.

  1. Now, create a controller method with serviceConnection struct receiver.
func (conn *serviceConnection) BooksController(c *gin.Context) {
    books := getBooks(conn.db)
}

As you see here, you have access to all the serviceConnection struct variables and you can use them in your controller.

Watford answered 10/5, 2021 at 16:7 Comment(0)
V
-1

https://github.com/lennon-guan/gin-utils/blob/main/inject/examples/main.go

I just push my library to github. In this "inject" library, you can easily inject any value into your handler func.

The comment is written in Chinese. Below is the example with English comment:

package main

import (
    "fmt"
    "sync/atomic"

    "github.com/gin-gonic/gin"
    "github.com/lennon-guan/gin-utils/inject"
)

func main() {
    var counter atomic.Int64
    // Add int64 injector
    inject.AddInjector(func() int64 {
        return counter.Add(1)
    })
    // Add SomeRes injector
    // SomeRes implements io.Closer, so when HandlerFunc returns, SomeRes's Close method will be execute automaticlly
    inject.AddInjector(func() SomeResource {
        fmt.Println("made a new SomeResource!")
        return SomeResource{}
    })

    engine := gin.Default()
    // inject.Wrap2 makes a HandlerFunc with two injected arguments
    engine.GET("/", inject.Wrap2(func(c *gin.Context, counter int64, res SomeResource) {
        c.String(200, "req %d", counter)
    }))
    engine.Run(":28080")
}

type SomeResource struct{}

func (SomeResource) Close() error {
    fmt.Println("SomeResource as io.Closer released!")
    return nil
}
Voiceful answered 7/6 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.