Как в Go отслеживать и обрабатывать ошибки соединения с базой данных?

Ответ

В Go для отслеживания и обработки ошибок соединения с БД используется комбинация нескольких подходов, так как пакет database/sql абстрагирует пул соединений.

  1. Проверка при инициализации (sql.Open) sql.Open не устанавливает соединение с БД, а лишь подготавливает структуру *sql.DB. Ошибка на этом этапе означает неверные параметры (например, неправильный DSN или несуществующий драйвер).

    db, err := sql.Open("postgres", "user=... password=... ...")
    if err != nil {
        // Ошибка конфигурации, а не сети
        log.Fatalf("Invalid DB config: %v", err)
    }
  2. Проверка реального соединения (db.PingContext) Это основной способ проверить, что соединение с базой данных может быть установлено. Его вызывают сразу после sql.Open.

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := db.PingContext(ctx); err != nil {
        log.Fatalf("DB connection failed: %v", err)
    }
  3. Обработка ошибок при выполнении запросов Любой вызов (QueryContext, ExecContext, BeginTx) может вернуть ошибку, связанную с потерей соединения. Пакет database/sql пытается автоматически переподключиться при некоторых временных ошибках, но если это не удается, ошибка будет возвращена приложению.

    rows, err := db.QueryContext(ctx, "SELECT id FROM users")
    if err != nil {
        // Здесь может быть ошибка сети, таймаут или другая проблема с соединением
        log.Printf("Query failed: %v", err)
        // Здесь можно реализовать логику повторных попыток (retry)
        return
    }
  4. Реализация логики повторных попыток (Retry) Для временных сбоев сети полезно реализовать механизм retry с экспоненциальной выдержкой (exponential backoff), чтобы не перегружать БД запросами на переподключение.

    // Псевдокод
    for i := 0; i < maxRetries; i++ {
        err = db.Ping()
        if err == nil {
            break // Успешно
        }
        // Ожидаем (1s, 2s, 4s, ...)
        time.Sleep(time.Second * (1 << i))
    }
  5. Мониторинг состояния пула соединений *sql.DB предоставляет статистику пула, которая помогает отслеживать его здоровье.

    stats := db.Stats()
    // stats.OpenConnections - количество открытых соединений
    // stats.WaitCount - сколько раз запросы ждали свободного соединения
    // stats.WaitDuration - общее время ожидания

    Эти метрики полезно экспортировать в систему мониторинга (Prometheus, Grafana) для долгосрочного анализа.

Ответ 18+ 🔞

А, слушай, вот этот ваш Go с его базой данных — это же просто отдельный вид искусства, блядь! Ну, типа, как будто ты пытаешься наладить связь с космосом, а там тебе отвечают на языке древних мартышек, ёпта.

Смотри, вот ты пишешь sql.Open — и думаешь, что всё, хуй с горы, соединение установлено! Ан нет, сука! Эта функция — она просто как будто говорит: «О, привет, я тут теоретически знаю, как подключиться, но реально я нихуя делать не буду, пока ты не попросишь». Это как взять телефон и не набрать номер, а просто держать его в руке, надеясь, что само позвонит. Ошибка тут будет только если ты вообще не ту строку подключения вписал или драйвер забыл, ну, полный распиздяй.

db, err := sql.Open("postgres", "user=... password=... ...")
if err != nil {
    // Вот тут ошибка — это ты просто конфиг накосячил, мудак
    log.Fatalf("Invalid DB config: %v", err)
}

А чтобы реально проверить, жива ли база, надо ей пингануть! db.PingContext — это как крикнуть в тёмную пещеру: «Эй, там кто есть?!» И если через пять секунд (таймаут, блядь, не забудь!) тебе не ответили эхом — всё, пиздец, связи нет.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := db.PingContext(ctx); err != nil {
    log.Fatalf("DB connection failed: %v", err) // Вот тут уже реальная проблема, сеть легла или база сдохла
}

Дальше — самое весёлое. Ты делаешь запрос, а он тебе возвращает ошибку. И кто виноват? Может, сеть отвалилась, может, база ушла в запой, а может, таймаут просто выстрелил. Пакет database/sql — он, конечно, умный, пытается сам переподключиться, но не всегда, блядь, получается. Так что готовься ловить эти ошибки на каждом углу.

rows, err := db.QueryContext(ctx, "SELECT id FROM users")
if err != nil {
    // А вот тут уже может быть всё что угодно: от 'connection reset by peer' до 'i'm a teapot', ёпта
    log.Printf("Query failed: %v", err)
    // И вот тут самое время включить логику повторных попыток, если ошибка временная
    return
}

И про повторные попытки — это вообще отдельная песня, блядь! Нельзя просто так тупо слать запросы раз в секунду. Надо делать exponential backoff — это когда ты ждёшь сначала секунду, потом две, потом четыре, и так далее. Как будто ты стучишься в дверь к соседу, который, возможно, просто в наушниках: сначала тихо, потом громче, потом уже ногой, блядь!

// Псевдокод, но суть ясна
for i := 0; i < maxRetries; i++ {
    err = db.Ping()
    if err == nil {
        break // Ура, прорвались!
    }
    // Ждём: 1с, 2с, 4с... чтоб не спамить как долбоёб
    time.Sleep(time.Second * (1 << i))
}

И напоследок, чтобы вообще спать спокойно, смотри статистику пула соединений. Это как датчики в машине: если OpenConnections зашкаливает, а WaitDuration растёт как сумасшедший — значит, твоя база задыхается, и скоро будет большой-большой бум, нахуй!

stats := db.Stats()
// stats.OpenConnections — сколько соединений висит
// stats.WaitCount — сколько запросов в очереди тупо ждали
// stats.WaitDuration — сколько в сумме все прождали, можно в Prometheus засунуть и паниковать на графиках

Короче, работа с БД в Go — это не «написал и забыл». Это постоянная слежка, как за капризным ребёнком, который то сеть роняет, то таймауты выставляет, то вообще молчит как Герасим из того рассказа, блядь. Муму-муму, сука, только и слышно!