# Many-Step Sequences in Go

Chris Lesiw describes a pattern for organizing multi-step sequences in Go — the kind you get when converting shell scripts or writing deployment automation. The core idea: each step returns the next step to execute, forming a state machine that's easy to test without cascading failures.

## The problem

Shell-to-Go conversions often produce mega-functions:

```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
}
```

Testing `upload` requires `build` and `test` to succeed first. A bug in `build` cascades into every downstream test. You can't isolate steps.

## The pattern

Inspired by Rob Pike's 2011 lexer talk, define steps as functions that return the next step:

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

The driver is trivial:

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

Each step does its work, then returns either the next step or `nil` to terminate.

## Method values for state

Instead of threading state through parameters, use method values:

```go
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
}
```

The method value `d.build` captures `d` at the call site. Steps access shared state through the receiver without polluting signatures.

## Generic parameter as namespace

The `T` in `Func[T]` isn't used at runtime — it's a compile-time namespace. `Func[deployer]` can only return `Func[deployer]`, not `Func[installer]`. This prevents accidentally chaining steps from different sequences.

## Testing without cascades

The library provides runtime function comparison:

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

Test that `build` returns `test`, that `test` returns `upload`. A bug in `build` doesn't cascade into `upload` tests — you're testing transitions, not execution chains.

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

## Handler for progress

The library includes a `Handler` interface for observing step completion:

```go
type Handler interface {
    Handle(Info)
}
```

Built-in `Log` handler shows progress:
- ✔ successful steps
- ✗ failed steps
- ⊘ non-fatal errors

## Orthogonal concerns

The two return values are independent:
- Returned function controls flow (next step or `nil` to stop)
- Returned error goes to handlers for reporting

A step can return an error and still continue (`return d.next, err`), or stop without error (`return nil, nil`).

## The library

Published as [`lesiw.io/step`](https://pkg.go.dev/lesiw.io/step). Useful for deployment scripts, installers, CI pipelines — anywhere you have sequential operations that deserve individual testing.

See also [[go-analysis-framework]] for another Go pattern involving chained operations, [[behavior-tree]] for a different approach to task sequencing.
