Skip to main content
pgx 是 Go 生态中使用最广泛的 PostgreSQL 驱动程序,pgxpool 则是它提供的连接池。在本指南中,您将通过经过证书验证的 TLS 连接,将 Go 服务接入 ClickHouse Managed Postgres,借助 golang-migrate 迁移创建 todos 表,并由 net/http 服务器处理增删改查 (创建、读取、更新和删除) 请求。

前置条件

  • Go 1.25 或更高版本。本指南已在 Go 1.27.1、pgx 5.11.0 和 golang-migrate 4.20.1 环境下测试通过。
  • 一个 ClickHouse Cloud 账户
  • psql,用于创建数据库。也可以在 SQL 控制台中运行 CREATE DATABASE 语句。

创建 ClickHouse Managed Postgres 服务

在 ClickHouse Cloud 控制台中,点击 New service,然后选择 Postgres。实例会在几分钟内就绪。详细操作步骤请参阅快速入门。

获取连接信息

在服务的左侧边栏中点击 Connect。保持选中 Directly,并开启 Use SSL。此时 url 选项卡中的连接 URL 会使用端口 5432,并以 sslmode=verify-full&sslrootcert=<service-name>-ca-certificate.pem 结尾。点击 Download CA certificate。您也可以稍后在 Settings → CA Certificate 中下载。该证书为您的实例所独有,因此 pgx 可以用它来验证所连接的确实是您的服务器。 本指南中,无论是执行迁移还是运行应用程序,都通过端口 5432 直接连接 Postgres。Go 服务是长期运行的进程,pgxpool 已在进程内维护了连接池,因此无需再额外使用连接池器。如果您运行了该服务的多个实例,且所需的客户端连接数超出 Postgres 的上限,请让应用程序改用内置的 PgBouncer (via PgBouncer,端口 6432) 。具体请参阅使用 PgBouncer,该方式需要额外配置一项连接设置。

搭建项目

创建一个 Go 模块,添加 pgx,并安装带 pgx 驱动程序的 migrate 命令行客户端:
-tags 'pgx5' 标志会在编译 migrate 命令行客户端时启用其 pgx v5 驱动程序,该驱动程序使用 pgx5:// URL 协议。go install 会将二进制文件安装到 $GOBIN;如果未设置 GOBIN,则安装到 $(go env GOPATH)/bin,因此请确保该目录已加入 PATH。
要集成到现有应用中?跳过 mkdir 和 go mod init。在你的模块中运行 go get github.com/jackc/pgx/v5 以及上面的 go install 命令,然后继续阅读配置连接。如果你的代码使用 database/sql,可以通过 stdlib.OpenDBFromPool 将同一个连接池封装为 *sql.DB,从而逐步迁移现有查询;迁移时请将 ? 占位符改为 $1、$2 等形式。

配置连接

将下载的 CA 证书移至项目目录,并重命名为 ca-certificate.pem。 为应用创建一个数据库。将 <PASSWORD> 和主机地址替换为 Connect 弹窗中显示的值:
在 shell 中设置两个环境变量。二者指向同一个 guide_go 数据库,唯一的区别在于 URL 协议:应用程序读取 DATABASE_URL,而 migrate 命令行客户端使用 MIGRATE_URL:
pgx 解析 URL 的方式与 libpq 相同。使用 sslmode=verify-full 时,pgx 会从 sslrootcert 加载 CA 文件,并同时校验证书链和主机名,因此无需编写任何 TLS 代码。sslrootcert 路径相对于运行命令时所在的目录进行解析。如果密码包含 @、/ 或 # 等特殊字符,请对其进行 URL 编码。
务必设置 sslmode=verify-full如果未设置 sslmode,pgx 默认使用 prefer,此时即使设置了 sslrootcert,也不会校验服务器证书。设置 sslmode=verify-full 后,如果 CA 文件不正确,pgx 会报错 x509: certificate signed by unknown authority;如果该文件不存在,则会报错 unable to read CA file。如果使用 IP 地址而非主机名进行连接,则会报错 x509: cannot validate certificate for <IP> because it doesn't contain any IP SANs。

通过迁移创建 schema

创建迁移文件:
编写 up 迁移的内容:
migrations/000001_create_todos.up.sql
以及用于撤销该变更的 down 迁移:
migrations/000001_create_todos.down.sql
执行该迁移:
migrate 会在 schema_migrations 表中记录当前版本,之后再运行时只会应用更新的迁移。请将 migrations 目录与代码一同提交。
请始终通过直接连接 (端口 5432) 运行 migrate。它在运行期间会持有一个会话级咨询锁 (advisory lock) ,以防止两个部署同时执行迁移。如果经由 PgBouncer 连接,加锁和解锁可能会在不同的 Postgres 连接上执行,导致锁始终未被释放,之后再运行 migrate 时会挂起,或因 timeout: can't acquire database lock 报错而失败。

