Содержание урока · 9 разделов
- Анатомия postgres://: из чего состоит DSN
- Параметры DSN, которые встречаются чаще всего
- sslmode: disable, require и verify-full
- PGHOST, PGUSER и другие env-переменные libpq
- Сборка DSN из env: паттерн для сервиса
- Параметры пула в той же строке: pool_max_conns
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
История, знакомая каждому, кто выкатывал сервис в первый раз: на ноутбуке всё работает, в стейджинге — падение с ошибкой про SSL. Код один и тот же, разница целиком в строке подключения: локальный Postgres принял незащищённый канал, а облачный требует шифрования. DSN — это не «просто строка с паролем», а конфигурация протокола: кто вы, куда подключаетесь, шифровать ли канал, сколько ждать ответа и как называть своё приложение в логах сервера. pgx парсит эту строку при создании соединения или пула, и почти каждое поле имеет разумный дефолт — из-за этого короткий DSN вроде postgres://localhost/app тоже работает, ровно до первого попадания на сервер с другим поведением. Задача урока — снять с строки подключения покров магии: вы должны уметь на глаз сказать, за что отвечает каждый фрагмент и что произойдёт, если его убрать. Бонус умения: один и тот же формат понимают psql, инструменты миграций и бэкапов — настроив строку один раз, вы говорите на одном языке со всем стеком инструментов Postgres. Именно поэтому в уроке мало кода и много разборов: строка подключения — место, где сходятся безопасность, сеть и эксплуатация.
Анатомия postgres://: из чего состоит DSN#
Формат — URL: postgres://user:password@host:port/dbname?param=value. Разберём по частям. user и password — креденшалы; если пароля нет, часть с двоеточием просто опускается. host — имя хоста, IP или путь к Unix-сокету, если начинается со слэша: localhost для локальной базы, доменное имя для облака. port — по умолчанию 5432, и это чуть ли не единственное поле, которое можно смело опускать; IPv6-адреса пишутся в квадратных скобках, как в любом URL. У user тоже есть дефолт — имя пользователя операционной системы: подключение без user иногда падает с неожиданным role does not exist, потому что pgx подставил вашего системного пользователя, а такой роли в базе нет. dbname — имя базы; если его не указать, сервер попробует базу с именем пользователя, и новичок ловит ошибку database app does not exist, потому что назвал базу как пользователя. После вопросительного знака живут параметры: sslmode, connect_timeout, application_name и параметры пула. pgx понимает и второй формат — пары key=value в стиле libpq: host=localhost user=app password=secret dbname=app. Оба формата принимают pgx.Connect, pgxpool.New и pgx.ParseConfig; выбирайте один на проект и не смешивайте — половина путаницы в чужом коде растёт из того, что человек читает URL, а пишет key=value. Отдельная приятность: pgx принимает несколько хостов через запятую — host=primary,replica — и при недоступности первого пробует второй; для чтения с резервной реплики это готовый механизм без внешних балансировщиков.
Три DSN на один и тот же конфиг
GO// локальная разработка: шифрование не нужно, трафик не покидает машину
const devDSN = "postgres://app:secret@localhost:5432/app?sslmode=disable"
// облако: шифрование с проверкой хоста, таймаут, метка приложения
const prodDSN = "postgres://app@pg.example.com:5432/app" +
"?sslmode=verify-full&connect_timeout=5&application_name=billing"
// конвенция libpq: тот же конфиг без URL-схемы
const kvDSN = "host=localhost user=app password=secret dbname=app sslmode=disable"Три строки подключения на одинаковый по смыслу конфиг. В dev шифрование отключено, потому что канал не покидает машину. В prod — verify-full с проверкой сертификата и имени хоста, пяти секундный потолок на установку соединения и application_name, чтобы сервис было видно в pg_stat_activity. Третий формат — key=value: удобен, когда пароль содержит символы URL.
Параметры DSN, которые встречаются чаще всего#
| Параметр | Дефолт | Зачем нужен |
|---|---|---|
| user / password | пользователь ОС | аутентификация на сервере |
| host / port | localhost / 5432 | куда подключаться: хост, домен или путь к сокету |
| dbname | имя пользователя | база внутри сервера Postgres |
| sslmode | prefer | режим шифрования канала: disable, require, verify-full и другие |
| connect_timeout | нет | потолок времени на установку соединения, в секундах |
| application_name | пусто | метка сервиса в pg_stat_activity и логах сервера |
| pool_max_conns | max(4, NumCPU) | потолок одновременных соединений пула — тема четвёртого урока |
sslmode: disable, require и verify-full#
Режимов шесть, но решение сводится к одному вопросу: насколько вы параноик по поводу канала между приложением и базой. disable — чистый TCP без шифрования: годится, когда соединение не покидает машину или идёт внутри доверенной приватной сети. allow и prefer — полумеры: prefer, дефолтный режим, пытается шифровать, а при неудаче тихо откатывается к открытому каналу; именно эта гибкость мешает поймать ошибку конфигурации на стейджинге — всё выглядит работающим, пока в трафик не заглянет кто-то чужой. require — шифрует обязательно, но не проверяет сертификат: канал защищён от подглядывания, а вот посредник с любым валидным сертификатом перехватит соединение незаметно. Для verify-full сертификат сервера должен подписываться доверенным CA, либо путь к корневому сертификату передаётся параметром sslrootcert — в облаках его обычно скачивают вместе с креденшалами. verify-ca добавляет проверку цепочки сертификатов, verify-full — ещё и сверку имени хоста, и это единственный режим, который действительно стоит против man-in-the-middle. Моя конвенция простая: локально disable, на проде verify-full и никаких prefer в продовых DSN — тихий откат к открытому каналу это не надёжность, а её имитация. Промежуточный вариант для внутреннего контура — verify-ca с собственным корневым сертификатом: канал шифруется, цепочка проверяется, а сверка имени хоста остаётся на совести доверенной сети. Если облачный провайдер подписал сертификат на внутреннее имя и verify-full ругается, честное решение — sslrootcert с его CA, а не откат к require. Проверить фактический режим легко: в pg_stat_ssl видно, шифруется ли каждое соединение, — без этой проверки require в конфиге остаётся благим пожеланием.
Пароль в константе живёт в git-истории навсегда: даже после правки и force-push его достанут из старых коммитов. А утечь дешевле всего через логи: ошибка подключения с полным DSN попадает в stdout контейнера — и пароль уезжает в централизованный сборщик логов. Правило: креденшалы приходят из окружения или секрет-менеджера, строка собирается при старте, а в логи ошибки печатаются без DSN.
PGHOST, PGUSER и другие env-переменные libpq#
pgx читает переменные окружения по конвенции libpq — так называют набор настроек, который поддерживают официальные клиенты Postgres на всех языках. PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE, PGAPPNAME — полный список живёт в документации PostgreSQL, а имена стабильны годами, потому что их читают десятки утилит, не только pgx. Механика такая: любой параметр, который явно не указан в строке, pgx берёт из окружения, а если и там пусто — применяет дефолт. Пустая строка в качестве DSN — легальный способ сказать «всё из env»: так собраны многие двенадцатифакторные сервисы, где код приложения вообще не знает, где живёт база. Приоритет важный: явный параметр в строке бьёт переменную окружения, поэтому фрагмент ?sslmode=disable в DSN перебьёт PGSSLMODE=verify-full — и это источник незаметных багов при переносе конфигов между средами. Отладить приоритет легко: соберите конфиг через pgx.ParseConfig и напечатайте итоговые поля — сразу видно, что приехало из переменных окружения, а что из дефолтов. В Kubernetes и docker-compose переменные задаются в манифесте, и один и тот же образ работает в dev и prod без пересборки. Одна оговорка: PGPASSWORD удобна для локальных скриптов, но пароль в окружении виден в выводе ps и дампах /proc — прод-секреты живут в секрет-менеджерах и короткоживущих токенах, а не в открытом env.
pgx.ParseConfig: разобрать и дополнить
GOconfig, err := pgx.ParseConfig(dsn)
if err != nil {
log.Fatalf("битый DSN: %v", err)
}
// метка, видимая в pg_stat_activity и логах сервера
config.ConnConfig.RuntimeParams["application_name"] = "billing"
// потолок времени на установку одного соединения
config.ConnectTimeout = 5 * time.Second
pool, err := pgxpool.NewWithConfig(ctx, config)pgx.ParseConfig разбирает строку в структуру Config: так можно проверить DSN на старте и дописать то, что неудобно выражать в URL. RuntimeParams — параметры, которые уходят на сервер при подключении; ConnectTimeout ограничивает установку каждого соединения. NewWithConfig собирает пул из готовой конфигурации — путь, который предпочитают сервисы с настройками из env. ParseConfig читает и PG* переменные: пустой DSN из окружения — легальный вход.
Сборка DSN из env: паттерн для сервиса#
Рабочий приём, который я копирую из проекта в проект: приложение читает переменные окружения с дефолтами и собирает строку подключения при старте. os.Getenv для хоста, порта и имени базы, при пустом значении — дефолт для локальной разработки; пароль — без дефолта, его отсутствие падает явно. Дальше два пути: собрать строку через fmt.Sprintf или сконструировать url.URL и не думать об экранировании. Второй путь надёжнее: спецсимволы в пароле — классика, из-за которой подключение падает на сервере, работая на ноутбуке. Обязательный шаг — валидация: pgx.ParseConfig возвращает ошибку для битой строки, и вызывать его на старте стоит всегда — это дешёвый fail fast: сервис упадёт через секунду после запуска с понятным текстом, а не через час после деплоя с загадочным таймаутом. Код урока, как и весь курс, исполняется локально: песочница сайта не умеет сеть и Postgres — вам понадобится Go и контейнер из второго урока. Собранная в переменной DATABASE_URL, строка подключения поедет и в миграции, и в тесты — одна конфигурация на весь проект. В docker-compose она передаётся через environment, в Kubernetes — через ConfigMap и Secret: один и тот же бинарник меняет среду без пересборки, меняются только значения окружения.
ParseConfig возвращает ошибку до всякой сети, и её текст стоит читать внимательно — там обычно назван символ, на котором споткнулся парсер. Причины повторяются из проекта в проект: кавычки вокруг значения, прилетевшие из .env-файла вместе с переменной — их снимают не все загрузчики переменных, и парсер ищет хост по имени в кавычках; пробелы внутри пар key=value, которые в этом формате означают отдельный синтаксис с кавычками, а чаще просто ломают разбор; неотэкранированные спецсимволы пароля — соседний callout целиком про них; и перепутанный порядок, когда имя базы оказалось после вопросительного знака и ушло в параметры. Дешёвая привычка для отладки: печатайте в лог не сам DSN, а поля собранной структуры Config — увидите, что парсер понял на самом деле, и не раскроете секрет в логах.
Пароль с @, : или / внутри разорвёт разбор postgres://: парсер примет его за разделитель. Экранируйте значения (символ @ превращается в %40), используйте key=value формат, где достаточно кавычек, или url.UserPassword при сборке из кода. Диагноз простой: DSN работает локально, но не парсится на сервере — первым делом проверьте пароль.
application_name из DSN попадает в pg_stat_activity и логи сервера: во время инцидента вы увидите в списке соединений billing, а не безликий ряд пустых строк. Одна переменная — и расследование «кто держит транзакцию с часу ночи» сокращается вдвое.
Параметры пула в той же строке: pool_max_conns#
У pgxpool нет отдельной строки подключения — пул принимает тот же DSN, и его параметры живут после вопросительного знака: pool_max_conns, pool_min_conns, pool_max_conn_lifetime, pool_max_conn_idle_time, pool_health_check_period. Синтаксис единый, читается одна строка — удобно, когда конфигурация хранится в одной переменной окружения DATABASE_URL и раздаётся всему приложению. Подробная настройка лимитов, жизненный цикл коннектов и расчёт под max_connections сервера — тема четвёртого урока; пока достаточно знать, что такие параметры существуют и работают без отдельного кода конфигурации.
Проверка: дефолты DSN
Какой порт Postgres используется, если в строке подключения его не указать?
Проверка: sslmode
Чем sslmode=require хуже verify-full?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
config, err := pgx.ParseConfig("postgres://app:secret@localhost/mydb")
if err != nil {
panic(err)
}
fmt.Println(config.ConnConfig.Host, config.ConnConfig.Port)Соберите DSN
Допишите строку подключения: пользователь, пароль и режим шифрования для локальной машины.
Соберите разбор конфига из env
Расставьте строки в порядке исполнения: чтение окружения, разбор, обработка ошибки, настройка, создание пула.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: строка подключения
Строка собрана, дефолты поняты — дальше по курсу: в четвёртом уроке настраиваем пул соединений — лимиты под max_connections, жизненный цикл коннектов и Stat для наблюдения. В пятом наконец отправим первые запросы. А в двадцать первом уроке та же строка подключения понесёт миграции схемы — соберите её в переменную окружения один раз и переиспользуйте везде: сервис, пул, миграции и тесты едут на одном значении. Для локальной отладки держите под рукой psql из того же контейнера — он принимает ту же строку подключения, и расхождение между ним и программой почти всегда означает разницу в переменных окружения.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- DSN — это URL: user:password@host:port/dbname плюс параметры; пропущенные поля подставляются из PG* env или дефолтов.
- Дефолтный порт — 5432; пропущенное dbname подставит имя пользователя и подарит загадочную ошибку.
- sslmode: локально disable, на проде verify-full; prefer молча откатывается к открытому каналу.
- require шифрует канал, но не проверяет сертификат — против man-in-the-middle только verify-full.
- pgx читает PGHOST, PGUSER, PGPASSWORD и остальные переменные libpq: пустой DSN — конфиг из окружения.
- Пароль живёт в env или секрет-менеджере; в git-истории и логах ему не место; спецсимволы экранируются.
- Параметры пула — pool_max_conns и другие — передаются в той же строке подключения.
Домашнее задание
Переведите программу из второго урока на DSN из переменной окружения: соберите DATABASE_URL со sslmode=disable и запустите. Затем поднимите второй контейнер с требованием шифрования и посмотрите, какая ошибка придёт при disable. После этого соберите конфиг через pgx.ParseConfig, поставьте application_name=pgx-course и найдите своё приложение в pg_stat_activity: docker exec -it pgx-course psql -U app -c 'select application_name, state from pg_stat_activity'.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем DSN отличается от conninfo?
По сути ничем: оба термина означают строку подключения. DSN — имя из мира ODBC и других языков, conninfo — термин libpq. pgx принимает оба формата: URL postgres:// и пары key=value — и внутри приводит их к одной структуре Config.
Как безопасно хранить пароль?
В переменной окружения, которая прилетает из секрет-хранилища: Kubernetes Secret, Vault, SOPS — что доступно. В коде пароля нет вообще: строка подключения собирается при старте из env, а часть параметров можно не собирать вовсе — pgx прочитает PGHOST и PGUSER сам.
Что делать, если база требует сертификат клиента?
К DSN добавляются sslcert и sslkey — пути к клиентскому сертификату и ключу, а sslrootcert указывает на корневой CA. Параметры работают и в URL, и через переменные окружения PGSSLCERT и PGSSLKEY; в облаках так подключаются к управляемым базам с взаимной TLS-аутентификацией.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
MongoDB в Go: официальный драйвер mongo-driver v2
Курс MongoDB для Go: подключение, bson-теги, CRUD, фильтры, агрегации, индексы, транзакции и Change Streams. 25 уроков по mongo-driver v2.
- Подключаться к MongoDB и настраивать клиента
- Моделировать документы структурами и bson-тегами
- Делать CRUD, сложные фильтры и проекции
- Строить агрегации $match/$group/$lookup