Как в Go работать с базами данных на примере PostgreSQL? Какие основные пакеты и практики используются?

Ответ

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

Популярные драйверы для PostgreSQL:

  • pgx: Современный, высокопроизводительный драйвер с богатой функциональностью. Рекомендуется для новых проектов.
  • lib/pq: Более старый, но все еще широко используемый драйвер.

Пример подключения и выполнения запросов с pgx:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "log"

    _ "github.com/jackc/pgx/v5/stdlib" // Анонимный импорт драйвера
)

func main() {
    // Строка подключения (DSN)
    connStr := "postgres://user:password@localhost:5432/dbname?sslmode=disable"

    // sql.Open создает пул соединений. Он потокобезопасен.
    db, err := sql.Open("pgx", connStr)
    if err != nil {
        log.Fatalf("Не удалось подключиться к базе данных: %v", err)
    }
    defer db.Close() // Важно закрыть пул при завершении работы

    // Проверка реального соединения с базой
    if err := db.Ping(); err != nil {
        log.Fatalf("Не удалось проверить соединение: %v", err)
    }

    // 1. Выполнение запросов без возврата строк (INSERT, UPDATE, DELETE)
    // Используйте параметризованные запросы ($1, $2) для защиты от SQL-инъекций!
    res, err := db.ExecContext(context.Background(), 
        "INSERT INTO users(name, age) VALUES($1, $2)", "Bob", 30)
    if err != nil {
        log.Printf("Ошибка вставки: %v", err)
    }
    // Можно получить количество затронутых строк
    rowsAffected, _ := res.RowsAffected()
    fmt.Printf("Добавлено %d строкn", rowsAffected)

    // 2. Запрос одной строки
    var name string
    var age int
    err = db.QueryRowContext(context.Background(), 
        "SELECT name, age FROM users WHERE id = $1", 1).
        Scan(&name, &age)
    if err != nil {
        // Важно обрабатывать sql.ErrNoRows отдельно
        if err == sql.ErrNoRows {
            fmt.Println("Пользователь с id=1 не найден")
        } else {
            log.Printf("Ошибка запроса одной строки: %v", err)
        }
    }

    // 3. Запрос нескольких строк
    rows, err := db.QueryContext(context.Background(), "SELECT id, name FROM users WHERE age > $1", 25)
    if err != nil {
        log.Printf("Ошибка запроса нескольких строк: %v", err)
    }
    defer rows.Close() // Обязательно закрывать rows!

    for rows.Next() { // Итерация по результатам
        var id int
        var currentName string
        if err := rows.Scan(&id, ¤tName); err != nil {
            log.Printf("Ошибка сканирования строки: %v", err)
        }
        fmt.Printf("ID: %d, Name: %sn", id, currentName)
    }
    // Проверка на ошибки, возникшие во время итерации
    if err := rows.Err(); err != nil {
        log.Printf("Ошибка итерации: %v", err)
    }
}

Лучшие практики:

  1. Защита от SQL-инъекций: Всегда используйте плейсхолдеры ($1, $2 для Postgres) вместо конкатенации строк.
  2. Управление соединениями: sql.Open создает пул соединений. Не нужно открывать/закрывать его на каждый запрос. Создайте один раз при старте приложения.
  3. defer.Close(): Всегда закрывайте объекты sql.DB и sql.Rows с помощью defer, чтобы избежать утечек ресурсов.
  4. Обработка ошибок: Тщательно проверяйте все ошибки, возвращаемые функциями database/sql, особенно sql.ErrNoRows для QueryRow.
  5. Использование Context: Для запросов, которые могут выполняться долго, передавайте context.Context для управления таймаутами и отменой.
  6. Альтернативы: Для сложных приложений рассмотрите использование кодогенераторов (например, sqlc) для создания типобезопасного кода из SQL-запросов или ORM (например, GORM), помня о компромиссах в производительности и контроле.