直接硬编码数据库连接不靠谱,因生产环境主从库地址、账号、超时等参数常变,每次修改需重新编译上线;且读写分离逻辑耦合DAO层会导致扩展从库或调整流量时易出错。

为什么 Gin 项目里直接写死数据库连接不靠谱
因为生产环境的主库和从库地址、账号、超时参数经常变,硬编码在 database.Init() 里会导致每次改配置都要重新编译上线。更麻烦的是,读写分离逻辑(比如“SELECT 走从库,INSERT/UPDATE/DELETE 走主库”)如果耦合在 DAO 层,后期加个新从库或切流量比例就容易出错。
用配置文件驱动读写分离开关与节点列表
参考 settings.yaml 的结构,把数据库配置外置为多节点形式:
database:
write:
host: "mysql-master.example.com"
port: 3306
user: "app_rw"
password: "xxx"
read:
- host: "mysql-slave-1.example.com"
port: 3306
user: "app_ro"
password: "xxx"
- host: "mysql-slave-2.example.com"
port: 3306
user: "app_ro"
password: "xxx"
max_open_conns: 50
max_idle_conns: 20关键点:
-
read是数组,不是单个对象 —— 后续可直接用rand.Intn(len(readNodes))做简单轮询 - 必须区分
write和read字段名,避免和旧版单库配置冲突 - 不要在配置里写
read_only: true这类 MySQL 服务端参数,那是my.cnf该管的事
在 database.Init() 中按需初始化多个 *sql.DB 实例
别用一个 *sql.DB 对象硬扛读写,Gin 本身不干预 DB 层,但你要让 DAO 能明确拿到对应实例:
func Init() error {
cfg := config.Get()
<pre class='brush:php;toolbar:false;'>// 主库
writeDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
cfg.Database.Write.User, cfg.Database.Write.Password,
cfg.Database.Write.Host, cfg.Database.Write.Port, "your_db")
writeDB, err := sql.Open("mysql", writeDSN)
if err != nil { return err }
writeDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
writeDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
// 从库(随机选一个初始化,后续按需拨号)
var readDBs []*sql.DB
for _, node := range cfg.Database.Read {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
node.User, node.Password, node.Host, node.Port, "your_db")
db, err := sql.Open("mysql", dsn)
if err != nil { return err }
db.SetMaxOpenConns(cfg.Database.MaxOpenConns / len(cfg.Database.Read))
readDBs = append(readDBs, db)
}
// 挂到全局变量或依赖注入容器
globalWriteDB = writeDB
globalReadDBs = readDBs
return nil}
注意:
- 每个
*sql.DB都要单独调用SetMaxOpenConns,否则所有从库共享主库的连接池上限 - 从库连接池大小建议均分,比如总上限 50,两个从库就各设 25
- 别在
Init()里执行db.Ping()—— 启动时网络不可达会导致整个服务起不来,应改为懒加载 + 重试
DAO 层如何安全路由读请求到从库
最简方案是加一层函数封装,避免业务代码感知底层细节:
func GetProductByID(id string) (*Product, error) {
// 强制走从库:SELECT 类型操作默认用 readDB
db := getReadDB() // 内部实现:return globalReadDBs[rand.Intn(len(globalReadDBs))]
row := db.QueryRow("SELECT id,name,price FROM products WHERE id = ?", id)
// ...
}但要注意边界情况:
- 事务内不能走从库 —— 如果你用了
db.Begin(),后续所有查询必须用同一个*sql.Tx对象,此时忽略读写分离规则 - 强一致性读(如刚写完立刻查)必须走主库,建议显式提供
WithMaster()选项,而不是靠注释提醒 - 不要在 HTTP handler 里手动选
readDB,统一收口到 DAO 或 repository 层,否则 Gin 中间件无法统一拦截审计
真正容易被忽略的点是:配置热更新。YAML 文件改了,config.Get() 返回的对象不会自动刷新。如果要用动态切换读库权重或下线某台从库,得自己实现监听 fsnotify + 重建 *sql.DB 实例 + 平滑过渡,这部分远比初始化复杂得多。


















