goose для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

goose для Go: полное руководство с примерами

Что это и зачем нужна

Библиотека goose (Go Object-Oriented SQL Environment) — это мощный инструмент для управления миграциями баз данных в экосистеме Go. В отличие от встроенных решений, goose предоставляет декларативный подход к версионированию схемы БД, позволяя разработчикам описывать изменения в SQL или Go-коде. Основная задача goose — автоматизировать процесс применения и отката изменений структуры базы данных, что критически важно для командной разработки и CI/CD пайплайнов.

Зачем нужна goose? Во-первых, она решает проблему синхронизации схемы БД между разработчиками: каждый участник команды может применить последние миграции одной командой. Во-вторых, goose поддерживает версионирование — каждая миграция имеет уникальный номер, что позволяет точно определить, какие изменения уже применены. В-третьих, библиотека умеет работать с различными СУБД (PostgreSQL, MySQL, SQLite, ClickHouse и другие) через единый интерфейс, что упрощает переключение между окружениями.

Ключевое преимущество goose перед аналогами (например, golang-migrate/migrate) — это поддержка миграций на Go, а не только на SQL. Вы можете писать сложные миграции с бизнес-логикой, использовать транзакции и обращаться к внешним API. Кроме того, goose предоставляет встроенную поддержку embed (встраивание миграций в бинарник), что идеально для production-сред.

Установка

Установка goose выполняется через стандартный менеджер пакетов Go. Для начала убедитесь, что у вас установлен Go версии 1.18 или выше.

go install github.com/pressly/goose/v3/cmd/goose@latest

После установки проверьте версию:

goose --version

Для использования goose как библиотеки в проекте добавьте зависимость:

go get github.com/pressly/goose/v3

Также потребуется драйвер для вашей СУБД. Например, для PostgreSQL:

go get github.com/lib/pq

Для MySQL:

go get github.com/go-sql-driver/mysql

Для SQLite:

go get github.com/mattn/go-sqlite3

Быстрый старт — минимальный рабочий пример

Создадим простой проект для демонстрации базовых возможностей goose.

1. Создайте директорию проекта и инициализируйте модуль:

mkdir goose-demo && cd goose-demo
go mod init goose-demo

2. Создайте директорию для миграций:

mkdir migrations

3. Создайте первую миграцию на SQL. Файл migrations/001_create_users.sql:

-- +goose Up
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- +goose Down
DROP TABLE users;

4. Создайте файл main.go для программного запуска миграций:

package main

import (
    "database/sql"
    "embed"
    "log"

    "github.com/pressly/goose/v3"
    _ "github.com/lib/pq"
)

//go:embed migrations/*.sql
var migrations embed.FS

func main() {
    // Подключение к базе данных
    db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres password=postgres dbname=testdb sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Устанавливаем источник миграций из embed.FS
    goose.SetBaseFS(migrations)

    // Выполняем миграции до последней версии
    if err := goose.Up(db, "migrations"); err != nil {
        log.Fatal(err)
    }
    log.Println("Миграции успешно применены!")
}

5. Запустите программу:

go run main.go

После выполнения в базе данных появится таблица users, а также служебная таблица goose_db_version, в которой goose отслеживает применённые миграции.

Основные методы/функции goose

1. goose.Up

Сигнатура: func Up(db *sql.DB, dir string, opts ...Option) error

Что делает: Применяет все ожидающие миграции из указанной директории. Если миграции уже были частично применены, применяет только новые.

err := goose.Up(db, "migrations")
if err != nil {
    log.Fatal(err)
}

2. goose.Down

Сигнатура: func Down(db *sql.DB, dir string, opts ...Option) error

Что делает: Откатывает последнюю применённую миграцию. Выполняет секцию -- +goose Down.

err := goose.Down(db, "migrations")
if err != nil {
    log.Fatal(err)
}

3. goose.Redo

Сигнатура: func Redo(db *sql.DB, dir string, opts ...Option) error

Что делает: Откатывает последнюю миграцию и сразу применяет её заново. Полезно для быстрой итерации при разработке.

err := goose.Redo(db, "migrations")
if err != nil {
    log.Fatal(err)
}

4. goose.Status

Сигнатура: func Status(db *sql.DB, dir string, opts ...Option) error

Что делает: Выводит статус всех миграций: какие применены, какие ожидают, их версии и время выполнения.

err := goose.Status(db, "migrations")
if err != nil {
    log.Fatal(err)
}
// Вывод:
//     Applied At                  Migration
//     =======================================
//     Mon Jan 15 10:30:00 2024 -- 001_create_users.sql
//     Pending                  -- 002_add_age_column.sql

5. goose.Version

Сигнатура: func Version(db *sql.DB, dir string, opts ...Option) (int64, error)

Что делает: Возвращает номер текущей применённой миграции. Если миграций нет, возвращает 0.

