A pgx Cheat Sheet for Pools Queries and Batches in Go
A pgx v5 cheat sheet: pool settings, Exec, Query, CollectRows, batches, transactions, COPY, errors, types and context, each snippet run against PostgreSQL 18.

A reference to keep open while you write pgx code: the calls, the signature shapes, the one line of warning each deserves. Every snippet below was run against PostgreSQL 18.6 from Go 1.25.6 with pgx v5.11.0, and the output of the whole program is under the code. The longer explanations are in the pgx tour and the articles it links; this page is the short form. It belongs to the database cornerstone.
Tested with Go 1.25.6, pgx v5.11.0, PostgreSQL 18.6 in Docker, macOS on Apple Silicon (arm64)
The program
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)
const dsn = "postgres://postgres:[email protected]:55432/lab?sslmode=disable&pool_max_conns=4&pool_max_conn_lifetime=30m&pool_health_check_period=1m"
type Item struct {
ID int64
Name string
Qty int
Notes *string
Tags []string
Added time.Time
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
ctx := context.Background()
// Pool
pool, err := pgxpool.New(ctx, dsn)
must(err)
defer pool.Close()
fmt.Println("pool: max", pool.Config().MaxConns, "| lifetime", pool.Config().MaxConnLifetime)
// Exec
_, err = pool.Exec(ctx, "drop table if exists sheet685")
must(err)
_, err = pool.Exec(ctx, "create table sheet685 (id bigint primary key, name text not null, qty int not null default 0, notes text, tags text[] not null default '{}', added timestamptz not null default now())")
must(err)
tag, err := pool.Exec(ctx, "insert into sheet685 (id, name, qty, tags) values ($1, $2, $3, $4)", 1, "bolt", 10, []string{"steel", "m6"})
must(err)
fmt.Println("exec:", tag, tag.RowsAffected())
// Named arguments
_, err = pool.Exec(ctx, "insert into sheet685 (id, name, qty) values (@id, @name, @qty)", pgx.NamedArgs{"id": 2, "name": "nut", "qty": 20})
must(err)
// QueryRow and Scan
var name string
var notes *string
must(pool.QueryRow(ctx, "select name, notes from sheet685 where id = $1", 1).Scan(&name, ¬es))
fmt.Println("queryrow:", name, "notes nil:", notes == nil)
// No rows
err = pool.QueryRow(ctx, "select name from sheet685 where id = 99").Scan(&name)
fmt.Println("no rows:", errors.Is(err, pgx.ErrNoRows))
// Collect rows
rows, _ := pool.Query(ctx, "select id, name, qty, notes, tags, added from sheet685 order by id")
items, err := pgx.CollectRows(rows, pgx.RowToStructByName[Item])
must(err)
fmt.Println("collect:", len(items), "items, first", items[0].Name, items[0].Tags)
// One row, strictly
rows, _ = pool.Query(ctx, "select name from sheet685 where id = $1", 2)
one, err := pgx.CollectExactlyOneRow(rows, pgx.RowTo[string])
must(err)
fmt.Println("exactly one:", one)
// Batch
b := &pgx.Batch{}
b.Queue("update sheet685 set qty = qty + 1 where id = $1", 1)
b.Queue("select qty from sheet685 where id = $1", 1)
br := pool.SendBatch(ctx, b)
_, err = br.Exec()
must(err)
var qty int
must(br.QueryRow().Scan(&qty))
must(br.Close())
fmt.Println("batch: qty is now", qty)
// Transaction
err = pgx.BeginFunc(ctx, pool, func(tx pgx.Tx) error {
if _, err := tx.Exec(ctx, "update sheet685 set qty = qty - 5 where id = 1"); err != nil {
return err
}
return errors.New("roll back")
})
must(pool.QueryRow(ctx, "select qty from sheet685 where id = 1").Scan(&qty))
fmt.Println("transaction:", err, "| qty still", qty)
// COPY
n, err := pool.CopyFrom(ctx, pgx.Identifier{"sheet685"}, []string{"id", "name"}, pgx.CopyFromRows([][]any{{10, "a"}, {11, "b"}}))
must(err)
fmt.Println("copy:", n, "rows")
// Errors
_, err = pool.Exec(ctx, "insert into sheet685 (id, name) values (1, 'dup')")
var pg *pgconn.PgError
if errors.As(err, &pg) {
fmt.Println("error:", pg.Code, pg.ConstraintName)
}
// Nullable and arrays
var text pgtype.Text
must(pool.QueryRow(ctx, "select notes from sheet685 where id = 1").Scan(&text))
var tags []string
must(pool.QueryRow(ctx, "select tags from sheet685 where id = 1").Scan(&tags))
fmt.Println("types: pgtype.Text valid", text.Valid, "| tags", tags)
// Any list
var found int
must(pool.QueryRow(ctx, "select count(*) from sheet685 where id = any($1)", []int64{1, 2, 99}).Scan(&found))
fmt.Println("any:", found)
// Context deadline
short, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
_, err = pool.Exec(short, "select pg_sleep(1)")
fmt.Println("deadline:", pgconn.Timeout(err))
// Acquire
conn, err := pool.Acquire(ctx)
must(err)
defer conn.Release()
var pid int
must(conn.QueryRow(ctx, "select pg_backend_pid()").Scan(&pid))
fmt.Println("acquire: one connection held, pool acquired =", pool.Stat().AcquiredConns())
}pool: max 4 | lifetime 30m0s
exec: INSERT 0 1 1
queryrow: bolt notes nil: true
no rows: true
collect: 2 items, first bolt [steel m6]
exactly one: nut
batch: qty is now 11
transaction: roll back | qty still 11
copy: 2 rows
error: 23505 sheet685_pkey
types: pgtype.Text valid false | tags [steel m6]
any: 2
deadline: true
acquire: one connection held, pool acquired = 1Connect
| Task | Code | Note |
|---|---|---|
| Create a pool | pgxpool.New(ctx, dsn) | One per process. defer pool.Close() |
| Settings in the URL | ...?pool_max_conns=4&pool_max_conn_lifetime=30m&pool_health_check_period=1m | The lab printed max 4, lifetime 30m0s |
| Read the config | pool.Config().MaxConns | ParseConfig(dsn) before creating to adjust hooks |
| One connection | c, err := pool.Acquire(ctx); defer c.Release() | For session state, LISTEN, advisory locks |
| Statistics | pool.Stat().AcquiredConns() | Export these as metrics |
| Single connection, no pool | pgx.Connect(ctx, dsn) | Not safe for concurrent use |
Write
| Task | Code | Note |
|---|---|---|
| Statement | tag, err := pool.Exec(ctx, "insert ... values ($1, $2)", a, b) | tag.RowsAffected(); the lab printed INSERT 0 1 |
| Named arguments | pool.Exec(ctx, "... values (@id, @name)", pgx.NamedArgs{"id": 2, "name": "nut"}) | @name rewritten to positional parameters |
| Several statements, one trip | b := &pgx.Batch{}; b.Queue(sql, args...); br := pool.SendBatch(ctx, b) | Read results in order, then br.Close(); the batch is one implicit transaction |
| Bulk load | pool.CopyFrom(ctx, pgx.Identifier{"t"}, cols, pgx.CopyFromRows(rows)) | All or nothing; no ON CONFLICT |
Read
| Task | Code | Note |
|---|---|---|
| One row | pool.QueryRow(ctx, sql, args...).Scan(&a, &b) | Releases the connection itself |
| No row | errors.Is(err, pgx.ErrNoRows) | The lab printed true |
| A list into structs | rows, _ := pool.Query(...); pgx.CollectRows(rows, pgx.RowToStructByName[T]) | Closes the rows; columns matched by name |
| By position | pgx.RowToStructByPos[T] | Field order must match the select list |
| One column | pgx.CollectRows(rows, pgx.RowTo[string]) | Slice of scalars |
| Exactly one row | pgx.CollectExactlyOneRow(rows, pgx.RowTo[string]) | ErrNoRows or ErrTooManyRows otherwise |
| Stream | pgx.ForEachRow(rows, []any{&a, &b}, func() error {...}) | No slice built |
| An IN list | ... where id = any($1) with []int64{1, 2, 99} | The lab found 2 of the 3 ids |
Transactions
| Task | Code | Note |
|---|---|---|
| Commit or roll back | pgx.BeginFunc(ctx, pool, func(tx pgx.Tx) error {...}) | Nil commits, an error or panic rolls back; the lab left qty at 11 |
| With options | pgx.BeginTxFunc(ctx, pool, pgx.TxOptions{IsoLevel: pgx.Serializable}, fn) | Retry on SQLSTATE 40001 |
| Savepoint | inner, _ := tx.Begin(ctx) | inner.Rollback(ctx) undoes only that part |
| Manual | tx, _ := pool.Begin(ctx); defer tx.Rollback(ctx); ...; tx.Commit(ctx) | Check the commit error |
Errors, types and context
| Task | Code | Note |
|---|---|---|
| Server error | var pg *pgconn.PgError; errors.As(err, &pg) | pg.Code, pg.ConstraintName; the lab printed 23505 sheet685_pkey |
| Timeout of any kind | pgconn.Timeout(err) | True for a context deadline; the lab printed true |
| Nullable column | *string, or pgtype.Text with Valid | A plain string cannot hold NULL |
| Array | []string for text[], []int32 for int4[] | NULL elements need pointers or pgtype wrappers |
| Deadline | ctx, cancel := context.WithTimeout(ctx, d) | Pass to every call; see context cancellation |
What the lab printed
The output confirms each row: the pool had four connections and a 30 minute lifetime; the first insert reported INSERT 0 1; a row with no notes scanned into a nil *string; a missing id gave ErrNoRows; two items were collected into structs with their tags; CollectExactlyOneRow returned nut; the batch raised the quantity to 11; a transaction that returned an error left it at 11; CopyFrom loaded 2 rows; a duplicate key gave 23505 with the constraint name; a pgtype.Text for a NULL note reported Valid false; the any list counted 2; a 100 millisecond deadline on a one second sleep was recognised by pgconn.Timeout; and Acquire held one connection.
Which read helper to use
The read calls overlap, and a short rule picks between them. Use QueryRow and Scan when you want one row and a fixed set of variables, such as a count or a single column. Use CollectRows with a struct function when you want a list that fits in memory, and RowToStructByName unless the query sits right beside the struct. Use CollectExactlyOneRow for a lookup by a unique key, since it turns "several rows" into an error instead of silently taking the first, which CollectOneRow does. Use ForEachRow, or a loop with rows.Next, for results too large to hold. And use Batch when the same request needs several independent statements, because it replaces several round trips with one.
Five rules that prevent most trouble
- One pool per process, sized from the database's budget, with lifetimes below any idle timeout on the path, as in pgxpool.
- A context with a deadline on every call.
- Let the helpers close things:
CollectRows,BeginFunc,defer Release, as in pgx mistakes. - Classify errors by SQLSTATE with
errors.As, never by message, as in pgx error codes. - Test against a real PostgreSQL, as in testing with pgx.
The complete API is in the pgx reference and pgxpool reference.


