Gin Integration

Complete guide for using godi with the Gin web framework.

Installation

go get github.com/junioryono/godi/v4
go get github.com/junioryono/godi/v4/gin

Quick Start

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/junioryono/godi/v4"
    godigin "github.com/junioryono/godi/v4/gin"
)

type UserController struct{}

func NewUserController() *UserController {
    return &UserController{}
}

func (c *UserController) List(ctx *gin.Context) {
    ctx.JSON(200, gin.H{"users": []string{"alice", "bob"}})
}

func main() {
    services := godi.NewCollection()
    services.AddScoped(NewUserController)

    provider, _ := services.Build()
    defer provider.Close()

    g := gin.Default()
    g.Use(godigin.ScopeMiddleware(provider))
    g.GET("/users", godigin.Handle((*UserController).List))

    g.Run(":8080")
}

ScopeMiddleware

Creates a request scope for each HTTP request. The scope is automatically closed when the request completes.

g := gin.New()
g.Use(godigin.ScopeMiddleware(provider))

Configuration Options

g.Use(godigin.ScopeMiddleware(provider,
    // Custom error handler for scope creation failures
    godigin.WithErrorHandler(func(c *gin.Context, err error) {
        c.AbortWithStatusJSON(500, gin.H{"error": "Service unavailable"})
    }),

    // Custom handler for scope close errors
    godigin.WithCloseErrorHandler(func(err error) {
        log.Printf("Scope close error: %v", err)
    }),

    // Middleware that runs after scope creation
    godigin.WithMiddleware(func(scope godi.Scope, c *gin.Context) error {
        // Initialize request context
        reqCtx := godi.MustResolve[*RequestContext](scope)
        reqCtx.UserID = c.GetHeader("X-User-ID")
        return nil
    }),
))

Handle

Wraps a controller method for type-safe resolution from the request scope.

type UserController interface {
    List(*gin.Context)
    GetByID(*gin.Context)
    Create(*gin.Context)
}

g.GET("/users", godigin.Handle(UserController.List))
g.GET("/users/:id", godigin.Handle(UserController.GetByID))
g.POST("/users", godigin.Handle(UserController.Create))

Handler Options

g.GET("/users", godigin.Handle(UserController.List,
    // Enable panic recovery
    godigin.WithPanicRecovery(true),

    // Custom panic handler
    godigin.WithPanicHandler(func(c *gin.Context, recovered any) {
        log.Printf("Panic: %v", recovered)
        c.AbortWithStatusJSON(500, gin.H{"error": "Unexpected error"})
    }),

    // Custom scope error handler
    godigin.WithScopeErrorHandler(func(c *gin.Context, err error) {
        c.AbortWithStatusJSON(500, gin.H{"error": "Session error"})
    }),

    // Custom resolution error handler
    godigin.WithResolutionErrorHandler(func(c *gin.Context, err error) {
        c.AbortWithStatusJSON(500, gin.H{"error": "Service unavailable"})
    }),
))

Complete Example

A production-ready setup with multiple controllers:

package main

import (
    "log"
    "net/http"
    "strconv"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
    "github.com/junioryono/godi/v4"
    godigin "github.com/junioryono/godi/v4/gin"
)

// === Services ===

type Logger struct{}

func NewLogger() *Logger { return &Logger{} }

func (l *Logger) Info(msg string, args ...any) {
    log.Printf("[INFO] "+msg, args...)
}

type RequestContext struct {
    ID        string
    UserID    string
    StartTime time.Time
}

func NewRequestContext() *RequestContext {
    return &RequestContext{
        ID:        uuid.New().String()[:8],
        StartTime: time.Now(),
    }
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

type UserService struct {
    reqCtx *RequestContext
    logger *Logger
}

func NewUserService(reqCtx *RequestContext, logger *Logger) *UserService {
    return &UserService{reqCtx: reqCtx, logger: logger}
}

func (s *UserService) GetAll() []User {
    s.logger.Info("[%s] Fetching all users", s.reqCtx.ID)
    return []User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}}
}

func (s *UserService) GetByID(id int) *User {
    s.logger.Info("[%s] Fetching user %d", s.reqCtx.ID, id)
    return &User{ID: id, Name: "User " + strconv.Itoa(id)}
}

// === Controllers ===

type UserController struct {
    service *UserService
    reqCtx  *RequestContext
}

func NewUserController(service *UserService, reqCtx *RequestContext) *UserController {
    return &UserController{service: service, reqCtx: reqCtx}
}

func (c *UserController) List(ctx *gin.Context) {
    users := c.service.GetAll()
    ctx.Header("X-Request-ID", c.reqCtx.ID)
    ctx.JSON(http.StatusOK, gin.H{
        "users":    users,
        "duration": time.Since(c.reqCtx.StartTime).String(),
    })
}

func (c *UserController) GetByID(ctx *gin.Context) {
    id, _ := strconv.Atoi(ctx.Param("id"))
    user := c.service.GetByID(id)
    ctx.Header("X-Request-ID", c.reqCtx.ID)
    ctx.JSON(http.StatusOK, user)
}

// === Main ===

func main() {
    // Register services
    services := godi.NewCollection()
    services.AddSingleton(NewLogger)
    services.AddScoped(NewRequestContext)
    services.AddScoped(NewUserService)
    services.AddScoped(NewUserController)

    // Build provider
    provider, err := services.Build()
    if err != nil {
        log.Fatalf("Failed to build provider: %v", err)
    }
    defer provider.Close()

    // Create router
    g := gin.New()
    g.Use(gin.Recovery())

    // Add scope middleware with initialization
    g.Use(godigin.ScopeMiddleware(provider,
        godigin.WithMiddleware(func(scope godi.Scope, c *gin.Context) error {
            reqCtx := godi.MustResolve[*RequestContext](scope)
            reqCtx.UserID = c.GetHeader("X-User-ID")
            return nil
        }),
    ))

    // Routes
    g.GET("/users", godigin.Handle((*UserController).List))
    g.GET("/users/:id", godigin.Handle((*UserController).GetByID))

    // Health check (no DI needed)
    g.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Start server
    log.Println("Server starting on :8080")
    g.Run(":8080")
}

Accessing Scope Manually

If you need to resolve services manually within a handler:

g.GET("/custom", func(c *gin.Context) {
    scope, err := godi.FromContext(c.Request.Context())
    if err != nil {
        c.AbortWithStatusJSON(500, gin.H{"error": "No scope"})
        return
    }

    service := godi.MustResolve[*UserService](scope)
    users := service.GetAll()
    c.JSON(200, users)
})

Route Groups

Use with Gin route groups:

api := g.Group("/api/v1")
{
    users := api.Group("/users")
    {
        users.GET("", godigin.Handle((*UserController).List))
        users.GET("/:id", godigin.Handle((*UserController).GetByID))
        users.POST("", godigin.Handle((*UserController).Create))
    }

    orders := api.Group("/orders")
    {
        orders.GET("", godigin.Handle((*OrderController).List))
        orders.POST("", godigin.Handle((*OrderController).Create))
    }
}

Error Responses

Default error responses return JSON:

{
  "error": "Internal Server Error"
}

Customize with error handlers:

godigin.WithResolutionErrorHandler(func(c *gin.Context, err error) {
    c.AbortWithStatusJSON(503, gin.H{
        "error":   "Service temporarily unavailable",
        "details": err.Error(),
    })
})

See also: Chi Integration | Echo Integration | Fiber Integration | net/http Integration