Skip to content

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活跃维护
pgxpoolpgx 自带连接池(生产标配)活跃维护
pgx/v5/stdlib把 pgx 适配成 database/sql 驱动(注册名 pgx)活跃维护
lib/pq老牌 database/sql 驱动官方声明进入维护模式
sqlx / GORM标准接口之上的增强层 / ORM按团队选择
text
  ┌──────────────┐  原生 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 特色:

  1. context 贯穿始终:Query(ctx, …)、Begin(ctx)、pool.Acquire(ctx),超时取消由 context.WithTimeout 控制,取消会沿 libpq 协议真正终止服务端语句执行;
  2. 占位符是 $1:与 PostgreSQL 原生一致,没有驱动层的转译层;
  3. 连接池是标配组件:pgxpool 开箱即用,健康检查、最大生命周期、空闲回收参数齐备。

语法与用法 ​

安装与最小连接:

bash
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool
go
package 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)
}

连接池(服务端应用的默认形态):

go
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")

事务与批量:

go
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 批量导入:

go
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/pqpgx 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)让两者兼得:

go
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 参数:

text
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=require

pgxpool.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 等字段显式调整:

go
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 归还:

go
conn, err := pool.Acquire(ctx)
defer conn.Release()

$1 占位符与 Query/Scan ​

pgx 的查询接口族:

go
// 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 参数:

go
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」:

go
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 把多条语句攒成一个往返发送,高延迟链路(跨机房、云端跨区)的批量小语句提速显著:

go
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 中速度最快的批量写入方式:

go
// 内存行
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 的处理足够原生,三种姿势按需选择:

go
// 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 ​

go
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 解包:

go
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 err

PgError 携带 Code(SQLSTATE)、Message、Detail、ConstraintName 等字段,按 SQLSTATE 分支是跨语言一致的错误处理正道;约束名(ConstraintName)可以精确映射到业务语义(如「邮箱已注册」)。

上下文超时与取消 ​

context 是 Go 驱动层的「请求级 statement_timeout」:

go
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简单协议、文本传参兼容性兜底与调试
go
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、参数与耗时挂进日志或链路追踪,业务代码零改动:

go
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 之上提供结构映射:

go
// 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 服务 ​

go
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 批量导入 ​

go
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 重试 ​

go
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 连接与池参数 ​

go
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 订阅器 ​

go
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,否则会出现「缓存命中的是别的真实连接」类诡异报错。

最佳实践 ​

  1. 新项目 pgx v5 原生 + pgxpool;标准接口需求用 pgx stdlib 桥接。 不要再为新代码引入 lib/pq。
  2. 连接串显式声明 sslmode、connect_timeout、application_name 与 pool_ 参数。* 四类参数对应加密、快速失败、溯源、池容量,缺一不可。
  3. 事务用「立即 defer Rollback + 显式 Commit」模板。 异常路径零遗漏,是 Go 事务书写的标准姿势。
  4. 批量写入分档:Batch(中)/ CopyFrom(大)。 十万行以上一律 CopyFrom;导入前摘索引、导后重建并 ANALYZE。
  5. 错误处理按 pgconn.PgError.Code 分支。 23505/23503/40001/40P01 的语义翻译成领域错误(ErrDuplicate/ErrRetryable…),调用方不感知驱动类型。
  6. 用 RowToStructByName/CollectRows 替代长串 Scan。 列增减时 struct 映射比位置 Scan 健壮得多。
  7. JSONB 优先 []byte 与 map/struct 直传。 让 pgx 的 JSON 编解码器承担序列化,不要手工拼 JSON 字符串。
  8. 需要独占连接的场景显式 Acquire。 LISTEN、大对象、游标、会话级参数都走独占连接并 Release 归还。
  9. context 超时作为请求级防线,与服务端 statement_timeout 互补。 批量任务用独立 context,避免被请求生命周期误杀。
  10. GORM/sqlx 只做映射,不做连接管理。 池参数在 pgxpool 或 database/sql 层一处管好,禁止多层池叠加。
  11. 多实例部署先做连接总账。 pgxpool.MaxConns × 实例数 ≤ max_connections 余量,超出用 PgBouncer 收敛。
  12. 把 application_name 规范到每个服务。 pg_stat_activity 里的连接可溯源,是排查悬挂事务的第一线索。

下一节

继续学习:Node.js 连接 PostgreSQL

参考链接 ​