Подключаем PostgreSQL к приложению на Go: пошаговая инструкция
Разбираемся, как подключить PostgreSQL к Go-приложению с нуля: от установки драйвера до первых запросов и пула соединений. На выходе — рабочий сервис, который читает и пишет данные в базу.

Что понадобится до старта
Прежде чем писать код, убедитесь, что у вас есть всё из этого списка. Иначе застрянете не на том шаге, где ожидали.
- Go версии 1.21 или новее (проверьте командой
go version). - Работающий сервер PostgreSQL 14+ — локально, в Docker или на удалённом хосте. Подойдёт и облачный managed-инстанс.
- Созданная база данных и пользователь с правами на чтение и запись в ней. Если ставили PostgreSQL только что — создайте базу командой
createdbиз терминала или черезpsql. - Строка подключения (DSN) вида
postgres://user:password@host:5432/dbname?sslmode=disable. На продеsslmodeвыставляйте вrequireилиverify-full—disableдопустим только для локальной разработки.
Всё на месте — переходим к коду. Весь путь займёт 20–30 минут.
Шаг 1. Инициализируем модуль и ставим драйвер
Go работает с PostgreSQL через драйвер, который реализует интерфейс database/sql. Самый распространённый вариант — pgx (пакет github.com/jackc/pgx/v5). Он же предоставляет совместимый адаптер для database/sql, что удобно, если хотите стандартный интерфейс.
# Создаём каталог проекта и инициализируем модуль
mkdir pgdemo && cd pgdemo
go mod init pgdemo
# Устанавливаем pgx и его адаптер для database/sql
go get github.com/jackc/pgx/v5
go get github.com/jackc/pgx/v5/stdlib
Проверка: файл go.mod содержит строки с jackc/pgx/v5. Если вместо этого видите ошибку «module not found» — убедитесь, что прокси модулей доступен (GOPROXY=https://proxy.golang.org,direct).
Шаг 2. Открываем соединение
Создайте файл main.go и вставьте код ниже целиком — он компилируется и запускается.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/jackc/pgx/v5/stdlib" // регистрируем драйвер pgx для database/sql
)
func main() {
// DSN берём из переменной окружения, чтобы не хранить пароль в коде
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
log.Fatal("переменная DATABASE_URL не задана")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
log.Fatalf("не удалось создать пул: %v", err)
}
defer db.Close()
// Настраиваем пул соединений
db.SetMaxOpenConns(10) // максимум открытых соединений
db.SetMaxIdleConns(5) // простаивающих
db.SetConnMaxLifetime(5 * time.Minute) // время жизни одного соединения
// Проверяем, что база действительно доступна
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.PingContext(ctx); err != nil {
log.Fatalf("база недоступна: %v", err)
}
fmt.Println("подключение к PostgreSQL установлено")
}
Запустите:
DATABASE_URL="postgres://user:password@localhost:5432/dbname?sslmode=disable" go run main.go
Проверка: в терминале появилась строка «подключение к PostgreSQL установлено». Если нет — смотрите раздел «Если не получилось» ниже.
Функцияsql.Openне открывает соединение немедленно — она создаёт пул. Реальное подключение происходит при первом запросе или при вызовеPing. ПоэтомуPingпослеOpen— не перестраховка, а единственный способ узнать, что база на месте.
Шаг 3. Создаём таблицу
Добавьте функцию createTable и вызовите её из main после проверки соединения.
func createTable(ctx context.Context, db *sql.DB) error {
query := `
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
ts TIMESTAMPTZ NOT NULL DEFAULT now()
);`
_, err := db.ExecContext(ctx, query)
return err
}
// в main, после Ping
if err := createTable(ctx, db); err != nil {
log.Fatalf("не удалось создать таблицу: %v", err)
}
fmt.Println("таблица events готова")
Проверка: подключитесь к базе через psql и выполните \dt — таблица events должна быть в списке.
Шаг 4. Вставляем и читаем данные
Теперь запишем строку и прочитаем её обратно. Обратите внимание: для вставки используем параметризованный запрос ($1), а не склейку строк. Это защищает от SQL-инъекций.
func insertEvent(ctx context.Context, db *sql.DB, name string) (int, error) {
var id int
err := db.QueryRowContext(ctx,
"INSERT INTO events (name) VALUES ($1) RETURNING id", name,
).Scan(&id)
return id, err
}
func listEvents(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT id, name, ts FROM events ORDER BY id")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id int
name string
ts time.Time
)
if err := rows.Scan(&id, &name, &ts); err != nil {
return err
}
fmt.Printf("#%d %s %s\n", id, name, ts.Format(time.RFC3339))
}
return rows.Err() // не забываем: ошибка может возникнуть после последней строки
}
// в main
id, err := insertEvent(ctx, db, "deploy-v1.2")
if err != nil {
log.Fatalf("вставка не удалась: %v", err)
}
fmt.Printf("создано событие #%d\n", id)
if err := listEvents(ctx, db); err != nil {
log.Fatalf("чтение не удалось: %v", err)
}
Проверка: в выводе — строка с id, именем deploy-v1.2 и текущей датой.
Шаг 5. Оборачиваем запись в транзакцию
Если нужно вставить несколько строк атомарно — используйте транзакцию. При ошибке на любом шаге все изменения откатятся.
func insertBatch(ctx context.Context, db *sql.DB, names []string) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
// Откатываем, если что-то пошло не так; после Commit вызов Rollback — no-op
defer tx.Rollback()
for _, n := range names {
if _, err := tx.ExecContext(ctx,
"INSERT INTO events (name) VALUES ($1)", n,
); err != nil {
return fmt.Errorf("ошибка при вставке %q: %w", n, err)
}
}
return tx.Commit()
}
Проверка: вызовите insertBatch с тремя именами, затем listEvents — все три строки на месте.
Настройки пула: что и зачем
В шаге 2 мы задали три параметра пула. Вот что стоит за каждым из них:
| Параметр | Что делает | Рекомендация для старта |
|---|---|---|
MaxOpenConns | Ограничивает общее число соединений к базе | Начните с 10; увеличивайте, только если видите ожидание в очереди |
MaxIdleConns | Сколько соединений держать открытыми без нагрузки | Половина от MaxOpenConns — разумный старт |
ConnMaxLifetime | Через какое время соединение закрывается и пересоздаётся | 5 минут; помогает при смене IP базы и при балансировке в облаке |
Если приложение — единственный клиент базы, MaxOpenConns можно поднять. Если клиентов много (несколько подов, воркеры, cron) — делите лимит max_connections PostgreSQL между ними с запасом или ставьте pgBouncer перед базой. Настройка pgBouncer — отдельная тема, здесь не разворачиваем.
Если не получилось
Пять ошибок, которые встречаются чаще всего на этом этапе:
dial tcp 127.0.0.1:5432: connect: connection refused— PostgreSQL не запущен или слушает другой порт. Проверьте статус сервиса (systemctl status postgresqlилиdocker ps) и порт вpostgresql.conf.password authentication failed for user— неправильный пароль или пользователь. Убедитесь, что DSN совпадает с тем, что задано приCREATE USER, и что вpg_hba.confразрешён методmd5илиscram-sha-256для вашего хоста.database "dbname" does not exist— база не создана. Выполнитеcreatedb dbnameот имени суперпользователя PostgreSQL.pq: SSL is not enabled on the server— в DSN стоитsslmode=require, а сервер не настроен на SSL. Для локальной разработки замените наsslmode=disable.cannot find package "github.com/jackc/pgx/v5/stdlib"— зависимость не скачана. Повторитеgo mod tidy.
Чего мы не сделали — и что делать дальше
Инструкция покрыла подключение, пул, CRUD и транзакции на уровне database/sql. Вот что стоит взять следующим шагом:
- Миграции. Хранить
CREATE TABLEв коде — путь к хаосу. Посмотрите наgolang-migrate/migrateилиpressly/goose— они версионируют схему файлами SQL. - Нативный API pgx. Интерфейс
database/sqlуниверсален, но теряет часть возможностей PostgreSQL:COPY, типизированные массивы,LISTEN/NOTIFY. Если проект растёт — переходите наpgx.Connилиpgxpool.Poolнапрямую. Документация pgx описывает оба пути. - Graceful shutdown. В продакшне вызывайте
db.Close()по сигналуSIGTERM, чтобы соединения закрывались корректно, а не обрывались. - Мониторинг. Метод
db.Stats()отдаёт текущее число открытых, занятых и ожидающих соединений — выгружайте их в Prometheus или аналог, чтобы видеть, когда пул исчерпан.
Запустите готовый main.go, убедитесь, что данные пишутся и читаются, а потом двигайтесь к миграциям — это первое, чего не хватает рабочему проекту.
Карточки для маркетплейсов: Таблица товаров и папка фотографий на входе — комплект слайдов и описаний на выходе, по требованиям Wildberries и Ozon.
Спросить за 5 ₽Источники: pgx — PostgreSQL driver and toolkit for Go (GitHub), database/sql — стандартная библиотека Go
Частые вопросы
Чем pgx отличается от lib/pq?
Обязательно ли использовать database/sql или можно работать через pgx напрямую?
Как хранить DSN, чтобы пароль не попал в репозиторий?
Что если у меня другая версия Go или pgx?
Нужен ли пул соединений или хватит одного коннекта?
Приложение падает с ошибкой «too many connections» — что делать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.