defer db.close() 不能直接写,因会关闭整个连接池导致后续操作失败;应只对 sql.conn、sql.rows、*sql.tx 等一次性资源 defer 关闭,并检查 close() 错误,按后申请先释放顺序排列 defer。

defer f.Close() 不等于安全释放数据库连接,直接 defer db.Close() 很可能提前断开连接或引发 panic。
defer db.Close() 为什么不能直接写
数据库连接对象(比如 *sql.DB)本身不是“一次性的句柄”,db.Close() 是关闭整个连接池,而非单次查询的资源。如果在函数开头就 defer db.Close(),后续所有 db.Query()、db.Exec() 都会失败,报错 "sql: database is closed"。
- 常见错误现象:
panic: sql: database is closed或查询返回nil+driver: bad connection - 正确做法是:只对从
db.Conn()显式获取的*sql.Conn延迟关闭,或对*sql.Rows、*sql.Tx等一次性资源 defer -
*sql.DB的生命周期应由上层统一管理,通常在整个应用启动时创建、退出时关闭
什么时候该 defer *sql.Rows 或 *sql.Tx
这两类对象必须显式关闭,且必须用 defer,否则容易泄漏连接或阻塞事务。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
*sql.Rows:每次db.Query()返回,必须defer rows.Close(),即使循环读取后已调用rows.Next()走到底,也要关——否则连接不会归还给池 -
*sql.Tx:开启事务后,无论Commit()还是Rollback(),都应在函数出口前确保执行,推荐写成:defer func() { if tx != nil { tx.Rollback() } }(),再在成功路径中显式tx.Commit() - 注意顺序:如果先
defer tx.Rollback()再tx.Commit(),会导致sql: transaction has already been committed or rolled back
defer 中调用 Close() 必须检查 error
Close() 方法本身可能返回 error(例如网络写缓冲失败、连接已断),而 defer 不提供捕获出口,直接忽略会掩盖问题。
- 错误写法:
defer rows.Close()—— error 完全丢失 - 正确写法:
defer func() { if err := rows.Close(); err != nil { log.Printf("rows.Close() failed: %v", err) } }() - 同理适用于
tx.Commit()、tx.Rollback(),尤其Rollback()在 commit 成功后调用也会返回 error - 高频服务中,建议把这类日志设为 warn 级别,避免被当成 debug 信息过滤掉
多个 defer 混用时的释放顺序陷阱
释放顺序错位会导致 “use of closed network connection” 或死锁,尤其在嵌套资源场景下。
- 典型错误:先
defer tx.Rollback(),再defer rows.Close()→ 实际执行时rows.Close()先于tx.Rollback(),但rows依赖tx,此时tx可能已被回收 - 正确顺序:先获取资源,再按“后申请、先释放”原则写 defer —— 即
rows在tx之后获取,就应先defer rows.Close(),再defer tx.Rollback() - 更稳妥做法:把 cleanup 逻辑封装进闭包,显式控制依赖,比如
defer cleanup(tx, rows),函数内按需调用
真正容易被忽略的是:defer 注册时参数已求值,但 *sql.Rows 和 *sql.Tx 是指针类型,defer 捕获的是地址而非状态;一旦它们提前被 close 或 commit,defer 执行时就会操作已释放资源——所以必须保证 defer 语句在资源有效期内注册,且不跨 goroutine 使用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










