Go 连接 PostgreSQL
Go 生态访问 PostgreSQL 有两条主流路径:pgx v5(github.com/jackc/pgx,当前社区首选,原生接口性能更好、PostgreSQL 特性支持最全)与 lib/pq(github.com/lib/pq,database/sql 标准接口的老牌驱动,官方已进入维护模式)。pgx 既能以原生 API 直连(推荐),也能通过 stdlib 适配层以 database/sql 驱动的身份运行,因此新项目可以「pgx 原生为主、需要标准接口的场合降级兼容」,一条路径覆盖大多数团队。
Go 驱动层的两个鲜明特色贯穿全页:一切皆 context——每个查询、事务、连接获取都带 ctx,超时与取消是一等公民;编译期依赖极少——没有重量级连接管理框架,连接池(pgxpool)开箱即用,参数 $1 占位符直接对齐 PostgreSQL 原生语法。
本页按「连接与池 → 查询与扫描 → 事务 → 批量与 COPY → JSONB/NOTIFY → 错误处理 → ORM/增强层」推进,最后以 pgxpool + CopyFrom 的批量导入实战收尾。
前置知识
建议先阅读:Java 连接 PostgreSQL
基础概念
| 方案 | 定位 | 状态 |
|---|---|---|
pgx v5 原生 API | 首选:原生接口、Batch、CopyFrom、大对象、NOTIFY | 活跃维护 |
pgxpool | pgx 自带连接池(生产标配) | 活跃维护 |
pgx/v5/stdlib | 把 pgx 适配成 database/sql 驱动(注册名 pgx) | 活跃维护 |
lib/pq | 老牌 database/sql 驱动 | 官方声明进入维护模式 |
sqlx / GORM | 标准接口之上的增强层 / ORM | 按团队选择 |
┌──────────────┐ 原生 API(首选)
│ pgx v5 │─────────────────────────► PostgreSQL
│ + pgxpool │
└──────┬───────┘
│ stdlib 适配
┌──────┴───────┐
│ database/sql │ 标准接口(lib/pq、sqlx、GORM 的底层)
└──────────────┘选型结论:新项目 pgx 原生 + pgxpool;存量 database/sql 代码可以无缝换到 pgx 的 stdlib 驱动获得 pgx 的传输性能;lib/pq 只用于老项目维护。所有 API 演示基于 pgx v5。
三个贯穿性的 Go 特色:
- context 贯穿始终:
Query(ctx, …)、Begin(ctx)、pool.Acquire(ctx),超时取消由context.WithTimeout控制,取消会沿 libpq 协议真正终止服务端语句执行; - 占位符是
$1:与 PostgreSQL 原生一致,没有驱动层的转译层; - 连接池是标配组件:pgxpool 开箱即用,健康检查、最大生命周期、空闲回收参数齐备。
语法与用法
安装与最小连接:
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpoolpackage main
import (
"context"
"log"
"github.com/jackc/pgx/v5"
)
func main() {
ctx := context.Background()
// 单连接(脚本、工具)
conn, err := pgx.Connect(ctx,
"postgres://app_rw:secret@db1:5432/appdb?sslmode=require&application_name=svc-orders")
if err != nil {
log.Fatal(err)
}
defer conn.Close(ctx)
var name string
err = conn.QueryRow(ctx,
"SELECT name FROM users WHERE id = $1", 42).Scan(&name)
if err != nil {
log.Fatal(err)
}
log.Println(name)
}连接池(服务端应用的默认形态):
import "github.com/jackc/pgx/v5/pgxpool"
pool, err := pgxpool.New(ctx,
"postgres://app_rw:secret@db1:5432/appdb?pool_max_conns=20")
// 查询接口与单连接一致
rows, err := pool.Query(ctx, "SELECT id, name FROM users WHERE status = $1", "active")事务与批量:
tx, err := pool.Begin(ctx)
if err != nil { return err }
defer tx.Rollback(ctx) // 已提交后 Rollback 是无害的空操作
_, err = tx.Exec(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2", 100, 1)
_, err = tx.Exec(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2", 100, 2)
return tx.Commit(ctx)COPY 批量导入:
rows := [][]any{
{1, "alpha", 12.5},
{2, "beta", 7.25},
}
count, err := pool.CopyFrom(ctx,
pgx.Identifier{"stg_items"}, // 目标表
[]string{"id", "name", "price"}, // 列
pgx.CopyFromRows(rows), // 数据源
)详细说明
lib/pq 与 pgx 的取舍
lib/pq 是 database/sql 时代的标准选择,但官方仓库已明确进入维护模式(只修关键问题,不加特性),并推荐新项目使用 pgx。两者差异的实质:
| 维度 | lib/pq | pgx v5 原生 |
|---|---|---|
| 接口 | database/sql 标准接口 | 原生 API |
| 性能 | 一般(标准接口约束 + 文本协议为主) | 更快(原生协议、扩展查询、二进制传输) |
| PostgreSQL 特性 | COPY/NOTIFY 等支持有限 | CopyFrom/WaitForNotification/Batch 全支持 |
database/sql 生态 | 原生 | 通过 stdlib 适配层兼容 |
database/sql 标准接口的价值在于可替换驱动与庞大的中间件生态(sqlx、GORM 底层、监控埋点等)。pgx 的 stdlib 适配层(_ "github.com/jackc/pgx/v5/stdlib",驱动名 pgx)让两者兼得:
import (
"database/sql"
_ "github.com/jackc/pgx/v5/stdlib"
)
db, err := sql.Open("pgx", "postgres://app_rw:secret@db1:5432/appdb")连接字符串与 pgxpool
pgx 接受两种连接串:URL 形式与 keyword/value 形式,均透传 libpq 参数:
postgres://app_rw:secret@db1:5432/appdb?sslmode=require&connect_timeout=5&application_name=svc-orders
host=db1 port=5432 dbname=appdb user=app_rw password=secret sslmode=requirepgxpool.New(ctx, dsn) 直接建池;池参数可写进连接串(pool_max_conns、pool_min_conns、pool_max_conn_lifetime、pool_max_conn_idle_time、pool_health_check_period),也可以用 pgxpool.ParseConfig 后在 Config.MaxConns 等字段显式调整:
cfg, err := pgxpool.ParseConfig(dsn)
cfg.MaxConns = 20
cfg.MinConns = 4
cfg.MaxConnLifetime = 30 * time.Minute
cfg.MaxConnIdleTime = 5 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, cfg)池大小的算法与 Java/Hikari 一致:应用实例数 × MaxConns ≤ max_connections 的安全余量,多实例场景用 PgBouncer 收敛。需要独占连接的场景(LISTEN、大对象、游标跨查询)从池里 Acquire 出来用完 Release 归还:
conn, err := pool.Acquire(ctx)
defer conn.Release()$1 占位符与 Query/Scan
pgx 的查询接口族:
// Exec:无结果集(INSERT/UPDATE/DELETE/DDL)
ct, err := pool.Exec(ctx, "DELETE FROM sessions WHERE user_id = $1", uid)
log.Println(ct.RowsAffected())
// QueryRow:单行,Scan 逐列取值
var (
id int64
name string
price float64
)
err = pool.QueryRow(ctx,
"SELECT id, name, price FROM items WHERE id = $1", 1).
Scan(&id, &name, &price)
// Query:多行迭代
rows, err := pool.Query(ctx, "SELECT id, name FROM users WHERE status = $1", "active")
if err != nil { return err }
defer rows.Close()
for rows.Next() {
var id int64
var name string
if err := rows.Scan(&id, &name); err != nil { return err }
}
if err := rows.Err(); err != nil { return err } // 迭代期间的非数据错误v5 的加分项:结构体扫描。CollectRows + RowToStructByName 把列按名映射进 struct(字段名与列名一致或带 db tag),告别长串 Scan 参数:
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
}
rows, err := pool.Query(ctx, "SELECT id, name, email FROM users WHERE status = $1", "active")
users, err := pgx.CollectRows(rows, pgx.RowToStructByName[User])注意 Scan 的目标类型必须与列类型兼容(bigint→int64、numeric→float64/string/pgtype.Numeric),类型不匹配是 Go 驱动错误的高发区。
事务:Begin/Tx/Commit 与 defer 模式
pgx 的事务对象 pgx.Tx 的惯用法是「立即 defer Rollback」:
func transfer(pool *pgxpool.Pool, from, to int64, amount int64) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
// Commit 成功后再执行 Rollback 会返回 ErrTxClosed,作为兜底无害
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx,
"UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1",
amount, from); err != nil {
return err
}
if _, err := tx.Exec(ctx,
"UPDATE accounts SET balance = balance + $1 WHERE id = $2",
amount, to); err != nil {
return err
}
return tx.Commit(ctx)
}defer tx.Rollback(ctx) 的语义是「没提交就回滚」,让异常路径不需要逐个分支处理;提交点之后的事务选项(隔离级别)通过 pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead}) 声明。pgx.BeginFunc 还提供函数式封装(进入函数即事务、返回即提交、panic 即回滚)。
Batch:攒批发送
Batch 把多条语句攒成一个往返发送,高延迟链路(跨机房、云端跨区)的批量小语句提速显著:
batch := &pgx.Batch{}
for _, it := range items {
batch.Queue("INSERT INTO items (id, name) VALUES ($1, $2)", it.ID, it.Name)
}
br := pool.SendBatch(ctx, batch)
defer br.Close()
for range items {
if _, err := br.Exec(); err != nil {
return err // 逐条检查结果
}
}Batch 与事务正交:需要原子时把 SendBatch 换成 tx.SendBatch。注意 Batch 是「攒批发送、串行执行」,与真正的服务端并行无关。
CopyFrom:Go 侧最快的写入通道
CopyFrom 走 COPY 二进制协议,是 pgx 中速度最快的批量写入方式:
// 内存行
count, err := pool.CopyFrom(ctx,
pgx.Identifier{"stg_orders"},
[]string{"order_id", "user_id", "amount", "created_at"},
pgx.CopyFromRows(orderRows),
)
// 流式数据源(实现 pgx.CopyFromSource 接口,边生成边送)
type csvSource struct {
r *csv.Reader
}
func (s *csvSource) Next() bool { return s.r.Scan() == nil }
func (s *csvSource) Values() ([]any, error) { /* 行转换 */ }
func (s *csvSource) Err() error { return nil }- 目标表名用
pgx.Identifier{schema, table}包裹,自动加引号防注入; - 源数据必须是 Go 原生值(int64/string/float64/[]byte/time.Time…),由 COPY 二进制协议序列化,不走 SQL 文本;
- 失败时整批回滚(COPY 是单事务),超大导入按行数切片;
- 与「摘索引 → 导入 → 重建索引 → ANALYZE」的性能清单完全兼容(见
\copy一页)。
JSONB 的写入与读取
pgx v5 对 JSONB 的处理足够原生,三种姿势按需选择:
// 1) []byte / json.RawMessage:原样传输,最直观
payload := []byte(`{"kind":"click","n":3}`)
_, err := pool.Exec(ctx, "INSERT INTO events (payload) VALUES ($1)", payload)
// 2) 任意 Go 值:pgx 的 JSON 编解码器经 encoding/json 自动处理
_, err = pool.Exec(ctx, "INSERT INTO events (payload) VALUES ($1)",
map[string]any{"kind": "click", "n": 3})
// 3) 读取回 struct / map
var ev map[string]any
err = pool.QueryRow(ctx, "SELECT payload FROM events WHERE id = $1", 1).Scan(&ev)原理上 pgx 为 JSON/JSONB 注册了默认编解码器:Go 侧任意可 JSON 序列化的值都能写入,读取时目标类型决定反序列化结果([]byte 拿原文、struct/map 拿结构)。要精确控制则注册自定义类型编解码器(见 pgx 文档的 custom types 章节)。
LISTEN/NOTIFY:WaitForNotification
conn, err := pool.Acquire(ctx) // 独占连接
defer conn.Release()
_, err = conn.Exec(ctx, "LISTEN order_events")
for {
n, err := conn.Conn().WaitForNotification(ctx)
if err != nil {
return err // ctx 取消或连接故障
}
log.Printf("channel=%s payload=%s", n.Channel, n.Payload)
}WaitForNotification 阻塞等待推送(不再需要 JDBC 式的轮询),ctx 超时/取消即退出。语义与《Java 连接 PostgreSQL》的 NOTIFY 一节一致:独占连接、事务提交后才可见、无持久化重放——适合协作信号,不适合业务总线。
错误处理:pgconn.PgError 与 SQLSTATE
pgx 的数据库错误统一通过 *pgconn.PgError 暴露,标准 errors.As 解包:
import (
"errors"
"github.com/jackc/pgx/v5/pgconn"
)
_, err := pool.Exec(ctx, "INSERT INTO users (email) VALUES ($1)", email)
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case "23505": // unique_violation
return ErrDuplicate
case "23503": // foreign_key_violation
return ErrMissingRef
case "40001", "40P01": // serialization_failure / deadlock_detected
return ErrRetryable
default:
return fmt.Errorf("db error %s: %s", pgErr.Code, pgErr.Message)
}
}
return errPgError 携带 Code(SQLSTATE)、Message、Detail、ConstraintName 等字段,按 SQLSTATE 分支是跨语言一致的错误处理正道;约束名(ConstraintName)可以精确映射到业务语义(如「邮箱已注册」)。
上下文超时与取消
context 是 Go 驱动层的「请求级 statement_timeout」:
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
rows, err := pool.Query(ctx, "SELECT … ") // 超时后取消超时触发取消时,pgx 会向服务器发送取消协议消息,服务端的语句执行也随之终止(不是客户端单方面放弃)。它与服务端 statement_timeout、lock_timeout 是三层互补的防线:网络层(context)、语句层(statement_timeout)、锁层(lock_timeout)。事务里的每条语句都应共享同一个带超时的 ctx,避免「事务里一半语句超时一半还在跑」的中间态。
QueryExecMode:协议形态的选择
pgx v5 的每次查询都有「怎么发送」的形态选择,由 DefaultQueryExecMode 控制:
| 模式 | 行为 | 适用 |
|---|---|---|
QueryExecModeCacheStatement(默认) | 首次 PREPARE + DESCRIBE 后缓存,参数二进制传输 | 直连 / session 池化,通用默认 |
QueryExecModeCacheDescribe | 缓存参数描述、每次 EXECUTE | 连接归属频繁漂移的环境 |
QueryExecModeExec | 不缓存语句,每轮完整扩展协议 | 配合 PgBouncer transaction 模式 |
QueryExecModeSimpleProtocol | 简单协议、文本传参 | 兼容性兜底与调试 |
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return err
}
// PgBouncer transaction 模式下:禁用语句缓存,改走 Exec
cfg.ConnConfig.DefaultQueryExecMode = pgx.QueryExecModeExec
pool, err := pgxpool.NewWithConfig(ctx, cfg)默认模式对直连最优;上 PgBouncer transaction 模式后,缓存的语句会「挂」在别的真实连接上失效,显式切到 QueryExecModeExec 是社区通行做法——这条与 Java 的 prepareThreshold、PHP 的 EMULATE_PREPARES、Node 的命名语句是同一问题的四种语言表达。
可观测性:Tracer 与慢查询定位
pgx v5 内建 Tracer 接口(QueryTracer 的 TraceQueryStart/TraceQueryEnd 钩子),把每次查询的 SQL、参数与耗时挂进日志或链路追踪,业务代码零改动:
type queryStartKey struct{}
type slowLogTracer struct{ threshold time.Duration }
func (t slowLogTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
return context.WithValue(ctx, queryStartKey{}, time.Now())
}
func (t slowLogTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
if start, ok := ctx.Value(queryStartKey{}).(time.Time); ok {
if elapsed := time.Since(start); elapsed > t.threshold {
slog.Warn("slow query", "took", elapsed, "err", data.Err)
}
}
}
cfg, err := pgxpool.ParseConfig(dsn)
if err != nil { log.Fatal(err) }
cfg.ConnConfig.Tracer = slowLogTracer{threshold: 200 * time.Millisecond}
pool, err := pgxpool.NewWithConfig(context.Background(), cfg)OpenTelemetry 场景用社区集成(如 otelpgx)把查询 span 接进链路,同样通过 cfg.ConnConfig.Tracer 挂载。服务端侧的配合手段——pg_stat_statements 找 TOP SQL、log_min_duration_statement 兜底记录——与 Tracer 构成「应用视角 + 数据库视角」的互补排查组合。
一个边界要清楚:Tracer 反映的是驱动发出时的形态,Batch 与 pipeline 的多个语句在追踪层会合并;需要逐语句的耗时归因时,以 pg_stat_statements 的服务端聚合为准。
GORM 与 sqlx
增强层在 database/sql 之上提供结构映射:
// GORM(底层驱动 postgres)
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=db1 user=app_rw password=secret dbname=appdb port=5432 sslmode=require"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.Exec("SET search_path TO tenant_a,public")
// sqlx(配合 pgx stdlib 驱动)
import _ "github.com/jackc/pgx/v5/stdlib"
import "github.com/jmoiron/sqlx"
db, err := sqlx.Connect("pgx", "postgres://app_rw:secret@db1:5432/appdb")
var users []User
err = db.Select(&users, "SELECT id, name, email FROM users WHERE status = $1", "active")GORM 的 Postgres 方言覆盖 UUID、JSONB、数组类型;gorm.Open 后建议显式设置 SetMaxOpenConns 等池参数(GORM 暴露的是 database/sql 池接口)。sqlx 的价值是轻量的结构体映射;两者都不要在 ORM 层再叠一层「连接池管理」,pgxpool/标准池一处管好。
实战示例
示例一:pgxpool 标准 CRUD 服务
package repo
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type OrderRepo struct{ pool *pgxpool.Pool }
func (r *OrderRepo) Create(ctx context.Context, o *Order) error {
return r.pool.QueryRow(ctx,
"INSERT INTO orders (user_id, amount) VALUES ($1, $2) RETURNING id, created_at",
o.UserID, o.Amount,
).Scan(&o.ID, &o.CreatedAt)
}
func (r *OrderRepo) Get(ctx context.Context, id int64) (*Order, error) {
o := &Order{}
err := r.pool.QueryRow(ctx,
"SELECT id, user_id, amount, created_at FROM orders WHERE id = $1", id,
).Scan(&o.ID, &o.UserID, &o.Amount, &o.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound // 把驱动错误翻译成领域错误
}
return o, err
}示例二:pgxpool + CopyFrom 批量导入
func importOrders(pool *pgxpool.Pool, csvPath string) (int64, error) {
f, err := os.Open(csvPath)
if err != nil { return 0, err }
defer f.Close()
rd := csv.NewReader(f)
rd.ReuseRecord = true
var total int64
for {
var batch [][]any
// 每批 5 万行
for i := 0; i < 50_000; i++ {
rec, err := rd.Read()
if err == io.EOF { break }
if err != nil { return total, err }
id, _ := strconv.ParseInt(rec[0], 10, 64)
amount, _ := strconv.ParseFloat(rec[2], 64)
batch = append(batch, []any{id, rec[1], amount})
}
if len(batch) == 0 { break }
n, err := pool.CopyFrom(context.Background(),
pgx.Identifier{"stg_orders"},
[]string{"order_id", "user_id", "amount"},
pgx.CopyFromRows(batch),
)
if err != nil { return total, err }
total += n
}
return total, nil
}示例三:事务 + SQLSTATE 重试
func withRetry(ctx context.Context, pool *pgxpool.Pool, fn func(pgx.Tx) error) error {
for attempt := 1; ; attempt++ {
tx, err := pool.Begin(ctx)
if err != nil { return err }
defer tx.Rollback(ctx)
if err := fn(tx); err == nil {
return tx.Commit(ctx)
} else {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) &&
(pgErr.Code == "40001" || pgErr.Code == "40P01") && attempt < 3 {
time.Sleep(time.Duration(attempt*50) * time.Millisecond)
continue // 可重试:退避后重来
}
return err
}
}
}示例四:GORM 连接与池参数
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=db1 user=app_rw password=secret dbname=appdb port=5432 sslmode=require"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(20)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(30 * time.Minute)示例五:NOTIFY 订阅器
func listenEvents(pool *pgxpool.Pool) error {
conn, err := pool.Acquire(context.Background())
if err != nil { return err }
defer conn.Release()
if _, err := conn.Exec(context.Background(),
"LISTEN order_events"); err != nil {
return err
}
for {
n, err := conn.Conn().WaitForNotification(context.Background())
if err != nil { return err }
log.Printf("event: %s", n.Payload)
}
}注意事项
lib/pq 已进入维护模式。 新项目直接 pgx;存量 database/sql 代码改用 pgx stdlib 驱动(驱动名从 postgres 换成 pgx),行为基本兼容且性能受益。
Scan 的类型必须与列类型匹配。 bigint→int64、numeric→string/float64/pgtype.Numeric、timestamptz→time.Time;类型错配的运行时 panic 是 Go 驱动最高频的错误。
pgxpool 不是共享连接的黑盒。 需要独占连接的场景(LISTEN、named cursor、大对象、会话级 SET)必须 Acquire 独占,用普通 pool.Exec 混用会出现「SET 落在别的连接」类诡异问题。
context 取消会终止服务端语句。 这是优点也是责任:Web 请求断开时长查询会被取消,批量任务要传递不受请求生命周期控制的 context(context.Background() 派生),避免「客户端刷新页面导致导入半途而废」。
CopyFrom 只接受 Go 原生值。 传 int(非 int64)、*string 等类型需要先转换;COPY 是单事务,失败整批回滚,超大导入切片分批。
Batch 攒批的内存与超时。 千万级 Queue 会把参数全攒在内存里;大批量按事务切片。Batch 内语句串行执行,依赖前条结果的逻辑要用 RETURNING 拆开。
SQLSTATE 分支要写全可重试码。 只认 40001/40P01 做重试,其余错误向上抛;无限重试网络类错误要加退避与上限。
池大小做连接总账。 实例数 × MaxConns 与 PostgreSQL max_connections 对账;多实例上 PgBouncer 时应用池相应收小,两层池的真实并发才是数据库承受的负载。
事务里的 ctx 共享同一个超时。 每条语句单独派生超时会出现事务内部分语句已超时的中间态;事务级超时一次派生、贯穿到底。
QueryExecMode 与池化形态要匹配。 默认缓存语句模式直连最优;PgBouncer transaction 模式下显式切 QueryExecModeExec,否则会出现「缓存命中的是别的真实连接」类诡异报错。
最佳实践
- 新项目 pgx v5 原生 + pgxpool;标准接口需求用 pgx stdlib 桥接。 不要再为新代码引入 lib/pq。
- 连接串显式声明 sslmode、connect_timeout、application_name 与 pool_ 参数。* 四类参数对应加密、快速失败、溯源、池容量,缺一不可。
- 事务用「立即 defer Rollback + 显式 Commit」模板。 异常路径零遗漏,是 Go 事务书写的标准姿势。
- 批量写入分档:Batch(中)/ CopyFrom(大)。 十万行以上一律 CopyFrom;导入前摘索引、导后重建并 ANALYZE。
- 错误处理按 pgconn.PgError.Code 分支。 23505/23503/40001/40P01 的语义翻译成领域错误(ErrDuplicate/ErrRetryable…),调用方不感知驱动类型。
- 用 RowToStructByName/CollectRows 替代长串 Scan。 列增减时 struct 映射比位置 Scan 健壮得多。
- JSONB 优先 []byte 与 map/struct 直传。 让 pgx 的 JSON 编解码器承担序列化,不要手工拼 JSON 字符串。
- 需要独占连接的场景显式 Acquire。 LISTEN、大对象、游标、会话级参数都走独占连接并 Release 归还。
- context 超时作为请求级防线,与服务端 statement_timeout 互补。 批量任务用独立 context,避免被请求生命周期误杀。
- GORM/sqlx 只做映射,不做连接管理。 池参数在 pgxpool 或 database/sql 层一处管好,禁止多层池叠加。
- 多实例部署先做连接总账。 pgxpool.MaxConns × 实例数 ≤ max_connections 余量,超出用 PgBouncer 收敛。
- 把 application_name 规范到每个服务。 pg_stat_activity 里的连接可溯源,是排查悬挂事务的第一线索。
下一节