version, err := goose.Version(db, "migrations")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Текущая версия миграций: %d\n", version)

6. goose.Create

Сигнатура: func Create(db *sql.DB, dir, name, migrationType string, opts ...Option) error

Что делает: Создаёт новый файл миграции с указанным именем и типом (sql или go). Автоматически генерирует номер версии на основе временной метки.

err := goose.Create(db, "migrations", "add_email_index", "sql")
if err != nil {
    log.Fatal(err)
}
// Создаст файл: migrations/20240115103000_add_email_index.sql

7. goose.UpByOne

Сигнатура: func UpByOne(db *sql.DB, dir string, opts ...Option) error

Что делает: Применяет только одну следующую миграцию (а не все сразу). Полезно для пошагового применения.

for i := 0; i < 3; i++ {
    err := goose.UpByOne(db, "migrations")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Применена миграция %d\n", i+1)
}

8. goose.DownTo

Сигнатура: func DownTo(db *sql.DB, dir string, version int64, opts ...Option) error

Что делает: Откатывает миграции до указанной версии (не включая её). Позволяет откатиться к конкретному состоянию схемы.

// Откатить до версии 2 (оставить только 2 миграции)
err := goose.DownTo(db, "migrations", 2)
if err != nil {
    log.Fatal(err)
}

9. goose.Reset

Сигнатура: func Reset(db *sql.DB, dir string, opts ...Option) error

Что делает: Откатывает все применённые миграции, возвращая базу данных в исходное состояние (без миграций).

err := goose.Reset(db, "migrations")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Все миграции откачены")

10. goose.SetBaseFS

Сигнатура: func SetBaseFS(fsys fs.FS)

Что делает: Устанавливает файловую систему для поиска миграций. Используется с embed.FS для встраивания миграций в бинарник.

//go:embed migrations/*.sql
var migrations embed.FS

func main() {
    goose.SetBaseFS(migrations)
    // Теперь goose будет искать миграции во встроенной файловой системе
    err := goose.Up(db, "migrations")
}

Реальный практический пример (100-200 строк кода)

Разработаем полноценный пример: приложение для управления заказами с миграциями на Go (с бизнес-логикой) и SQL.

Структура проекта:

goose-orders/
├── main.go
├── migrations/
│   ├── 001_create_orders.sql
│   └── 002_seed_data.go
└── go.mod

Файл migrations/001_create_orders.sql:

-- +goose Up
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    user_id INT NOT NULL,
    product_name VARCHAR(255) NOT NULL,
    quantity INT NOT NULL DEFAULT 1,
    price DECIMAL(10,2) NOT NULL,
    status VARCHAR(50) DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);

-- +goose Down
DROP TABLE IF EXISTS orders;

Файл migrations/002_seed_data.go:

package migrations

import (
    "database/sql"
    "fmt"
    "math/rand"
    "time"

    "github.com/pressly/goose/v3"
)

func init() {
    goose.AddMigration(Up_002, Down_002)
}

func Up_002(tx *sql.Tx) error {
    // Вставляем 100 тестовых заказов
    products := []string{"Laptop", "Mouse", "Keyboard", "Monitor", "Headphones"}
    statuses := []string{"pending", "processing", "shipped", "delivered", "cancelled"}

    for i := 0; i < 100; i++ {
        userID := rand.Intn(50) + 1
        product := products[rand.Intn(len(products))]
        quantity := rand.Intn(5) + 1
        price := float64(rand.Intn(1000)+50) + 0.99
        status := statuses[rand.Intn(len(statuses))]
        createdAt := time.Now().Add(-time.Duration(rand.Intn(30*24)) * time.Hour)

        _, err := tx.Exec(
            `INSERT INTO orders (user_id, product_name, quantity, price, status, created_at)
             VALUES ($1, $2, $3, $4, $5, $6)`,
            userID, product, quantity, price, status, createdAt,
        )
        if err != nil {
            return fmt.Errorf("failed to insert seed data: %w", err)
        }
    }
    return nil
}

func Down_002(tx *sql.Tx) error {
    _, err := tx.Exec("DELETE FROM orders")
    return err
}

Файл main.go:

package main

import (
    "database/sql"
    "embed"
    "fmt"
    "log"
    "os"

    "github.com/pressly/goose/v3"
    _ "github.com/lib/pq"
)

//go:embed migrations/*.sql migrations/*.go
var migrations embed.FS

func main() {
    // Конфигурация из переменных окружения
    dbHost := getEnv("DB_HOST", "localhost")
    dbPort := getEnv("DB_PORT", "5432")
    dbUser := getEnv("DB_USER", "postgres")
    dbPass := getEnv("DB_PASS", "postgres")
    dbName := getEnv("DB_NAME", "orders_db")

    connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
        dbHost, dbPort, dbUser, db                    

Рекомендаций