跳转到内容

连接数据库

本指南展示如何使用显式构造函数注入将数据库连接注入 Plumego handler — 与参考服务对所有依赖使用的模式相同。

边界原理请参见 Store Primer

  • 在 handler 结构体上持有 DB 句柄
  • 通过 app 构造函数注入
  • 注册使用注入句柄的路由
  • 将稳定的 store 基础元语排除在业务逻辑之外

第一步 — 定义持有依赖的 handler 结构体

Section titled “第一步 — 定义持有依赖的 handler 结构体”
internal/handler/item.go
package handler
import (
"database/sql"
"net/http"
"github.com/spcent/plumego/contract"
)
type ItemHandler struct {
DB *sql.DB
}
type itemResponse struct {
ID int `json:"id"`
Name string `json:"name"`
}
func (h *ItemHandler) Get(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeRequired).
Detail("field", "id").
Message("id is required").
Build())
return
}
var item itemResponse
err := h.DB.QueryRowContext(r.Context(),
"SELECT id, name FROM items WHERE id = $1", id,
).Scan(&item.ID, &item.Name)
if err == sql.ErrNoRows {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeNotFound).
Message("item not found").
Build())
return
}
if err != nil {
_ = contract.WriteError(w, r, contract.NewErrorBuilder().
Type(contract.TypeInternal).
Message("database error").
Build())
return
}
_ = contract.WriteResponse(w, r, http.StatusOK, item, nil)
}

第二步 — 在 app 构造函数中打开连接

Section titled “第二步 — 在 app 构造函数中打开连接”
internal/app/app.go
import (
"database/sql"
_ "github.com/lib/pq" // 或你的驱动
)
type App struct {
Core *core.App
Cfg config.Config
DB *sql.DB
}
func New(cfg config.Config) (*App, error) {
db, err := sql.Open("postgres", cfg.DatabaseDSN)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("ping db: %w", err)
}
app := core.New(cfg.Core, core.AppDependencies{
Logger: plumelog.NewLogger(),
})
recoveryMw, err := recovery.Middleware(recovery.Config{Logger: app.Logger()})
if err != nil {
return nil, err
}
accesslogMw, err := accesslog.Middleware(accesslog.Config{Logger: app.Logger()})
if err != nil {
return nil, err
}
app.Use(requestid.Middleware())
app.Use(recoveryMw)
app.Use(accesslogMw)
return &App{Core: app, Cfg: cfg, DB: db}, nil
}

第三步 — 在路由注册时注入 handler

Section titled “第三步 — 在路由注册时注入 handler”
internal/app/routes.go
func (a *App) RegisterRoutes() error {
items := &handler.ItemHandler{DB: a.DB}
return a.Core.Get("/api/items", http.HandlerFunc(items.Get))
}

参考服务的 Start 方法通过 defer 调用 Shutdown。在那里关闭 DB:

func (a *App) Start() error {
defer a.DB.Close()
// ... 正常进行 Prepare、Server、ListenAndServe
}
  • Handler 结构体是边界:其字段是它拥有的全部依赖。
  • 无包级全局 DB 变量,意味着测试可以传入任何 *sql.DB — 包括用于单元测试的内存 SQLite DB。
  • 连接在启动时打开一次,在关闭时关闭一次 — 无每请求连接逻辑。
现象先检查
handler 看到 nil DB确认 app 构造函数打开连接,并且路由注册时传入 DB: a.DB
请求挂起或不响应取消使用带 r.Context()QueryRowContextQueryContextExecContext
测试难以隔离通过 handler 字段或 interface 传入 DB;避免包级全局变量
关闭时连接泄露在 app 生命周期中关闭一次 DB,不要每个请求关闭
领域逻辑直接导入存储基础元语storedatabase/sql wiring 保持在 handler/repository 边界

参考服务展示了数据库连接依赖注入的完整模式:

本地运行验证:

Terminal window
git clone https://github.com/spcent/plumego
cd plumego/reference/standard-service
go run .