Почему Go и PostgreSQL — это идеальная пара?
В мире backend-разработки Go (или Golang) и PostgreSQL зарекомендовали себя как мощный тандем для создания высоконагруженных и надежных систем. Go дарит нам невероятную производительность, простоту и отличную поддержку конкурентности, а PostgreSQL — это промышленная база данных с богатым функционалом, надежностью и расширяемостью. Объединив их, вы получаете стек, способный выдержать нагрузки стартапа и требования enterprise-проекта.
В этой статье мы не просто пробежимся по синтаксису. Мы создадим полноценное практическое руководство: от установки драйвера до оптимизации пула соединений и работы с транзакциями. Вы узнаете, как избежать типичных ошибок и писать эффективный код на Go для работы с PostgreSQL. Поехали!
Начало работы: выбираем драйвер и подключаемся
Первое, что нужно сделать — установить драйвер. В мире Go есть два главных кандидата:
- lib/pq — классический, простой, но уже не поддерживается активно.
- pgx — современный, быстрый, активно развивается, поддерживает больше возможностей PostgreSQL (например, COPY, LISTEN/NOTIFY, массивы). Рекомендую именно его.
Устанавливаем pgx:
go get github.com/jackc/pgx/v5
Теперь напишем код для подключения к базе данных. Создадим файл main.go:
package main
import (
"context"
"fmt"
"log"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
// URL подключения: пользователь, пароль, хост, порт, имя БД
dsn := "postgres://myuser:mypassword@localhost:5432/mydb?sslmode=disable"
// Создаем пул соединений (connection pool)
pool, err := pgxpool.New(context.Background(), dsn)
if err != nil {
log.Fatalf("Не удалось создать пул: %v", err)
}
defer pool.Close()
// Проверяем соединение
err = pool.Ping(context.Background())
if err != nil {
log.Fatalf("Не удалось подключиться: %v", err)
}
fmt.Println("Успешно подключились к PostgreSQL!")
}
Обратите внимание: мы используем pgxpool, а не простое соединение. Пул соединений — это must-have для production. Он позволяет переиспользовать соединения, избегая накладных расходов на их создание и закрытие.
CRUD-операции: создаем, читаем, обновляем и удаляем
Теперь давайте реализуем классические операции с таблицей users. Предположим, у нас есть структура:
type User struct {
ID int
Name string
Age int
}
Создание таблицы (Create)
Лучше выполнять миграции отдельными инструментами, но для примера покажу, как выполнить raw SQL:
func createTable(ctx context.Context, pool *pgxpool.Pool) error {
query := `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
age INT NOT NULL
);
`
_, err := pool.Exec(ctx, query)
return err
}
Добавление записи (Insert)
Используем QueryRow для возврата сгенерированного ID:
func createUser(ctx context.Context, pool *pgxpool.Pool, user *User) error {
query := `INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id`
err := pool.QueryRow(ctx, query, user.Name, user.Age).Scan(&user.ID)
return err
}
Чтение записи (Select)
func getUserByID(ctx context.Context, pool *pgxpool.Pool, id int) (*User, error) {
query := `SELECT id, name, age FROM users WHERE id = $1`
user := &User{}
err := pool.QueryRow(ctx, query, id).Scan(&user.ID, &user.Name, &user.Age)
if err != nil {
return nil, err
}
return user, nil
}
Обновление и удаление (Update / Delete)
func updateUser(ctx context.Context, pool *pgxpool.Pool, user *User) error {
query := `UPDATE users SET name = $1, age = $2 WHERE id = $3`
_, err := pool.Exec(ctx, query, user.Name, user.Age, user.ID)
return err
}
func deleteUser(ctx context.Context, pool *pgxpool.Pool, id int) error {
query := `DELETE FROM users WHERE id = $1`
_, err := pool.Exec(ctx, query, id)
return err
}
Транзакции и работа с ошибками в Go
Транзакции — это основа целостности данных. В Go их реализация проста и элегантна. Представьте, что нам нужно создать пользователя и сразу записать лог:
func createUserWithLog(ctx context.Context, pool *pgxpool.Pool, user *User) error {
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
// Важно: если ошибка, откатываем транзакцию
defer tx.Rollback(ctx)
// Вставляем пользователя
queryUser := `INSERT INTO users (name, age) VALUES ($1, $2) RETURNING id`
err = tx.QueryRow(ctx, queryUser, user.Name, user.Age).Scan(&user.ID)
if err != nil {
return err
}
// Вставляем лог
queryLog := `INSERT INTO logs (action, user_id) VALUES ('created', $1)`
_, err = tx.Exec(ctx, queryLog, user.ID)
if err != nil {
return err
}
// Если все ОК, фиксируем транзакцию
return tx.Commit(ctx)
}
Всегда проверяйте ошибку от Commit — если БД упала, она может вернуть ошибку даже после успешных Exec.
Пулы соединений: настройка под нагрузку
Пул соединений — это сердце производительности. По умолчанию pgxpool создает пул с разумными настройками, но их можно и нужно кастомизировать:
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
log.Fatal(err)
}
// Максимальное количество соединений в пуле
config.MaxConns = 25
// Минимальное количество простаивающих соединений
config.MinConns = 5
// Максимальное время жизни соединения
config.MaxConnLifetime = 1 * time.Hour
// Максимальное время бездействия соединения
config.MaxConnIdleTime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(context.Background(), config)
Совет: Не делайте MaxConns слишком большим. PostgreSQL имеет ограничение на количество соединений (обычно 100-300). Лучше ориентироваться на MaxConns = (количество ядер CPU * 2) + эффективный дисковый ввод-вывод.
Best Practices и типичные ошибки
Всегда используйте параметризованные запросы
Никогда не подставляйте значения через fmt.Sprintf — это прямой путь к SQL-инъекциям. Используйте $1, $2 и т.д.
// П