依赖注入模式
随着 Gin 应用的增长,你需要一种简洁的方式在处理函数之间共享数据库连接、配置和服务等依赖。Go 的简洁性鼓励使用直接的模式,而不是重量级的 DI 框架。
闭包模式
最符合 Go 习惯的方式:通过闭包传递依赖。
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")}此模式适用于中小型应用。每个处理函数显式声明其依赖。
基于结构体的处理函数
对于有许多共享依赖的应用,将处理函数分组到结构体中:
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")}中间件注入
使用中间件将依赖注入到请求上下文中。当许多处理函数需要相同的依赖时很有用:
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")}模式比较
| 模式 | 类型安全 | 可测试性 | 适用场景 |
|---|---|---|---|
| 闭包 | 编译时 | 容易 mock | 小型应用,少量依赖 |
| 结构体 | 编译时 | 容易 mock | 中大型应用 |
| 中间件 | 运行时 | 中等 | 横切关注点,共享状态 |
使用依赖注入进行测试
所有模式都使测试变得简单——注入测试替身:
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)}