controls¶
A small service-lifecycle supervisor for long-running Go processes —
concurrent startup, health probes, ordered (reverse-registration) shutdown, and self-healing restarts,
behind one Controller, as a light, framework-free library.
controls is the same supervisor behind go-tool-base's
service commands, extracted so any project can register its services (HTTP/gRPC
servers, background workers, schedulers) and let the Controller start them
concurrently, monitor their health, optionally take ownership of
SIGINT/SIGTERM, and drive a bounded graceful shutdown — without pulling
in the framework.
The light-footprint promise¶
The module graph is deliberately tiny. go.mod declares exactly one external
dependency, cockroachdb/errors — no
config framework, no TUI, no OpenTelemetry, no go-tool-base. A
depfootprint_test.go guard fails the build if any forbidden dependency ever
enters the graph.
The only logging seam is the standard library's *slog.Logger, injected via
WithLogger. Supply one, or supply none — it defaults to a discard handler, so
logging is never mandatory and never a dependency you must adopt.
Design¶
- Framework-free. One external dependency (
cockroachdb/errors); one logging seam (*slog.Logger). Everything else is functional options. - Correct concurrency. Idempotent
Start/Stop; goroutines that terminate on shutdown rather than leak or busy-spin; a restart supervisor that distinguishes clean start / cancellation / failure and never floods the error channel; force-stop at the shutdown deadline; readiness that fails closed until the first async health check has actually run. Race-clean under-race. - Transport-agnostic health.
Status()/Liveness()/Readiness()return plainHealthReportvalues. The module does not open ports or register HTTP/gRPC handlers — you wire the reports into whatever transport your process already speaks (see Add health checks).
Quick start¶
package main
import (
"context"
"gitlab.com/phpboyscout/go/controls"
)
func main() {
c := controls.NewController(context.Background())
c.Register("api",
controls.WithStart(func(ctx context.Context) error {
// start serving; return when ctx is cancelled
<-ctx.Done()
return ctx.Err()
}),
controls.WithStop(func(ctx context.Context) {
// graceful shutdown, bounded by ctx
}),
)
c.Start() // launches the services and returns; installs no signal handler
c.Wait() // blocks until Stop (or the parent context) drains the shutdown sequence
}
Add controls.WithSignals() to NewController when this process is the
outermost layer and Ctrl-C should stop it — see Handle graceful shutdown &
signals.
Key concepts¶
Controller— the supervisor.Registerservices beforeStart;Waitblocks until the full shutdown sequence has completed and every supervisor goroutine has unwound (it requires context-respecting start callbacks —WaitContextis the deadline-bounded variant for services that may wrap cancellation-ignoring third-party code).- Health probes — attach
WithStatus/WithLiveness/WithReadinessto a service, or register standaloneHealthChecks (sync, or async with anInterval).Status()/Liveness()/Readiness()aggregate them into aHealthReport. - Restart policy —
WithRestartPolicyenables self-healing with exponential backoff, aMaxRestartscap, a health-failure threshold, and a consecutive-failure counter that resets after a healthy window. - Options —
WithLogger,WithShutdownTimeout,WithSignals(standalone mains), andWithValidError(exempt expected terminal errors likehttp.ErrServerClosedfrom the restart count).
Install¶
Where to go next¶
The documentation follows the Diátaxis framework:
- Getting started — a learning-oriented walkthrough: register one service, start it, and shut down cleanly on Ctrl-C.
- How-to guides — task-oriented recipes:
- Explanation — understanding-oriented background:
-
Reference — every option, field and default, with what happens when it is set wrongly:
The generated listing of every exported symbol, with runnable
Exampletests, is on pkg.go.dev.
Using go-tool-base? The framework wires this supervisor into its service commands and exposes the
HealthReports through its ownpkg/httpandpkg/grpchandlers. That glue lives in go-tool-base, not here; this module is transport- and framework-agnostic.
Further reading¶
The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.
Ask phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.