Package step runs sequences of step functions.
A step is a function that receives a context and returns the next step
to run. Step functions are typically defined as methods on a state
type, then passed to Do as method values:
type etl struct {
raw []byte
parsed []string
}
func (e *etl) extract(context.Context) (step.Func[etl], error) {
e.raw = []byte("a,b,c")
return e.transform, nil
}
func (e *etl) transform(context.Context) (step.Func[etl], error) {
e.parsed = strings.Split(string(e.raw), ",")
return e.load, nil
}
func (e *etl) load(context.Context) (step.Func[etl], error) {
fmt.Println(e.parsed)
return nil, nil
}Run the sequence by passing a context and the first step:
var e etl
if err := step.Do(ctx, e.extract); err != nil {
log.Fatal(err)
}Steps can branch by returning different functions:
func (d *deploy) install(context.Context) (step.Func[deploy], error) {
switch d.os {
case "linux":
return d.installLinux, nil
case "darwin":
return d.installDarwin, nil
}
return nil, fmt.Errorf("unsupported OS: %s", d.os)
}The returned function controls whether the sequence continues (non-nil) or stops (nil). The returned error is passed to handlers.
Do may return an *Error containing the step name:
err := step.Do(ctx, e.extract)
if stepErr, ok := errors.AsType[*step.Error](err); ok {
fmt.Println("failed at:", stepErr.Name)
}Do also checks for context cancellation before each step.
When a step returns an error but the sequence does not stop, Log
renders it with ⊘:
✔ download
⊘ install: skip
✔ configure
A Handler receives step completion events. Log provides a default
handler that prints check marks and X marks:
step.Do(ctx, e.extract, step.Log(os.Stderr))✔ extract
✔ transform
✘ load: something went wrong
Multiple handlers run in sequence:
step.Do(ctx, e.extract, step.Log(os.Stderr), step.HandlerFunc(e.handle))Since the handler is called after each step, the handler itself can be a method on the state type. This is useful for buffered logging, where step output is captured and only shown on failure:
type etl struct {
bytes.Buffer
raw []byte
parsed []string
}
func (e *etl) handle(i step.Info) {
if i.Err != nil {
io.Copy(os.Stderr, e)
}
e.Reset()
}Use Equal and Name to test transitions:
func TestInstallLinux(t *testing.T) {
d := &deploy{os: "linux"}
got, err := d.install(t.Context())
if err != nil {
t.Fatalf("install err: %v", err)
}
if want := d.installLinux; !step.Equal(got, want) {
t.Errorf("got %s, want %s", step.Name(got), step.Name(want))
}
}Equal and Name compare and identify functions by name using the
runtime, making step transitions testable without comparing function
values directly.