Avatar ·

Рекомендуемый способ подключения к MySQL из Go: драйверы, пулы соединений и best practices

Всем привет! Я начинающий Go-разработчик, перехожу с Python, где всё было просто с SQLAlchemy. Сейчас пишу небольшой REST API на Go (чистый net/http) и мне нужно подключиться к MySQL.

Я прочитал несколько туториалов, но они противоречат друг другу. Кто-то использует go-sql-driver/mysql, кто-то database/sql напрямую, а кто-то рекомендует ORM вроде GORM. Я совсем запутался.

Вот пример моего текущего кода, который я набросал, но он выглядит "на коленке":

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    // Тут я не понимаю: нужно ли вызывать db.Ping()?
    // Нужно ли настраивать пул соединений?
    // Как правильно обрабатывать ошибки?

    rows, err := db.Query("SELECT id, name FROM users")
    if err != nil {
        panic(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err = rows.Scan(&id, &name)
        if err != nil {
            panic(err)
        }
        fmt.Println(id, name)
    }
}

Конкретные вопросы:

  1. Какой драйвер считается "стандартом" для MySQL в 2024 году?
  2. Нужно ли всегда вызывать db.Ping() после sql.Open()?
  3. Как правильно настроить пул соединений (MaxOpenConns, MaxIdleConns, ConnMaxLifetime)?
  4. Стоит ли использовать ORM (например, GORM) или писать чистый SQL?
  5. Как правильно закрывать соединения и обрабатывать ошибки в production-коде?

Буду благодарен за развёрнутый ответ с примерами production-ready кода.

<<>>

Рекомендуемый стек: database/sql + go-sql-driver/mysql

На текущий момент (2024 год) стандартом де-факто является связка:

  • database/sql — встроенный пакет Go (не требует установки);
  • github.com/go-sql-driver/mysql — самый популярный драйвер MySQL (почти 15k звёзд на GitHub).

Альтернативы вроде github.com/siddontang/go-mysql или github.com/ziutek/mymysql менее популярны и хуже поддерживаются. ORM (GORM, Ent) — это отдельный вопрос, о нём ниже.

db.Ping() — обязательно, но не так, как вы думаете

sql.Open() не создаёт реального соединения с БД. Он только проверяет синтаксис DSN и инициализирует пул. Реальное соединение устанавливается лениво — при первом запросе.

Поэтому всегда вызывайте db.Ping() при старте приложения, чтобы убедиться, что БД доступна:

db, err := sql.Open("mysql", dsn)
if err != nil {
    log.Fatalf("Failed to open DB: %v", err)
}

// ВАЖНО: проверяем, что БД реально доступна
if err = db.Ping(); err != nil {
    log.Fatalf("Failed to ping DB: %v", err)
}

В production используйте db.PingContext(ctx) с таймаутом:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err = db.PingContext(ctx); err != nil {
    log.Fatalf("DB not reachable: %v", err)
}

Настройка пула соединений — критически важно

Без настройки пула Go создаёт неограниченное количество соединений, что убивает MySQL. Вот production-конфигурация:

db.SetMaxOpenConns(25)                 // максимум открытых соединений
db.SetMaxIdleConns(10)                 // сколько держать в режиме ожидания
db.SetConnMaxLifetime(5 * time.Minute) // максимальное время жизни соединения
db.SetConnMaxIdleTime(1 * time.Minute) // сколько соединение может быть без дела

Почему так:

  • MaxOpenConns — предотвращает "утечку" соединений. 25 — хорошее значение для большинства приложений.
  • MaxIdleConns — ускоряет запросы (не нужно создавать новое соединение). 10 — разумный минимум.
  • ConnMaxLifetime — пересоздаёт соединения, чтобы избежать проблем с сетевыми таймаутами MySQL (по умолчанию 8 часов).
  • ConnMaxIdleTime — закрывает неиспользуемые соединения (экономит ресурсы).

Production-ready пример подключения

package main

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

    _ "github.com/go-sql-driver/mysql"
)

type DBConfig struct {
    User     string
    Password string
    Host     string
    Port     string
    DBName   string
}

func (c DBConfig) DSN() string {
    return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        c.User, c.Password, c.Host, c.Port, c.DBName)
}

func NewDB(cfg DBConfig) (*sql.DB, error) {
    db, err := sql.Open("mysql", cfg.DSN())
    if err != nil {
        return nil, fmt.Errorf("sql.Open: %w", err)
    }

    // Настройка пула
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(10)
    db.SetConnMaxLifetime(5 * time.Minute)
    db.SetConnMaxIdleTime(1 * time.Minute)

    // Проверка соединения с таймаутом
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    if err = db.PingContext(ctx); err != nil {
        return nil, fmt.Errorf("db.Ping: %w", err)
    }

    return db, nil
}

func main() {
    cfg := DBConfig{
        User:     "app_user",
        Password: "strong_password",
        Host:     "127.0.0.1",
        Port:     "3306",
        DBName:   "myapp",
    }

    db, err := NewDB(cfg)
    if err != nil {
        log.Fatalf("Failed to connect: %v", err)
    }
    defer db.Close()

    // Пример запроса с контекстом
    ctx := context.Background()
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE active = ?", true)
    if err != nil {
        log.Fatalf("Query failed: %v", err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        if err := rows.Scan(&id, &name); err != nil {
            log.Printf("Scan error: %v", err)
            continue
        }
        fmt.Printf("User: %d - %s\n", id, name)
    }

    // Всегда проверяем rows.Err()
    if err = rows.Err(); err != nil {
        log.Printf("Rows iteration error: %v", err)
    }
}

GORM или чистый SQL?

Для простых CRUD — GORM (или Ent), если вам нужна скорость разработки и вы не боитесь "магии".

Для сложных запросов, высокой нагрузки или микросервисов — чистый SQL. ORM скрывает SQL, но когда возникают проблемы с производительностью, вы теряете контроль. Кроме того, GORM генерирует не всегда оптимальные запросы.

Мой совет: начинайте с чистого database/sql, чтобы понять, как работают соединения и пулы. Когда почувствуете боль от написания однотипного кода — подключайте GORM или напишите свой тонкий слой абстракции.

Обработка ошибок в production

  • Никогда не используйте panic() в production-коде. Возвращайте ошибки.
  • Используйте context.WithTimeout для всех запросов к БД.
  • Проверяйте rows.Err() после цикла rows.Next().
  • Логируйте ошибки с контекстом: log.Printf("db query failed: %v", err).
  • Для транзакций используйте db.BeginTx(ctx, nil).
<<>>

Альтернатива: использование GORM для быстрого старта

Если вы не хотите писать шаблонный код для каждой таблицы, GORM — отличный выбор. Он автоматически управляет пулом соединений, миграциями и предо