> Как реализовать миграции базы данных (Go)

Уровень: senior · Роль: backend · Категория: Технические вопросы

Компании: Лукойл

Стек: Go

> Пример ответа

Короткий ответ

Миграции в Go реализуют через специализированные библиотеки (golang-migrate, goose, atlas) либо самописный инструмент на основе embed.FS. Основная идея - версионирование схемы БД: каждый файл миграции содержит up/down SQL, применяется атомарно, состояние отслеживается в служебной таблице. Для production критичны: транзакционность, идемпотентность, возможность отката, интеграция в CI/CD и запуск перед деплоем приложения.

Подробное объяснение

Миграции решают проблему эволюции схемы БД в команде. Без них каждый разработчик меняет БД вручную, что приводит к рассинхрону. В Go подходы:

  1. Файловые миграции - SQL-файлы с версиями (например, 0001_create_users.up.sql и .down.sql). Инструмент применяет только неприменённые. Популярные: golang-migrate, goose, tern.

  2. Миграции на Go - миграция описывается кодом (например, migrate.Up(func(tx *sql.Tx) error {...})). Удобно для сложной логики, но хуже читается.

  3. Декларативный подход - описывается целевая схема, инструмент генерирует diff (atlas, skeema). Меньше контроля, но проще поддержка.

Ключевые механизмы:

  • Таблица версий - обычно schema_migrations с полями version, dirty. Инструмент блокирует таблицу или использует advisory lock для конкурентного доступа.
  • Транзакции - DDL в PostgreSQL транзакционен, в MySQL - нет. Поэтому для MySQL часто применяют миграции по одной, с проверкой идемпотентности.
  • Откат - down-миграции должны быть симметричны up. Для необратимых изменений (удаление данных) - down может быть пустым или выбрасывать ошибку.
  • Порядок применения - строго по возрастанию версий, без пропусков. Dirty-флаг сигнализирует о сбое - требуется ручное вмешательство.

На практике

Для production-системы:

  • Миграции запускаются отдельным процессом (в CI или init-контейнере), а не внутри основного приложения - иначе несколько реплик одновременно начнут миграции.
  • Используйте advisory lock (PostgreSQL) или GET_LOCK (MySQL) для предотвращения гонок.
  • Храните миграции в репозитории рядом с кодом, в папке migrations/.
  • Для встраивания в бинарник - embed.FS, чтобы не зависеть от файловой системы при деплое.
  • В CI: сначала прогон миграций на staging, затем на production, затем деплой приложения.
  • Для отката - отдельная команда migrate down, но в production часто предпочитают forward-only (новую миграцию, исправляющую проблему), чтобы избежать потери данных.

Пример кода

GO
// migrations/0001_create_users.up.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
// migrations/0001_create_users.down.sql
DROP TABLE users;
GO
// main.go - запуск миграций через golang-migrate
package main
import (
"embed"
"log"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/lib/pq"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
func runMigrations(databaseURL string) error {
source, err := iofs.New(migrationsFS, "migrations")
if err != nil {
return err
}
db, err := sql.Open("postgres", databaseURL)
if err != nil {
return err
}
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
if err != nil {
return err
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}

Как отвечать на собеседовании

Начните с определения: миграции - это версионирование схемы БД. Затем опишите два подхода: файловые SQL-миграции и код-миграции. Упомяните инструменты для Go: golang-migrate, goose, atlas. Обязательно расскажите про таблицу версий, dirty-флаг и транзакционность. Подчеркните разницу между PostgreSQL (DDL в транзакции) и MySQL (нет). Затем переходите к практике: отдельный процесс, embed.FS, advisory lock, CI/CD. Если спросят про откат - объясните trade-off между down-миграциями и forward-only. Завершите примером кода, если просят.

Что проверяет интервьюер

  • Понимание проблемы, которую решают миграции (синхронизация схемы, воспроизводимость).
  • Знание механизмов: таблица версий, блокировки, транзакции.
  • Умение выбирать инструмент под задачу, а не "всё подряд".
  • Понимание production-нюансов: конкурентный запуск, откат, CI/CD.
  • Способность объяснить trade-off между подходами (SQL vs код, файлы vs декларативный).

Типичные ошибки

  • Запуск миграций внутри основного приложения при нескольких репликах - гонки.
  • Игнорирование dirty-флага - после сбоя миграции система остаётся в неопределённом состоянии.
  • Отсутствие down-миграций или несимметричные up/down.
  • Применение миграций без транзакции там, где это возможно (PostgreSQL).
  • Хранение миграций вне репозитория или без embed - проблемы с деплоем.
  • Смешивание миграций и сидов (seed) - это разные вещи.
  • Использование migrate down в production без понимания последствий потери данных.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью