EnglishРусский Map

Многоэтапные последовательности в Go

title
Многоэтапные последовательности в Go
type
summary
summary
Паттерн конечного автомата для многоэтапных задач, где шаги возвращают следующий шаг и тестируются отдельно
tags
golang, patterns, testing
created
2026-04-16
updated
2026-04-16
lang
ru
translation_of
go-step-sequences
source_updated
2026-04-16
translated
2026-09-01
translator
lllm/antigravity/gemini-3.7-flash-medium

Chris Lesiw описывает паттерн организации многоэтапных последовательностей в Go - таких, какие получаются при переводе shell-скриптов на Go или написании автоматизации развёртывания. Основная идея: каждый шаг возвращает следующий шаг для выполнения, образуя конечный автомат, который легко тестировать без каскадных сбоев.

Проблема

Перевод shell-скриптов на Go часто приводит к мегафункциям:

func deploy(ctx context.Context) error {
    if err := build(ctx); err != nil { return err }
    if err := test(ctx); err != nil { return err }
    if err := upload(ctx); err != nil { return err }
    if err := notify(ctx); err != nil { return err }
    return nil
}

Чтобы протестировать upload, нужно сначала успешно выполнить build и test. Ошибка в build каскадно ломает все тесты ниже по цепочке. Изолировать шаги невозможно.

Паттерн

Вдохновляясь докладом Роба Пайка (Rob Pike) 2011 года о лексерах, шаги определяются как функции, возвращающие следующий шаг:

type Func[T any] func(context.Context) (Func[T], error)

Исполняющий цикл тривиален:

for f != nil {
    f, err = f(ctx)
}

Каждый шаг выполняет свою работу и возвращает либо следующий шаг, либо nil для завершения.

Значения методов для хранения состояния

Вместо пробрасывания состояния через параметры используются значения методов (method values):

type deployer struct {
    artifact string
    version  string
}

func (d *deployer) build(ctx context.Context) (step.Func[deployer], error) {
    d.artifact = "app.tar.gz"
    return d.test, nil
}

func (d *deployer) test(ctx context.Context) (step.Func[deployer], error) {
    // use d.artifact
    return d.upload, nil
}

Значение метода d.build захватывает d в месте вызова. Шаги обращаются к общему состоянию через получатель (receiver), не засоряя сигнатуры функций.

Обобщённый параметр как пространство имён

Параметр T в Func[T] не используется во время выполнения - это пространство имён на этапе компиляции. Func[deployer] может вернуть только Func[deployer], но не Func[installer]. Это предотвращает случайное связывание шагов из разных последовательностей.

Тестирование без каскадов

Библиотека предоставляет сравнение функций во время выполнения:

func Equal[T any](a, b Func[T]) bool {
    return fullName(a) == fullName(b)
}

Проверяется, что build возвращает test, а test возвращает upload. Ошибка в build не ломает тесты для upload - тестируются переходы, а не цепочки выполнения.

func TestBuildReturnsTest(t *testing.T) {
    d := &deployer{}
    next, err := d.build(ctx)
    if !step.Equal(next, d.test) {
        t.Error("expected test step")
    }
}

Handler для отслеживания прогресса

Библиотека включает интерфейс Handler для наблюдения за выполнением шагов:

type Handler interface {
    Handle(Info)
}

Встроенный обработчик Log показывает ход выполнения:

  • ✔ успешные шаги
  • ✗ шаги с ошибками
  • ⊘ некритичные ошибки

Ортогональные задачи

Два возвращаемых значения независимы:

  • Возвращаемая функция управляет потоком выполнения (следующий шаг или nil для остановки)
  • Возвращаемая ошибка передаётся обработчикам для отчётности

Шаг может вернуть ошибку и всё равно продолжить выполнение (return d.next, err), либо завершиться без ошибки (return nil, nil).

Библиотека

Опубликована как lesiw.io/step. Полезна для скриптов развёртывания, инсталляторов, CI-пайплайнов - везде, где есть последовательные операции, требующие изолированного тестирования.

См. также go-analysis-framework - ещё один паттерн в Go для цепочек операций, а также behavior-tree - другой подход к выстраиванию последовательностей задач.