Niklas9
Niklas9

Reputation: 9396

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

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 :)

Upvotes: 49

Views: 53559

Answers (8)

lennon guan
lennon guan

Reputation: 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
}

Upvotes: -1

wildneuro
wildneuro

Reputation: 213

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

Upvotes: 9

blackgreen
blackgreen

Reputation: 45140

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
    // ...
}

Upvotes: 3

Vasuki Dileep
Vasuki Dileep

Reputation: 583

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.

Upvotes: 0

Joakim Karlsson
Joakim Karlsson

Reputation: 19

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

Upvotes: 1

elithrar
elithrar

Reputation: 24300

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

Upvotes: 61

deepak
deepak

Reputation: 55

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

Upvotes: -1

rcmgleite
rcmgleite

Reputation: 1321

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.

Upvotes: 42

Related Questions