gogo-gin

How can I pass a database Instance from my route.go to my controller.go file?


I need to pass a database instance from my route.go file to a controller file in gin.

I tried passing the Server Struct But I'm getting a circular import. Also, I cant use any of *sql.DB function even in the same file. I am using melkey's go-blueprint package.

This is my server.go file

er

import (
    "backend/internal/database"
    "fmt"
    "net/http"
    "os"
    "strconv"
    "time"

    _ "github.com/joho/godotenv/autoload"
)

type Server struct {
    port int

    db database.Service
}

func NewServer() *http.Server {
    port, _ := strconv.Atoi(os.Getenv("PORT"))
    NewServer := &Server{
        port: port,

        db: database.New(),
    }

    // Declare Server config
    server := &http.Server{
        Addr:         fmt.Sprintf(":%d", NewServer.port),
        Handler:      NewServer.RegisterRoutes(),
        IdleTimeout:  time.Minute,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
    }

    return server
}

this is my route.go file

package server

import (
    "backend/internal/controllers"
    "net/http"

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

func (s *Server) RegisterRoutes() http.Handler {
    r := gin.Default()

    r.GET("/", s.HelloWorldHandler)

    r.GET("/health", s.healthHandler)
    r.GET("/test", controllers.TestController)
    auth := r.Group("/auth")
    AuthRoute(auth)
    return r
}

func (s *Server) HelloWorldHandler(c *gin.Context) {
    resp := make(map[string]string)
    resp["message"] = "Hello World"
        // Unable to use s.db.Query() or something
    c.JSON(http.StatusOK, resp)
}

func (s *Server) healthHandler(c *gin.Context) {
        
    c.JSON(http.StatusOK, s.db.Health())
}

Controller File

package controllers

import (
    "net/http"

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

func TestController(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "message": "Hello World from the controllers package",
    })
}

And this is the Database.go file

package database

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "os"
    "time"

    _ "github.com/jackc/pgx/v5/stdlib"
    _ "github.com/joho/godotenv/autoload"
)

type Service interface {
    Health() map[string]string
}

type service struct {
    db *sql.DB
}

var (
    database   = os.Getenv("DB_DATABASE")
    password   = os.Getenv("DB_PASSWORD")
    username   = os.Getenv("DB_USERNAME")
    port       = os.Getenv("DB_PORT")
    host       = os.Getenv("DB_HOST")
    dbInstance *service
)

func New() Service {
    // Reuse Connection
    if dbInstance != nil {
        return dbInstance
    }
    connStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", username, password, host, port, database)
    db, err := sql.Open("pgx", connStr)
    if err != nil {
        log.Fatal(err)
    }
    dbInstance = &service{
        db: db,
    }

    // We Migrate all the data by calling the migrate Function
    Migrate(dbInstance)
    return dbInstance
}

func (s *service) Health() map[string]string {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    err := s.db.PingContext(ctx)
    if err != nil {
        log.Fatalf(fmt.Sprintf("db down: %v", err))
    }

    return map[string]string{
        "message": "It's healthy",
    }
}

Solution

  • I'll recommend you to use dependency injection by passing DB where is needed. allow me to propose the following refactoring.

    database.go

    package main
    
    import (
        "database/sql"
        "fmt"
        _ "github.com/lib/pq"
    )
    
    func NewDatabase(dbUser, dbPassword, dbHost, dbName, dbPort string) (*sql.DB, error) {
        connectionStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dbUser, dbPassword, dbHost, dbPort, dbName)
        return sql.Open("postgres", connectionStr)
    }
    
    

    service.go

    package main
    
    import "database/sql"
    
    type User struct {
        Id     string `json:"id"`
        Name   string `json:"name"`
        Email  string `json:"email"`
        Gender string `json:"gender"`
    }
    type UserLoader interface {
        GetAll() ([]User, error)
        GetUserByEmail(string) (User, error)
    }
    
    type userLoader struct {
        dbCon *sql.DB
    }
    
    func NewUserLoader(dbCon *sql.DB) UserLoader {
        return &userLoader{dbCon: dbCon}
    }
    
    func (l *userLoader) GetAll() ([]User, error) {
        rows, err := l.dbCon.Query("SELECT * FROM users")
        if err != nil {
            return nil, err
        }
        defer rows.Close()
        users := make([]User, 0)
        for rows.Next() {
            var user User
            if err := rows.Scan(&user.Id, &user.Name, &user.Email, &user.Gender); err != nil {
                return nil, err
            }
            users = append(users, user)
        }
        return users, nil
    }
    
    func (l *userLoader) GetUserByEmail(email string) (User, error) {
        panic("implement me")
    }
    
    

    router.go

    package main
    
    import (
        "github.com/gin-gonic/gin"
        "net/http"
    )
    
    func NewRouter(userLoader UserLoader) *gin.Engine {
        r := gin.Default()
    
        r.GET("/", HelloWorld)
    
        r.GET("/health", Health)
        api := r.Group("/api")
        api.GET("/users", GetAllUsers(userLoader))
        return r
    }
    
    func GetAllUsers(loader UserLoader) gin.HandlerFunc {
        return func(c *gin.Context) {
            users, err := loader.GetAll()
            if err != nil {
                c.AbortWithStatus(http.StatusInternalServerError)
            }
            c.JSON(http.StatusOK, users)
        }
    }
    
    func HelloWorld(c *gin.Context) {
        c.JSON(200, gin.H{"message": "Hello World from the controllers package"})
    }
    
    func Health(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    }
    
    

    main.go

    package main
    
    import (
        "fmt"
        "github.com/gin-gonic/gin"
        "os"
    )
    
    func main() {
        database := os.Getenv("DB_DATABASE")
        dbPassword := os.Getenv("DB_PASSWORD")
        dbUsername := os.Getenv("DB_USERNAME")
        dbPort := os.Getenv("DB_PORT")
        host := os.Getenv("DB_HOST")
        serverPort := os.Getenv("SERVER_PORT")
    
        dbCon, err := NewDatabase(dbUsername, dbPassword, host, database, dbPort)
        if err != nil {
            panic(err)
        }
    
        userLoader := NewUserLoader(dbCon)
    
        router := gin.Default()
        router.Use(gin.Recovery())
    
        router.GET("/ping", func(c *gin.Context) {
            c.JSON(200, gin.H{})
        })
    
        NewRouter(userLoader).Run(fmt.Sprintf(":%s", serverPort))
    }