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 Также в библиотеке
mapstructure для Go: полное руководство с примерами
Работа с ZIP/архивами в Swift (iOS/macOS): полное руководство
Очереди задач в Swift: GCD, OperationQueue и Swift Concurrency
Использование GraphQL в Swift: библиотеки Apollo и Relay для iOS/macOS
Boost.Filesystem для C++: полное руководство с примерами