连接数据库
本指南展示如何使用显式构造函数注入将数据库连接注入 Plumego handler — 与参考服务对所有依赖使用的模式相同。
边界原理请参见 Store Primer。
- 在 handler 结构体上持有 DB 句柄
- 通过 app 构造函数注入
- 注册使用注入句柄的路由
- 将稳定的
store基础元语排除在业务逻辑之外
第一步 — 定义持有依赖的 handler 结构体
Section titled “第一步 — 定义持有依赖的 handler 结构体”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 构造函数中打开连接”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”func (a *App) RegisterRoutes() error { items := &handler.ItemHandler{DB: a.DB}
return a.Core.Get("/api/items", http.HandlerFunc(items.Get))}第四步 — 关闭时关闭连接
Section titled “第四步 — 关闭时关闭连接”参考服务的 Start 方法通过 defer 调用 Shutdown。在那里关闭 DB:
func (a *App) Start() error { defer a.DB.Close() // ... 正常进行 Prepare、Server、ListenAndServe}这种模式带来什么
Section titled “这种模式带来什么”- Handler 结构体是边界:其字段是它拥有的全部依赖。
- 无包级全局
DB变量,意味着测试可以传入任何*sql.DB— 包括用于单元测试的内存 SQLite DB。 - 连接在启动时打开一次,在关闭时关闭一次 — 无每请求连接逻辑。
如果没有按预期工作
Section titled “如果没有按预期工作”| 现象 | 先检查 |
|---|---|
| handler 看到 nil DB | 确认 app 构造函数打开连接,并且路由注册时传入 DB: a.DB |
| 请求挂起或不响应取消 | 使用带 r.Context() 的 QueryRowContext、QueryContext 或 ExecContext |
| 测试难以隔离 | 通过 handler 字段或 interface 传入 DB;避免包级全局变量 |
| 关闭时连接泄露 | 在 app 生命周期中关闭一次 DB,不要每个请求关闭 |
| 领域逻辑直接导入存储基础元语 | 将 store 和 database/sql wiring 保持在 handler/repository 边界 |
参考应用中的完整示例
Section titled “参考应用中的完整示例”参考服务展示了数据库连接依赖注入的完整模式:
reference/standard-service/internal/app/app.go— 构造函数打开 DB 并注入 handlerreference/standard-service/internal/app/routes.go— 传入已填充的 handler 结构体reference/standard-service/main.go— 启动顺序:打开 → 注入 → 运行 → defer 关闭
本地运行验证:
git clone https://github.com/spcent/plumegocd plumego/reference/standard-servicego run .