查询数据库

创建 db.go。该文件基于 DATABASE_URL 创建连接池,并在启动时向 Postgres 查询当前会话使用的 TLS 版本,以此检查连接是否正常:
db.go
创建 todos.go,在其中编写 HTTP 处理程序。每个处理程序都会在连接池上执行一条参数化查询,再由 pgx.CollectRows 和 pgx.CollectExactlyOneRow 按列名将结果行扫描到 Todo 结构体中:
todos.go
以参数形式 ($1) 传递值,而不是将其拼接到 SQL 字符串中,可以防止 SQL 注入。pgx 会将这些值与查询文本分开发送。您无需单独检查 pool.Query 返回的错误,因为 pgx.CollectRows 和 pgx.CollectExactlyOneRow 会返回该错误。 创建 main.go。该程序为整个进程创建一个连接池,在端口 3114 上提供上述路由,并在按下 Ctrl+C 时关闭连接池。如果是现有应用,则应在启动时调用一次 newPool,并在您的 http.ServeMux 上调用 registerTodoRoutes:
main.go

运行并验证

将 pgxpool 的其余依赖项添加到 go.mod 和 go.sum 中,然后启动服务器:
TLSv1.3 取自服务器上的 pg_stat_ssl,由此可以确认该会话已加密。如果将 sslrootcert 指向其他任意证书,服务器将无法启动,并在日志中输出 x509: certificate signed by unknown authority,这说明证书确实经过了校验。 在另一个终端中,创建两个待办事项,将第一个标记为已完成,然后删除第二个:
PATCH 请求会返回更新后的行:
在浏览器中打开 http://localhost:3114/todos,即可列出剩余的待办事项。pgx 返回的 timestamptz 值采用本地时区。响应如下所示:
按 Ctrl+C 停止服务器。服务器会在日志中输出 Shutting down,并关闭连接池。 要在控制台中查看数据,请在服务的左侧边栏中打开 SQL 控制台,依次展开 guide_go 和 public,然后点击 todos 表。旁边的 schema_migrations 表记录了迁移版本。todos 表中仅剩下一行数据:

使用 PgBouncer

如需让应用的查询经由内置的 PgBouncer 发送,请在 Connect 弹窗中选择 via PgBouncer,将 DATABASE_URL 中的端口改为 6432,并添加 default_query_exec_mode=cache_describe。MIGRATE_URL 仍使用端口 5432:
pgx 会从 URL 中读取 default_query_exec_mode,但不会将其发送到服务器。无需修改代码,原有的 CA 证书也照常可用。启动日志现在会显示 TLS: none,这是因为 pg_stat_ssl 报告的是 PgBouncer 自身到 Postgres 的连接。应用到 PgBouncer 的连接仍会经过验证:如果 CA 证书不正确,连接同样会失败。
不要通过 PgBouncer 使用 pgx 的默认查询模式默认情况下,pgx 会在每个连接上将每条查询预处理为命名语句 (每个连接仅一次) ,并复用返回的列类型。PgBouncer 会在实际执行该查询的 Postgres 连接上预处理这些语句,并在客户端之间共享。如果迁移更改了某列的类型,或向以 SELECT * 查询的表中添加了列,经由 PgBouncer 的查询可能会持续报错 cached plan must not change result type,即使重启应用也无济于事;在 pgx 5.9 及更高版本中,甚至可能不报错却返回错误的值。例如,某列从 bigint 改为 double precision 后,返回的会是 float64 值的原始比特,如 42.5 会返回为 4631178160564600832。cache_describe 只缓存参数类型和列类型,在单次往返中以未命名语句执行每条查询,并根据服务器随每个结果返回的列类型解码行,因此不受此问题影响。exec 同样可用。不要通过 PgBouncer 使用 describe_exec:它会将每条查询拆分为两次往返,而 PgBouncer 可能会把这两次往返发送到不同的 Postgres 连接。无需设置 statement_cache_capacity=0。
PgBouncer 以事务池化模式运行,因此每个事务都可能在不同的 Postgres 连接上执行:
  • 通过 pool.Begin 或 pgx.BeginFunc 使用事务不受影响,可照常工作。
  • 通过 SET 进行的会话设置不会保留在您的连接上,还可能泄漏给其他客户端。请改为在事务内使用 SET LOCAL。此外,如果连接 URL 中包含 search_path,PgBouncer 会拒绝连接并报错 unsupported startup parameter: search_path;请改用带 schema 限定的表名。
  • SQL 层面的 PREPARE 和 EXECUTE、LISTEN 以及会话级咨询锁均无法使用。

后续步骤

  • 连接:连接字符串、PgBouncer 和 TLS
  • 设置:Postgres 和 PgBouncer 参数,例如 max_connections
  • 只读副本:使用第二个 pgxpool.Pool 将只读查询发送到副本
  • 安全:IP 访问列表和私有网络