跳转到内容

依赖注入模式

随着 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)
}

另请参阅