How to Use pgx for PostgreSQL in Go (Native Driver)
You are building a service that talks to PostgreSQL. You started with database/sql because it is the standard library. It works for simple queries. Then you hit a wall. You need to bulk insert ten thousand rows and the loop of individual inserts takes forever. You have a JSONB column and you are wrestling with manual marshaling. You want to use Postgres-specific features like COPY or full-text search vectors, but the standard interface hides them behind reflection and generic byte slices.
pgx is the solution. It is a native PostgreSQL driver written in pure Go. It speaks the wire protocol directly, offers zero-reflection scanning, and exposes the full feature set of PostgreSQL. It is the default recommendation for Go developers who need performance and deep database integration.
What pgx actually is
pgx is a driver that implements the PostgreSQL protocol in Go. Unlike older drivers that wrap C libraries, pgx has no external dependencies. This means you can cross-compile your application to any platform without installing Postgres client headers. It also means better security and simpler deployment.
The driver provides two ways to interact with the database. You can use the direct pgx API, which gives you full access to Postgres features and high performance. Or you can use pgx as a backend for database/sql, which lets you keep the standard library interface while gaining the speed of a native driver. The direct API is where pgx shines. It allows you to map complex types, run bulk operations, and inspect query plans without abstraction tax.
Go code follows strict formatting conventions. Run gofmt on your files. The community expects it. Arguments about indentation or brace placement are settled by the tool. Most editors run gofmt on save. Trust the formatter so you can focus on logic.
Minimal connection and query
Here is the bare minimum to connect to a database and run a query. You get a single connection object, execute a statement, and scan the result.
package main
import (
"context"
"log"
"github.com/jackc/pgx/v5"
)
func main() {
// Context carries cancellation and deadlines. Always pass it to database calls.
ctx := context.Background()
// Connect opens a TCP connection and performs the PostgreSQL handshake.
// The connection string format matches libpq for familiarity.
conn, err := pgx.Connect(ctx, "host=localhost port=5432 user=postgres password=secret dbname=pgx_test")
if err != nil {
log.Fatalf("connection failed: %v", err)
}
// Close releases the connection. Always defer close for single-use connections.
defer conn.Close(ctx)
var result string
// QueryRow executes a query expected to return a single row.
// $1 is a positional parameter; pgx binds the type automatically.
err = conn.QueryRow(ctx, "SELECT $1::text", "hello").Scan(&result)
if err != nil {
log.Fatalf("query failed: %v", err)
}
log.Println(result)
}
The code imports github.com/jackc/pgx/v5. Install the package with go get github.com/jackc/pgx/v5. The pgx.Connect function dials the server, authenticates the user, and returns a *pgx.Conn. This object represents one physical connection. You pass a context to control timeouts and cancellation. The QueryRow method sends the query and returns a row scanner. Calling Scan writes the data into the provided pointer. If the query returns no rows, Scan returns an error. Error handling in Go is explicit. The pattern if err != nil { return err } is verbose by design. The community accepts the boilerplate because it makes failure paths visible. Do not swallow errors or panic on database failures.
How the connection works
When you call pgx.Connect, the library opens a socket to the database server. It negotiates the protocol version and sends authentication credentials. The server responds with a ready state. The returned connection holds session state like the current transaction, search path, and timezone settings.
Every query you run goes through this connection. The connection can only handle one query at a time. If you try to run two queries concurrently on the same *pgx.Conn, the second one will block or fail. This is a common source of confusion. A single connection is not a pool. For concurrent workloads, you need a pool of connections.
The context.Context parameter is the first argument to almost every pgx method. This is a Go convention. The context carries deadlines and cancellation signals. If the context is cancelled, pgx stops waiting for the database and returns an error. Functions that take a context should respect cancellation and deadlines. Name the context parameter ctx. This is the universal convention in Go codebases.
Mapping rows to structs
Real applications map database rows to Go structs. pgx excels here with its pgtype package. The package provides types that handle Postgres-specific data like arrays, JSONB, and nullable values without reflection.
Here is how you scan a row into a struct using pgx types.
package main
import (
"context"
"log"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
// User maps to a table row.
// pgtype.Text handles null strings gracefully without extra boilerplate.
type User struct {
ID int32
Name string
Email pgtype.Text
CreatedAt time.Time
}
// FetchUser retrieves a user by ID and scans directly into the struct fields.
// pgx matches columns to arguments by position, so the order must align with the SELECT.
func FetchUser(ctx context.Context, conn *pgx.Conn, id int32) (*User, error) {
var u User
err := conn.QueryRow(ctx, "SELECT id, name, email, created_at FROM users WHERE id = $1", id).Scan(
&u.ID,
&u.Name,
&u.Email,
&u.CreatedAt,
)
if err != nil {
// Return the error; the caller decides whether to log or wrap it.
return nil, err
}
return &u, nil
}
The User struct uses pgtype.Text for the email field. This type tracks whether the value is null. If the database returns NULL, Email.Valid will be false and Email.String will be empty. You avoid the sql.NullString wrapper and get better performance because pgx can scan directly into the type. The Scan method takes pointers to the struct fields. The order of arguments must match the order of columns in the SELECT statement. pgx does not use reflection to match names by default. It scans by position. This is faster and safer because a typo in a column name causes a compile-time error or a clear runtime mismatch rather than a silent misalignment.
Pitfalls and common errors
The biggest mistake is treating a single connection like a pool. If you call pgx.Connect inside an HTTP handler, you create a new connection for every request. The database server has a limit on concurrent connections. You will hit that limit quickly. The server will reject new connections with too many connections for role. Use pgxpool for servers. pgxpool manages a set of connections and hands them out as needed. You check out a connection, use it, and return it to the pool.
Another trap is scanning NULL values into non-nullable types. If a column contains NULL and you scan into a string, the operation fails. The error message reads cannot scan NULL into type string. Use a nullable type like pgtype.Text or pgtype.Int4 for columns that can be null. Check the Valid field after scanning to handle nulls explicitly.
Goroutine leaks happen when you start a query and forget to close the result set. The Query method returns a pgx.Rows object that holds resources. You must call Close on the rows when you are done. If you use QueryRow, the driver handles closing automatically. The worst goroutine bug is the one that never logs. Leaks accumulate until the application runs out of file descriptors or memory. Always close resources.
The compiler rejects code that violates type rules. If you pass a string where an integer is expected, you get cannot use "value" (untyped string constant) as int32 value in argument. If you forget to import a package, you get undefined: pkg. If you import a package and do not use it, you get imported and not used. Go requires all imports to be used. This keeps code clean and prevents dead dependencies.
Decision matrix
Pick the tool that matches your concurrency model and feature needs.
Use pgxpool when your application serves concurrent requests and needs to manage a set of database connections efficiently.
Use the pgx direct API when you are writing a script, a migration tool, or a background worker that requires a single dedicated connection.
Use pgx wrapped in database/sql when your architecture depends on the standard library interface and you want to swap in pgx for performance without rewriting query logic.
Use raw database/sql with a generic driver when database portability is a requirement and you are willing to accept the overhead of reflection and limited type support.
Where to go next
- How to Execute Queries with database/sql in Go
- Fix: "sql: no rows in result set" in Go
- How to Use ent for Database Schema and Code Generation
Pool connections. Close resources. Respect the context. pgx is the specialist. Use it when you need the full power of PostgreSQL.