time.Tick 会阻塞 goroutine,因其返回无缓冲 channel,for range 无法跳过未消费 tick;应改用 time.NewTicker 配合 select/default 或 context 控制,并对 reconnectDB 加锁防并发 panic。

time.Tick 会阻塞 goroutine,别直接在主流程里调用
很多人一上来就写 for range time.Tick(d) 然后在里面做数据库 ping,结果发现程序卡住、超时不触发、甚至 goroutine 泄漏。根本原因是 time.Tick 返回的是一个无缓冲 channel,每次发送都会阻塞,直到被接收;而 for range 本身不会“跳过”未消费的 tick —— 如果一次检测耗时超过 tick 间隔,后续的 tick 就会堆积并阻塞在 channel 发送端。
正确做法是用 select 配合 default 或带超时的接收,避免阻塞:
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
<p>for {
select {
case </p>
更稳妥的是直接用 time.AfterFunc 或循环中手动 sleep,避开 time.Tick 的 channel 阻塞风险。
重连逻辑必须加锁,否则并发 ping 可能 panic
Go 的 *sql.DB 本身是并发安全的,但它的底层连接池和健康状态不是原子更新的。如果你在多个 goroutine 里同时调用 reconnectDB()(比如两次 ping 都失败、又都触发重连),可能造成多次 sql.Open、旧连接未关闭、甚至 db.Close() 后又被误用。
用一个 sync.Mutex 包住重连入口即可:
var mu sync.Mutex
<p>func reconnectDB() {
mu.Lock()
defer mu.Unlock()</p><pre class="brush:php;toolbar:false;">// 检查是否已重连成功,避免重复操作
if db != nil {
if err := db.Ping(); err == nil {
return
}
}
newDB, err := sql.Open("mysql", dsn)
if err != nil {
log.Printf("failed to open new db: %v", err)
return
}
if err := newDB.Ping(); err != nil {
log.Printf("new db fails ping: %v", err)
return
}
// 原 db 存活则先 Close,注意:Close 不会立即中断活跃事务,但会阻止新请求
if db != nil {
db.Close()
}
db = newDB}
- 不要在
reconnectDB里直接赋值db = newDB前不做db.Ping()校验,否则可能把一个刚打开但尚未 ready 的*sql.DB暴露出去 -
db.Close()是线程安全的,但调用后不能再对原db调用任何方法(包括Ping),否则 panic
time.Tick 不适合长周期检测,改用 time.NewTicker + 显式控制
time.Tick 是包级函数,返回的 ticker 无法 Stop(除非你用反射黑进去),且没有上下文支持。实际项目中,你需要随时停掉检测(比如服务 shutdown),或根据数据库状态动态调整间隔(如连续失败 3 次后拉长到 30s)。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
所以应优先使用 time.NewTicker 并配合 context.Context:
func startHealthCheck(ctx context.Context, db *sql.DB, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
<pre class="brush:php;toolbar:false;">for {
select {
case <p>}</p>注意:ticker.Reset() 在 Go 1.15+ 是安全的,但在旧版本中若在 select 外调用可能 panic,务必确保只在 case 分支内调用。
sql.DB 自带连接池健康检测,别过度轮询
*sql.DB 默认会在每次获取连接时做轻量级校验(取决于驱动实现),并且支持 SetConnMaxLifetime、SetMaxOpenConns 等参数自动淘汰老化连接。很多场景下,你根本不需要自己写 ping 检测 —— 只要配置合理,应用层报错(如 "connection refused")时再重试即可。
真要加定时检测,建议:
- 间隔不低于 10s,太频繁反而增加数据库负担
- 仅在关键服务(如支付、订单)中启用,非核心模块靠错误重试更轻量
- 记录每次 ping 的耗时,用于监控连接质量,不只是通/断
- 避免在 init 函数或全局变量初始化阶段启动 ticker,容易导致测试难 mock、依赖混乱
最常被忽略的一点:ping 成功不代表事务能提交,也不代表从库已同步。真正的可用性检测,应该结合业务 SQL(比如 SELECT 1 或轻量 SELECT NOW())并捕获具体错误码,而不是只看 db.Ping() 的 bool 返回值。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










