生产环境必须用pgx/v5,禁用已归档的lib/pq;推荐database/sql+pgx/v5驱动(导入_ "github.com/jackc/pgx/v5/database/sql")或直连pgxpool.Pool;连接串开发用sslmode=disable、生产强制sslmode=require;初始化后必须立即db.Ping()验证。

pgx/v5 是当前 Go 连 PostgreSQL 的事实标准,lib/pq 已归档不再维护。别再用 sql.Open("postgres", ...) 配 lib/pq —— 驱动注册错、SSL 默认行为不一致、NULL 处理易 panic、批量和事务支持弱,上线后问题一堆。
怎么选驱动和连接方式
生产环境必须用 pgx/v5,不是 pgx/v4,也不是 lib/pq。它原生支持连接池、context 取消、类型映射更准(比如 jsonb → json.RawMessage),还兼容 database/sql 接口。
两种主流用法:
- 用
database/sql+pgx驱动:导入_ "github.com/jackc/pgx/v5/database/sql",连接字符串格式为postgres://user:pass@host:port/db?sslmode=require;适合已有database/sql代码、想平滑迁移的项目 - 直连
pgxpool.Pool:导入"github.com/jackc/pgx/v5/pgxpool",用pgxpool.New()或pgxpool.NewWithConfig()初始化;能用Batch、CopyFrom、流式查询等高级功能,性能和控制力更强
本地开发可加 sslmode=disable,但上线必须设为 sslmode=require 或更严(如 verify-full)——PostgreSQL 12+ 默认拒接非 SSL 连接。
为什么 db.Ping() 成功但查询报错
常见现象:db.Ping() 返回 nil,但 QueryRow().Scan() panic 或报 cannot scan into *string from NULL。
立即学习“go语言免费学习笔记(深入)”;
根本原因不是连不上,而是类型不匹配或 NULL 处理失当:
- 字段允许 NULL?别用
*string,改用sql.NullString(database/sql方式)或pgtype.Text(pgx原生方式) - 时间字段用
*time.Time,数值字段用sql.NullInt64,避免直接扫进基础类型 -
rows.Scan()严格按 SQL 字段顺序,别靠名字;查SELECT name, created_at就得先扫&name再扫&createdAt,顺序错就全乱 - 用
rows.Columns()打印列名调试,尤其在有函数、别名、视图时
事务里执行多条语句总卡在第二条
错误信息:pq: current transaction is aborted, commands ignored until end of transaction block。
这是 PostgreSQL 强一致性机制:事务中任一语句出错(比如唯一键冲突、类型转换失败),整个事务立刻进入 aborted 状态,后续所有语句都被拒绝,直到显式 ROLLBACK。
正确做法:
- 所有
tx.Query()、tx.Exec()后必须立即检查err != nil - 一旦出错,立刻
tx.Rollback()并返回,不能继续调用其他tx方法 - 成功路径末尾必须
tx.Commit(),漏掉会导致连接卡死、连接池耗尽 - 别在事务里混用
pool.Query()—— 它走新连接,不在事务上下文中
推荐写法:tx, err := pool.Begin(ctx),出错直接 return err;成功最后 return tx.Commit(ctx)。
批量插入慢得像爬虫?
循环 100 次 Exec() 插 100 行,等于发 100 个网络包,延迟爆炸,还容易触发连接池饥饿。
pgx.Batch 是正解:
- 构造
batch := &pgx.Batch{},用batch.Queue()加入多条语句 - 用
pool.SendBatch(ctx, batch)一次性提交(注意是pool,不是tx) - 拿到
*pgx.BatchResults后,必须调.Close(),否则连接泄漏 - 单次 Batch 不要超过 1000 行——太大易触发 PostgreSQL 的内存压力或
max_stack_depth限制
如果字段含 NULL,确保 struct 字段是 *string 或 sql.NullString,否则 Scan() 会直接 panic。
context.WithTimeout() 传给 pool.Begin(),而不是依赖 SQL 层设置;还有连接字符串里的 user 和 password 必须从环境变量读(os.Getenv("DB_USER")),硬编码等于把密码塞进二进制里。


















