byob-go-cli

Thread context.Context through every runFunc

byob-lifecycle.1 contextlifecycle

Problem: a runFunc signature of func(opts *Options) error can't be cancelled. Every HTTP call, DB query, or subprocess inside it is uninterruptible, and Ctrl-C produces an orphaned child process or a stuck database connection.

Idea: follow the Go stdlib convention: every function that might block takes a context.Context as its first argument, and returns quickly when ctx.Done() fires. runFuncs are no exception — make them func(ctx context.Context, opts *Options) error.

main() constructs the root ctx (see byob-lifecycle.2 for the signal.NotifyContext setup) and threads it through root.ExecuteContext(ctx). Cobra's cmd.Context() returns that same context inside every RunE, so the plumbing is: pull ctx out of cmd.Context() and pass it to your runFunc.

Tradeoffs: every blocking call inside a command now has to accept ctx and pass it through. That's a one-time audit of your codebase, not ongoing friction. The alternative — ignoring cancellation — produces the user-hostile behavior described in the Problem.

When not to use: never. Context threading is table stakes in modern Go.

Design

// main.go — see byob-lifecycle.2 for the signal.NotifyContext wiring that
// feeds root.ExecuteContext(ctx). This decision picks up from there.

// pkg/cmd/list/list.go
type Options struct {
    IO    *iostreams.IOStreams
    Store func() (Store, error)
    Limit int
}

func NewCmdList(f *Factory, runF func(ctx context.Context, opts *Options) error) *cobra.Command {
    opts := &Options{IO: f.IOStreams, Store: f.Store}
    cmd := &cobra.Command{
        Use: "list",
        RunE: func(c *cobra.Command, args []string) error {
            if runF != nil { return runF(c.Context(), opts) }
            return listRun(c.Context(), opts)
        },
    }
    return cmd
}

func listRun(ctx context.Context, opts *Options) error {
    s, err := opts.Store()
    if err != nil { return err }
    items, err := s.List(ctx) // cancellable
    ...
}