Dependency Injection Patterns
As your Gin application grows, you need a clean way to share dependencies like database connections, configuration, and services across handlers. Go’s simplicity encourages straightforward patterns over heavy DI frameworks.
Closure pattern
Section titled “Closure pattern”The most idiomatic Go approach: pass dependencies via closures.
package main
import ( "database/sql" "net/http"
"github.com/gin-gonic/gin" _ "github.com/lib/pq")
func PingHandler(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { if err := db.Ping(); err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database unreachable"}) return } c.JSON(http.StatusOK, gin.H{"status": "ok"}) }}
func GetUserHandler(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { id := c.Param("id") var name string err := db.QueryRowContext(c.Request.Context(), "SELECT name FROM users WHERE id = $1", id).Scan(&name) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"name": name}) }}
func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable") if err != nil { panic(err) } defer db.Close()
r := gin.Default() r.GET("/ping", PingHandler(db)) r.GET("/users/:id", GetUserHandler(db)) r.Run(":8080")}This pattern works well for small to medium applications. Each handler explicitly declares its dependencies.
Struct-based handlers
Section titled “Struct-based handlers”For applications with many shared dependencies, group handlers in a struct:
package main
import ( "database/sql" "log/slog" "net/http"
"github.com/gin-gonic/gin" _ "github.com/lib/pq")
type App struct { DB *sql.DB Logger *slog.Logger}
func (a *App) GetUser(c *gin.Context) { id := c.Param("id") var name string err := a.DB.QueryRowContext(c.Request.Context(), "SELECT name FROM users WHERE id = $1", id).Scan(&name) if err != nil { a.Logger.Error("user not found", slog.String("id", id), slog.String("error", err.Error())) c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } c.JSON(http.StatusOK, gin.H{"id": id, "name": name})}
func (a *App) CreateUser(c *gin.Context) { var input struct { Name string `json:"name" binding:"required"` } if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
_, err := a.DB.ExecContext(c.Request.Context(), "INSERT INTO users (name) VALUES ($1)", input.Name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create user"}) return } c.JSON(http.StatusCreated, gin.H{"name": input.Name})}
func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable") if err != nil { panic(err) } defer db.Close()
app := &App{ DB: db, Logger: slog.Default(), }
r := gin.Default() r.GET("/users/:id", app.GetUser) r.POST("/users", app.CreateUser) r.Run(":8080")}Middleware injection
Section titled “Middleware injection”Use middleware to inject dependencies into the request context. This is useful when many handlers need the same dependency:
func DatabaseMiddleware(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { c.Set("db", db) c.Next() }}
func GetUser(c *gin.Context) { db := c.MustGet("db").(*sql.DB) // Use db...}
func main() { db, _ := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable")
r := gin.Default() r.Use(DatabaseMiddleware(db)) r.GET("/users/:id", GetUser) r.Run(":8080")}Comparing patterns
Section titled “Comparing patterns”| Pattern | Type safety | Testability | Best for |
|---|---|---|---|
| Closure | Compile-time | Easy to mock | Small apps, few dependencies |
| Struct | Compile-time | Easy to mock | Medium-large apps |
| Middleware | Runtime | Moderate | Cross-cutting concerns, shared state |
Testing with dependency injection
Section titled “Testing with dependency injection”All patterns make testing straightforward — inject test doubles:
func TestGetUser(t *testing.T) { // Set up test database or mock db := setupTestDB(t)
router := gin.New() router.GET("/users/:id", GetUserHandler(db)) // closure pattern
w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/users/1", nil) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)}