How to Connect to PostgreSQL from Go

7 min readPostgreSQL

Go has one clear recommendation for PostgreSQL today: pgx. The older lib/pq driver still works and still appears in countless tutorials, but its README now states it is in maintenance mode and points new projects to pgx. This guide shows working code for both, explains the database/sql vs native-pgx interface choice that trips up most newcomers, and covers the configuration that actually matters: pooling, context timeouts, and TLS.

The options

DriverImport pathInterfacePoolingVersion (as of June 2026)
pgx (native)github.com/jackc/pgx/v5pgx-specific APIpgxpoolv5.10.0
pgx (stdlib)github.com/jackc/pgx/v5/stdlibdatabase/sqldatabase/sql poolv5.10.0
lib/pqgithub.com/lib/pqdatabase/sqldatabase/sql poolv1.10.9 (maintenance mode)

The decision tree:

  • New project, want the best PostgreSQL support: pgx with its native interface (pgxpool). You get PostgreSQL-specific types, the binary protocol, LISTEN/NOTIFY, COPY, and better performance.
  • You need database/sql (because an ORM, query builder, or migration tool requires the standard interface): use pgx through its stdlib adapter. You keep the standard API and still get a well-maintained driver.
  • Existing codebase already on lib/pq: it works and is stable, but no new features are coming. Migrating to pgx's stdlib adapter is usually a near drop-in change of the import and driver name.

A key point that confuses people: pgx is both a standalone driver with its own API and a database/sql driver. You pick which face to use. lib/pq only offers the database/sql face.

go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/pgxpool

For anything serving concurrent requests, use pgxpool, not a single pgx.Conn. A single connection is not safe for concurrent use by multiple goroutines; the pool hands out connections per query and is concurrency-safe.

package main
 
import (
	"context"
	"fmt"
	"log"
	"os"
 
	"github.com/jackc/pgx/v5/pgxpool"
)
 
func main() {
	ctx := context.Background()
 
	pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatalf("unable to create pool: %v", err)
	}
	defer pool.Close()
 
	rows, err := pool.Query(ctx,
		"SELECT id, name FROM customers WHERE region = $1",
		"EMEA",
	)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
 
	for rows.Next() {
		var id int
		var name string
		if err := rows.Scan(&id, &name); err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	if err := rows.Err(); err != nil { // errors surface here, not in the loop
		log.Fatal(err)
	}
}

Two things every Go database loop gets wrong at least once:

  • Always check rows.Err() after the loop. A failure mid-iteration ends the loop normally; the error only appears here. Skipping it silently truncates results.
  • Always defer rows.Close(). Leaked result sets hold a pooled connection until garbage collection, starving the pool.

Placeholders are $1, $2, ... -- PostgreSQL's native numbered parameters. Never build SQL with fmt.Sprintf; pass values as arguments so the driver parameterizes them.

For a single row, QueryRow plus Scan is cleaner:

var name string
err := pool.QueryRow(ctx,
	"SELECT name FROM customers WHERE id = $1", 42,
).Scan(&name)
if err != nil {
	if errors.Is(err, pgx.ErrNoRows) { // no row matched
		// handle "not found"
	}
	log.Fatal(err)
}

Connection string vs struct config

The DATABASE_URL form is the simplest:

postgres://app_user:secret@db.example.com:5432/sales?sslmode=verify-full

For programmatic control, parse it and adjust pool settings:

cfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
if err != nil {
	log.Fatal(err)
}
cfg.MaxConns = 10                       // default: greater of 4 or runtime.NumCPU()
cfg.MinConns = 0
cfg.MaxConnLifetime = time.Hour
cfg.MaxConnIdleTime = 30 * time.Minute
 
pool, err := pgxpool.NewWithConfig(ctx, cfg)

The pool default for MaxConns is the larger of 4 and the number of CPUs -- often fine for one process, but remember that each running instance of your service gets its own pool. Eight pods × 10 connections = 80 server connections, and PostgreSQL's default max_connections is 100. Size accordingly or put PgBouncer in front.

Context timeouts are not optional

Every pgx call takes a context.Context. Use it to bound query time so a slow query doesn't pin a connection forever:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
 
rows, err := pool.Query(ctx, "SELECT ...")

When the context expires, pgx cancels the in-flight query on the server side too, not just client-side. This is the single most useful habit for keeping a Go service responsive under load.

pgx through database/sql

If a tool needs the standard interface, import the stdlib adapter as a side-effecting blank import:

import (
	"database/sql"
	_ "github.com/jackc/pgx/v5/stdlib"
)
 
db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
if err != nil {
	log.Fatal(err)
}
defer db.Close()
 
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)

Note that sql.Open does not connect -- it only validates arguments and prepares the pool lazily. The first real connection happens on the first query. To verify connectivity at startup, call db.PingContext(ctx). This catches a bad host or credentials immediately instead of on the first user request.

lib/pq (legacy, maintenance mode)

import (
	"database/sql"
	_ "github.com/lib/pq"
)
 
db, err := sql.Open("postgres",
	"host=db.example.com port=5432 user=app_user password=secret dbname=sales sslmode=verify-full")

It works the same way through database/sql, including the lazy-connect behavior. The reason not to start a new project here is simply that the project is in maintenance mode -- bug fixes only, no new features -- and the maintainers themselves recommend pgx. If you already depend on it, there's no fire to put out; plan the migration when convenient.

The TLS / sslmode trap

The most common first error against a hosted PostgreSQL (RDS, Supabase, Neon, Cloud SQL, DigitalOcean) is a TLS one:

pq: SSL is not enabled on the server
# or, the opposite problem:
failed to connect: tls: failed to verify certificate: x509: certificate signed by unknown authority

sslmode controls this, and the values are not all equally safe:

sslmodeEncryptsVerifies certificateVerifies hostname
disableNoNoNo
requireYesNoNo
verify-caYesYesNo
verify-fullYesYesYes

require encrypts the connection but does not protect against a man-in-the-middle, because it doesn't check who it's talking to. For production, use verify-full and point the driver at the provider's CA bundle:

postgres://user:pass@host:5432/db?sslmode=verify-full&sslrootcert=/etc/ssl/rds-ca.pem

Every major provider documents where to download its CA certificate. Reaching for sslmode=disable to "make it work" is the wrong move on any network you don't fully control.

Common errors

ErrorLikely cause
dial tcp ... connection refusedWrong host/port, or PostgreSQL not listening on TCP (listen_addresses)
password authentication failed for userWrong credentials, or pg_hba.conf auth method mismatch
pq: SSL is not enabled on the serversslmode requires TLS but the server isn't configured for it
x509: certificate signed by unknown authorityverify-ca/verify-full without sslrootcert pointing at the provider CA
sorry, too many clients alreadyPool size × process count exceeds server max_connections
conn busy (pgx native)Sharing one pgx.Conn across goroutines -- use pgxpool instead

That last one is the classic native-pgx mistake: a single pgx.Conn is not concurrency-safe. If you see conn busy, you're sharing a connection across goroutines and should be using the pool.

Querying without writing connection code

If your goal is to explore a PostgreSQL database rather than build a service, a GUI client skips all of the above. Mako connects to PostgreSQL with AI-assisted SQL -- see our PostgreSQL GUI client guide for how it compares to pgAdmin, DBeaver, and TablePlus. Connecting from another language? See How to Connect to PostgreSQL from Python and from Node.js.

Mako connects to PostgreSQL with AI-powered autocomplete. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.