Рекомендуемый способ подключения к 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)
}
}
Конкретные вопросы:
- Какой драйвер считается "стандартом" для MySQL в 2024 году?
- Нужно ли всегда вызывать
db.Ping()послеsql.Open()? - Как правильно настроить пул соединений (MaxOpenConns, MaxIdleConns, ConnMaxLifetime)?
- Стоит ли использовать ORM (например, GORM) или писать чистый SQL?
- Как правильно закрывать соединения и обрабатывать ошибки в 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 — отличный выбор. Он автоматически управляет пулом соединений, миграциями и предо