Go and Gin: Passing around struct for database context?
Asked Answered
D

5

22

I've just started trying out Go, and I'm looking to re-implement an API server written in node with it.

I've hit a hurdle with trying to use dependency injection to pass around a database context as a gin middleware. So far I've set it up as this:

main.go:

package main

import (
        "fmt"
        "runtime"
        "log"
        "github.com/gin-gonic/gin"
        "votesforschools.com/api/public"
        "votesforschools.com/api/models"
)

type DB struct {
        models.DataStore
}

func main() {
        ConfigRuntime()
        ConfigServer()
}

func Database(connectionString string) gin.HandlerFunc {
        dbInstance, err := models.NewDB(connectionString)
        if err != nil {
                log.Panic(err)
        }

        db := &DB{dbInstance}

        return func(c *gin.Context) {
                c.Set("DB", db)
                c.Next()
        }
}


func ConfigRuntime() {
        nuCPU := runtime.NumCPU()
        runtime.GOMAXPROCS(nuCPU)
        fmt.Printf("Running with %d CPUs\n", nuCPU)
}

func ConfigServer() {

        gin.SetMode(gin.ReleaseMode)

        router := gin.New()
        router.Use(Database("<connectionstring>"))
        router.GET("/public/current-vote-pack", public.GetCurrentVotePack)
        router.Run(":1000")
}

models/db.go

package models

import (
        "database/sql"
        _ "github.com/go-sql-driver/mysql"
)

type DataStore interface {
        GetVotePack(id string) (*VotePack, error)
}

type DB struct {
        *sql.DB
}

func NewDB(dataSource string) (*DB, error) {
        db, err := sql.Open("mysql", dataSource)
        if err != nil {
                return nil, err
        }
        if err = db.Ping(); err != nil {
                return nil, err
        }
        return &DB{db}, nil
}

models/votepack.go

package models

import (
        "time"
        "database/sql"
)

type VotePack struct {
        id string
        question string
        description string
        startDate time.Time
        endDate time.Time
        thankYou string
        curriculum []string
}

func (db *DB) GetVotePack(id string) (*VotePack, error) {

        var votePack *VotePack

        err := db.QueryRow(
                "SELECT id, question, description, start_date AS startDate, end_date AS endDate, thank_you AS thankYou, curriculum WHERE id = ?", id).Scan(
                &votePack.id, &votePack.question, &votePack.description, &votePack.startDate, &votePack.endDate, &votePack.thankYou, &votePack.curriculum)

        switch {
        case err == sql.ErrNoRows:
                return nil, err
        case err != nil:
                return nil, err
         default:
                return votePack, nil
        }
}

So with all of the above, I want to pass the models.DataSource around as a middleware so it can be accessed like this:

public/public.go

package public

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

func GetCurrentVotePack(context *gin.Context) {
        db := context.Keys["DB"]

        votePack, err := db.GetVotePack("c5039ecd-e774-4c19-a2b9-600c2134784d")
        if err != nil{
                context.String(404, "Votepack Not Found")
        }
        context.JSON(200, votePack)
}

However I get public\public.go:10: db.GetVotePack undefined (type interface {} is interface with no methods)

When I inspect in the debugger (using Webstorm with plugin) the db is just an empty object. I'm trying to be good and avoid global variable use

Dialect answered 27/2, 2016 at 17:17 Comment(0)
G
14

The values within context.Keys are all of type interface{}, so db will not be able to call methods from type *DB until it's converted back to that type.

The safe way:

db, ok := context.Keys["DB"].(*DB)
if !ok {
        //Handle case of no *DB instance
}
// db is now a *DB value

The less safe way, which will panic if context.Keys["DB"] is not a value of type *DB:

db := context.Keys["DB"].(*DB)
// db is now a *DB value

Effective Go has a section on this.

Galactose answered 27/2, 2016 at 18:7 Comment(3)
While this solves the issue, it looks like very bad design to me. Relying in someone putting interface{} structs in a generic context doesn't look like proper strongly typed design. I guess most of the fault is because gin expects a function for the handle method instead of an interfaceAlphonse
@Galactose Is it good way to pass database connection through Context? Is there any other way&?Autoerotic
@Autoerotic It's a while back, but yes, there is. Check out https://mcmap.net/q/570633/-go-and-gin-passing-around-struct-for-database-context.Ieyasu
A
40

I don't think context should be used as DI container: https://golang.org/pkg/context/

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

I would rather use:

package public

type PublicController struct {
        Database *DB
}

func (c *PublicController) GetCurrentVotePack(context *gin.Context) {
        votePack, err := c.Database.GetVotePack("c5039ecd-e774-4c19-a2b9-600c2134784d")
        if err != nil{
                context.String(404, "Votepack Not Found")
        }
        context.JSON(200, votePack)
}

and configure your controller in main:

func main() {
        pCtrl := PublicController { Database: models.NewDB("<connectionstring>") }

        router := gin.New()
        router.GET("/public/current-vote-pack", pCtrl.GetCurrentVotePack)
        router.Run(":1000")
}
Aretina answered 20/2, 2020 at 16:37 Comment(1)
This should be the accepted answer. As stated here, the context should not carry any database configuration (or similar).Ieyasu
G
14

The values within context.Keys are all of type interface{}, so db will not be able to call methods from type *DB until it's converted back to that type.

The safe way:

db, ok := context.Keys["DB"].(*DB)
if !ok {
        //Handle case of no *DB instance
}
// db is now a *DB value

The less safe way, which will panic if context.Keys["DB"] is not a value of type *DB:

db := context.Keys["DB"].(*DB)
// db is now a *DB value

Effective Go has a section on this.

Galactose answered 27/2, 2016 at 18:7 Comment(3)
While this solves the issue, it looks like very bad design to me. Relying in someone putting interface{} structs in a generic context doesn't look like proper strongly typed design. I guess most of the fault is because gin expects a function for the handle method instead of an interfaceAlphonse
@Galactose Is it good way to pass database connection through Context? Is there any other way&?Autoerotic
@Autoerotic It's a while back, but yes, there is. Check out https://mcmap.net/q/570633/-go-and-gin-passing-around-struct-for-database-context.Ieyasu
V
1

You would need type assertion to convert the interface (db := context.Keys["DB"]) into something useful. See for example this post: convert interface{} to int in Golang

Vestibule answered 27/2, 2016 at 17:52 Comment(0)
I
0

There's another way to do it when DB is set to the context during startup.

 db := ctx.MustGet("DB").(*gorm.DB)

MustGet returns the value for the given key if it exists, otherwise it panics.

Irrespective answered 16/2, 2021 at 12:7 Comment(0)
S
0

Great article on this topic: Organising Database Access in Go which covers pluses and minuses of many possibilities.

Regarding using context for passing db connection:

[...] request context should only be used to store values which are created during an individual request cycle and are no longer needed after the request has completed. It's not really intended to store long-lived handler dependencies like connection pools, loggers or template caches.

The solution proposed by Łukasz Marszał is also described and it fits me quite well. "Wrapping the connection pool" may appeal more for some people, but in my opinion it adds too much complexity (e.g. implies using models) - not a problem if these structures are already present in your project.

Serialize answered 29/9, 2023 at 22:30 Comment(1)
I've added this answer, because I hadn't found satisfying explanation of design choices in existing ones.Desiredesirea

© 2022 - 2024 — McMap. All rights reserved.