Расскажите о вашем опыте работы с PostgreSQL в Go-приложениях.

Ответ

Да, я активно использовал PostgreSQL в качестве основной СУБД для Go-приложений. Мой опыт включает работу через стандартный интерфейс database/sql и современные драйверы.

Основные инструменты:

  • database/sql: Стандартный пакет Go, который предоставляет общий интерфейс для работы с SQL-базами данных.
  • Драйверы: В основном использовал pgx, так как он является наиболее производительным и функциональным современным драйвером. Ранее также работал с lib/pq.

Ключевые концепции database/sql:

  1. sql.DB — это пул соединений: Важно понимать, что sql.DB представляет собой потокобезопасный пул соединений, а не одно конкретное подключение. Его нужно создавать один раз при старте приложения и переиспользовать.

  2. Разделение запросов:

    • db.QueryRowContext(): Для запросов, возвращающих одну строку. Результат считывается через .Scan().
    • db.QueryContext(): Для запросов, возвращающих множество строк. Результат итерируется в цикле с обязательным rows.Close() для возврата соединения в пул.
    • db.ExecContext(): Для команд, не возвращающих строки (INSERT, UPDATE, DELETE).

Пример запроса одной строки:

import (
    "database/sql"
    "context"
    _ "github.com/jackc/pgx/v5/stdlib"
)

var db *sql.DB // Инициализируется при старте

func GetUserName(ctx context.Context, userID int) (string, error) {
    var name string
    // Используем QueryRowContext для запроса одной строки
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", userID).Scan(&name)
    if err != nil {
        if err == sql.ErrNoRows {
            // Это не ошибка приложения, а ожидаемый результат "не найдено"
            return "", fmt.Errorf("user not found")
        }
        // Реальная ошибка БД
        return "", err
    }
    return name, nil
}

Мой опыт также включает:

  • Транзакции: Использование db.Begin() для выполнения группы операций в атомарной транзакции с COMMIT или ROLLBACK.
  • Подготовленные запросы: Работа с db.PrepareContext() для повышения производительности часто выполняемых запросов и защиты от SQL-инъекций.
  • Миграции БД: Использование инструментов вроде golang-migrate/migrate или goose для версионирования схемы базы данных.
  • Работа с ORM: Имею представление о работе с ORM-библиотеками, такими как GORM или sqlc (генератор кода), и понимаю их преимущества и недостатки.

Ответ 18+ 🔞

А, PostgreSQL с Go? Да, конечно, имел дело, и не раз. Это как раз тот случай, когда инструмент не подводит, если его в руках не переворачивать.

Чем обычно воюю:

  • database/sql: Ну, стандарт де-факто, куда ж без него. Обёртка, которая не даёт тебе наделать совсем уж эпичных глупостей.
  • Драйверы: Раньше пихал lib/pq, как все. Потом пересел на pgx — и, ёпта, как будто с велосипеда на мотоцикл пересел. Быстрее, умнее, приятнее. Рекомендую, не пожалеешь.

А теперь главное, что многие тупо не въезжают сходу:

  1. sql.DB — это НЕ одно соединение, это ПУЛ. Представь бассейн с лежаками. Ты создаёшь его один раз при старте приложения и потом все тусят там, берут лежак, валяются, возвращают. Не надо на каждый чих создавать новый sql.DB — это пиздец как ресурсоёмко и медленно.

  2. Три кита, на которых всё держится:

    • db.QueryRowContext() — когда ждёшь ровно одну запись. Сканируешь результат в переменные и живёшь дальше. Идеально для SELECT ... WHERE id = ....
    • db.QueryContext() — когда строк много. Обязательно, блядь, ЗАКРЫВАЙ rows.Close() после использования, иначе соединение не вернётся в пул и всё накроется медным тазом. Типичная ошибка новичков.
    • db.ExecContext() — когда тебе не нужен результат в виде строк (INSERT, UPDATE). Просто сделал дело — и свободен.

Вот, смотри, как это выглядит в коде, на примере простого запроса:

import (
    "database/sql"
    "context"
    _ "github.com/jackc/pgx/v5/stdlib" // Драйвер
)

var db *sql.DB // Глобальная переменная — и пусть весь мир подождёт!

func GetUserName(ctx context.Context, userID int) (string, error) {
    var name string
    // QueryRowContext — наш выбор для одной строки
    err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", userID).Scan(&name)
    if err != nil {
        if err == sql.ErrNoRows {
            // Это не конец света, просто пользователя нет
            return "", fmt.Errorf("user not found")
        }
        // А вот это уже реальная проблема, надо разбираться
        return "", err
    }
    return name, nil
}

Что ещё в арсенале было:

  • Транзакции: db.Begin() — это святое. Группируешь операции, которые должны выполниться либо все, либо никак. Либо коммитишь успех, либо откатываешься, как краб.
  • Подготовленные выражения (Prepare) : Полезная штука для запросов, которые крутятся в цикле. И безопаснее (инъекции отсекаются), и быстрее.
  • Миграции: Без этого — никуда. Использовал golang-migrate. Версионирование схемы — это не прихоть, а необходимость, если не хочешь вручную на десятке серверов SQL-скрипты гонять.
  • ORMs и кодогенераторы: GORM — это, конечно, мощно, но иногда чувствуешь себя так, будто везешь диван на велосипеде. sqlc — другое дело, генерация типобезопасного кода из SQL — это овердохуище удобно. Хотя иногда хочется просто написать запрос руками и не париться.