pgxpool is its connection pool. In this guide, you connect a Go service to ClickHouse Managed Postgres over verified TLS, create a todos table with a golang-migrate migration, and serve create, read, update, and delete requests from a net/http server.
Prerequisites
- Go 1.25 or later. This guide was tested with Go 1.27.1, pgx 5.11.0, and golang-migrate 4.20.1.
- A ClickHouse Cloud account
psql, to create the database. You can also run theCREATE DATABASEstatement in the SQL console.
Create a ClickHouse Managed Postgres service
In the ClickHouse Cloud console, click New service and select Postgres. The instance is ready in a few minutes. See the quickstart for a walkthrough.Get your connection details
Click Connect in the left sidebar of your service. Keep Directly selected and turn on Use SSL. The connection URL on the url tab now uses port5432 and ends in sslmode=verify-full&sslrootcert=<service-name>-ca-certificate.pem. Click Download CA certificate. You can also download it later from Settings → CA Certificate. The certificate is unique to your instance, so pgx can use it to verify that it’s talking to your server.
This guide connects directly to Postgres on port 5432, both for migrations and for the app. A Go service is a long-lived process, and pgxpool already keeps a pool of connections in it, so it doesn’t need a second pooler. If you run many instances of the service and need more client connections than Postgres allows, use the bundled PgBouncer (via PgBouncer, port 6432) for the app; see Use PgBouncer, which needs one extra connection setting.
Set up the project
Create a Go module, add pgx, and install themigrate CLI with the pgx driver:
-tags 'pgx5' flag compiles the migrate CLI with its pgx v5 driver, which uses the pgx5:// URL scheme. go install puts the binary in $GOBIN, or in $(go env GOPATH)/bin if GOBIN isn’t set, so make sure that directory is on your PATH.
Configure the connection
Move the CA certificate you downloaded into the project directory and rename it toca-certificate.pem.
Create a database for the app. Replace <PASSWORD> and the host with the values from the Connect modal:
guide_go database and differ only in the URL scheme: the app reads DATABASE_URL, and the migrate CLI uses MIGRATE_URL:
libpq does. With sslmode=verify-full, it loads the CA file from sslrootcert and checks both the certificate chain and the hostname, so you don’t need any TLS code. The sslrootcert path is resolved relative to the directory you run commands from. If your password contains special characters such as @, /, or #, URL-encode them.
Create the schema with a migration
Create the migration files:up migration:
migrations/000001_create_todos.up.sql
down migration, which reverts it:
migrations/000001_create_todos.down.sql
migrate records the current version in the schema_migrations table and only applies newer migrations on later runs. Commit the migrations directory with your code.
Always run
migrate over the direct connection (port 5432). It holds a session-level advisory lock while it runs, so that two deployments can’t migrate at the same time. Through PgBouncer, the lock and unlock can run on different Postgres connections: the lock stays held, and later migrate runs hang or fail with timeout: can't acquire database lock.Query the database
Createdb.go. It creates the pool from DATABASE_URL and checks the connection at startup by asking Postgres which TLS version the session uses:
db.go
todos.go with the HTTP handlers. Each handler runs one parameterized query on the pool, and pgx.CollectRows and pgx.CollectExactlyOneRow scan the rows into the Todo struct by column name:
todos.go
$1) rather than formatting them into the SQL string prevents SQL injection. pgx sends them separately from the query text. You don’t need to check the error from pool.Query separately: pgx.CollectRows and pgx.CollectExactlyOneRow return it.
Create main.go. It creates one pool for the whole process, serves the routes on port 3114, and closes the pool on Ctrl+C. In an existing app, call newPool once at startup and registerTodoRoutes on your http.ServeMux instead:
main.go
Run and verify
Add the remaining dependencies ofpgxpool to go.mod and go.sum, then start the server:
TLSv1.3 comes from pg_stat_ssl on the server, so it confirms that the session is encrypted. If you point sslrootcert at any other certificate, the server doesn’t start and logs x509: certificate signed by unknown authority, which shows that the certificate is actually checked.
In a second terminal, create two todos, mark the first one done, and delete the second:
PATCH request returns the updated row:
http://localhost:3114/todos in your browser to list the remaining todos. pgx returns timestamptz values in your local time zone. The response looks like this:
Shutting down and closes the pool.
To see the data in the console, open SQL console in the left sidebar of your service, expand guide_go and then public, and click the todos table. The schema_migrations table next to it holds the migration version. The todos table contains the remaining row:
Use PgBouncer
To send the app’s queries through the bundled PgBouncer, select via PgBouncer in the Connect modal, change the port inDATABASE_URL to 6432, and add default_query_exec_mode=cache_describe. Keep MIGRATE_URL on port 5432:
default_query_exec_mode from the URL and doesn’t send it to the server. No code changes are needed, and the same CA certificate works. The startup log now shows TLS: none, because pg_stat_ssl reports PgBouncer’s own connection to Postgres. Your app’s connection to PgBouncer is still verified: a wrong CA certificate fails in the same way.
PgBouncer runs in transaction pooling mode, so each transaction can run on a different Postgres connection:
- Transactions with
pool.Beginorpgx.BeginFuncwork as usual. - Session settings made with
SETdon’t stay with your connection, and can leak to other clients. UseSET LOCALinside a transaction instead. PgBouncer also rejectssearch_pathin the connection URL withunsupported startup parameter: search_path; schema-qualify table names instead. - SQL-level
PREPAREandEXECUTE,LISTEN, and session advisory locks don’t work.
Next steps
- Connection: connection strings, PgBouncer, and TLS
- Settings: Postgres and PgBouncer parameters, such as
max_connections - Read replicas: send read-only queries to a replica with a second
pgxpool.Pool - Security: IP access lists and private networking