Go и PostgreSQL: Полное руководство по интеграции

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему 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 и т.д.

// П                

Блоги

Рекомендаций по книгам