
本文介绍如何在 go 应用中避免硬编码数据库连接字符串,通过结构化配置、单例连接池和依赖注入方式统一管理 mysql 连接,提升代码可维护性与并发安全性。
本文介绍如何在 go 应用中避免硬编码数据库连接字符串,通过结构化配置、单例连接池和依赖注入方式统一管理 mysql 连接,提升代码可维护性与并发安全性。
在 Go 开发中,将数据库连接凭据(如驱动名、DSN)重复写入每个函数不仅违反 DRY 原则,更会显著增加维护成本和出错风险。正确的做法不是简单地用全局常量“偷懒式复用”,而是结合 Go 的设计哲学——显式依赖传递 + 单次初始化连接池 + 结构化配置,实现真正可控、可测试、可扩展的依赖管理。
✅ 推荐方案:配置结构体 + 初始化函数 + 依赖注入
首先,定义结构化配置,并在 database.go 中集中管理:
// database.go
package db
import (
"database/sql"
"fmt"
"log"
_ "github.com/go-sql-driver/mysql" // MySQL 驱动
)
type Config struct {
Driver string
User string
Password string
Host string
Port string
Database string
}
var DefaultConfig = Config{
Driver: "mysql",
User: "user",
Password: "password",
Host: "127.0.0.1",
Port: "3306",
Database: "hello",
}
func (c Config) DSN() string {
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", c.User, c.Password, c.Host, c.Port, c.Database)
}
// 全局连接池(仅初始化一次)
var DB *sql.DB
var err error
func Init() (*sql.DB, error) {
if DB != nil {
return DB, nil
}
DB, err = sql.Open(DefaultConfig.Driver, DefaultConfig.DSN())
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// 可选:设置连接池参数(强烈建议)
DB.SetMaxOpenConns(20)
DB.SetMaxIdleConns(10)
DB.SetConnMaxLifetime(60 * time.Second)
// 验证连接有效性
if err = DB.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return DB, nil
}⚠️ 注意:sql.Open 并不真正建立连接,它只是返回一个连接池句柄;务必调用 DB.Ping() 进行首次健康检查。
然后,在业务逻辑中(如 readall.go),不自行创建/关闭 DB 实例,而是接收已初始化的 *sql.DB 作为参数——这正是依赖注入的核心体现:
// readall.go
package main
import (
"log"
"your-app/db" // 替换为实际模块路径
)
func ReadAll(db *sql.DB) error {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
log.Printf("User: %d - %s", id, name)
}
return rows.Err()
}
// 使用示例(main.go)
func main() {
db, err := db.Init()
if err != nil {
log.Fatal(err)
}
defer db.Close() // 整个应用生命周期结束时关闭(非每个函数)
if err := ReadAll(db); err != nil {
log.Fatal(err)
}
}❌ 不推荐的做法及原因
- 全局常量 DSN 字符串(如 const GlobalDB = "mysql:..."):虽解决了复用问题,但无法支持环境差异化(开发/测试/生产)、缺少类型安全、难以注入 mock 用于单元测试。
- 每个函数内 sql.Open() + defer db.Close():严重违背 database/sql 设计初衷。sql.DB 是线程安全的连接池,频繁 Open/Close 会导致连接泄漏、性能骤降,甚至被数据库拒绝连接。
- 未设置连接池参数:默认 MaxOpenConns=0(无限制),易触发数据库连接数超限。
✅ 进阶建议:支持多环境与 DI 框架集成
- 使用 viper 或 envconfig 加载 .env 或 YAML 配置,动态覆盖 DefaultConfig;
- 在大型项目中,可引入轻量 DI 容器(如 wire)自动生成依赖图,彻底消除手动传参;
- 单元测试时,可通过接口抽象(如 type DBQuerier interface { Query(...) })注入 mock 实现,解耦数据库依赖。
总之,Go 中的“依赖注入”不依赖框架魔法,而在于清晰的职责划分、显式的依赖声明和一次初始化、多次复用的连接池模式。从 Init() 开始,让每一处数据库操作都成为可追踪、可配置、可测试的可靠组件。

















