Многоэтапные последовательности в Go
- title
- Многоэтапные последовательности в Go
- type
- summary
- summary
- Паттерн конечного автомата для многоэтапных задач, где шаги возвращают следующий шаг и тестируются отдельно
- tags
- golang, patterns, testing
- sources
- many-step-sequences-in-go
- 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 - другой подход к выстраиванию последовательностей задач.