github.com/go-srvc/srvc
go get github.com/go-srvc/srvc@v1.12.1
README
Simple, Safe, and Modular Service Runner
srvc library provides a simple but powerful interface with zero external dependencies for running service modules.
The library itself stays dependency free: it ships the runner, the Module interface, and a handful of thin adapters (InitMod, RunMod, StopMod, CtxMod, IdleMod) that turn plain functions into modules. They are glue, nothing more. Modules that wrap real dependencies and carry their own domain logic, such as HTTP servers, signal handling, databases, and telemetry, live in github.com/go-srvc/mods.
Use Case
Normally Go services are composed of multiple "modules" which each run in their own goroutine such as http server, signal listener, kafka consumer, ticker, etc. These modules should remain alive throughout the lifecycle of the whole service, and if one goes down, graceful exit should be executed to avoid "zombie" services. srvc takes care of all this via a simple module interface.
Ready made modules with batteries included can be found under github.com/go-srvc/mods, while this repo keeps only the runner and the function adapters.
Usage
Main package
package main
import (
"fmt"
"net/http"
"github.com/go-srvc/mods/httpmod"
"github.com/go-srvc/mods/logmod"
"github.com/go-srvc/mods/metermod"
"github.com/go-srvc/mods/sigmod"
"github.com/go-srvc/mods/sqlxmod"
"github.com/go-srvc/mods/tracemod"
"github.com/go-srvc/srvc"
)
func main() {
db := sqlxmod.New()
srvc.RunAndExit(
logmod.New(),
sigmod.New(),
tracemod.New(),
metermod.New(),
db,
httpmod.New(
httpmod.WithAddr(":8080"),
httpmod.WithHandler(handler(db)),
),
)
}
func handler(db *sqlxmod.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := db.DB().PingContext(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprint(w, "OK")
})
}
Implementing custom modules
package main
import "github.com/go-srvc/srvc"
func main() {
srvc.RunAndExit(&MyMod{})
}
type MyMod struct {
done chan struct{}
}
func (m *MyMod) Init() error {
m.done = make(chan struct{})
return nil
}
// Run should block until the module is stopped.
// If you don't have a blocking operation, you can use done channel to block.
func (m *MyMod) Run() error {
<-m.done
return nil
}
func (m *MyMod) Stop() error {
defer close(m.done)
return nil
}
func (m *MyMod) ID() string { return "MyMod" }
Function modules
InitMod, RunMod, StopMod, CtxMod, and IdleMod wrap plain functions into a module when the full interface is not needed. They are deliberately minimal adapters with no logic of their own; anything that needs configuration, defaults, or dependency specific behaviour belongs in a real module, either your own or one from mods.
package main
import "github.com/go-srvc/srvc"
func main() {
srvc.RunAndExit(
// Called during init, Run blocks until shutdown.
srvc.InitMod("migrations", runMigrations),
// Called during stop, in reverse order like any other module.
srvc.StopMod("flush", flushBuffers),
// Called during run, service shuts down once it returns.
srvc.RunMod("job", doWork),
// Called during run with a context that is canceled on shutdown.
srvc.CtxMod("consumer", consume),
// Init and stop pair without any work of its own during run.
srvc.IdleMod("db", openDB, closeDB),
)
}
RunMod and CtxMod have no-op Init, and their function must return on its own for the service to shut down. CtxMod cancels the context passed to its function when Stop is called, so a function that respects the context returns on shutdown. InitMod, StopMod, and IdleMod have no work of their own during run, so their Run blocks until the service shuts down.
Optional modules
Optional toggles a module with a flag. A disabled module is dropped before the init phase, so none of its Init, Run, or Stop functions are called, and its ID is logged as disabled. The module is still constructed by the caller, so keep heavy work in Init. A disabled module may be nil.
srvc.RunAndExit(
srvc.IdleMod("db", openDB, closeDB),
srvc.Optional(cfg.MetricsEnabled, metrics.New()),
)
Lifecycle
Run executes modules through a deterministic lifecycle:
- Init is called sequentially in the order modules are passed. If any
Initreturns an error, the loop stops andStopis called on already-initialized modules in reverse order. Uninitialized modules never getInitorStop. - Run is started for each successfully initialized module in its own goroutine. Start order is not guaranteed.
- When the first
Runreturns (with or without error), the service moves to shutdown. - Stop is called sequentially in reverse order on every initialized module. Each module's
Stopmust cause itsRunto return. Runblocks until everyRungoroutine has returned, then returns the joined errors fromInit,Run, andStop(ornil).
Panic recovery
Panics inside Init, Run, or Stop are recovered. The stack trace is logged, and the panic is converted into an error wrapping srvc.ErrModulePanic so other modules can still shut down gracefully.
Exit behaviour
RunAndExit calls os.Exit(1) if Run returns any error, and returns normally on success.
Contracts modules must uphold
Stopmust makeRunreturn. IfRunignoresStop,srvc.Runwill block in its final wait. There is no built-in shutdown timeout, so a stuck module hangs the service.IDshould return a stable, unique identifier used for log attribution.
Why no context.Context on lifecycle methods?
Init, Run, and Stop deliberately take no context.Context. Shutdown deadlines are already enforced by the platform the service runs on (Kubernetes terminationGracePeriodSeconds, systemd TimeoutStopSec, ECS stopTimeout), and a Go-side deadline cannot extend past the platform SIGKILL. Adding context plumbing to the interface would expand the surface without giving modules any extra safety. Modules that need a startup or shutdown budget for their own internal calls can take a context via their own options without changing Module. Functions that already take a context can be wrapped with CtxMod, which keeps the plumbing in the helper instead of the interface.
Acknowledgements
This library is something I have found myself writing over and over again in every project I been part of. One of the iterations can be found under https://github.com/elisasre/go-common.
Overview
Package srvc provides simple but powerful Run functionality on top of Module abstraction. Ready made modules can be found under: github.com/go-srvc/mods.
Constants
const ErrModulePanic = ErrStr("module recovered from panic")Variables
var JoinErrors = errors.JoinJoinErrors is used by srvc to combine multiple errors into one. Override it to plug in custom multi-error formatting.
Functions
func Run
func Run(modules ...Module) errorRun will run all given modules using following control flow:
- Exec Init() for each module in order. If any Init() returns error the Init loop is stopped and Stop() will be called for already initialized modules in reverse order.
- Exec Run() for each module in own goroutine so order isn't guaranteed.
- Wait for any Run() function to return nil or an error and move to Stop loop.
- Exec Stop() for modules in reverse order.
- Wait for all Run() goroutines to return.
- Return all errors or nil
Panics inside modules are recovered so other modules can shut down gracefully. Recovered panics are returned as errors wrapping ErrModulePanic.
func RunAndExit
func RunAndExit(modules ...Module)RunAndExit is convenience wrapper for Run that calls os.Exit with code 1 in case of an error. The common use case is to srvc.RunAndExit from main function and let the srvc handle the rest.
package main
import "github.com/go-srvc/srvc"
func main() {
srvc.RunAndExit(
// Add your modules here
)
}
Types
type ErrGroup
type ErrGroup struct {
// contains filtered or unexported fields
}ErrGroup is a goroutine group that waits for all goroutines to finish and collects errors.
func (*ErrGroup) Go
func (eg *ErrGroup) Go(f func() error)Go runs the given function in a goroutine.
func (*ErrGroup) Wait
func (eg *ErrGroup) Wait() errorWait waits for all goroutines to finish and returns all errors that occurred.
type ErrStr
type ErrStr stringErrStr adds Error method to string type.
func (ErrStr) Error
func (e ErrStr) Error() stringtype Module
type Module interface {
// ID should return identifier for logging purposes.
ID() string
// Init allows synchronous initialization of module.
Init() error
// Run should start the module and block until stop is called or error occurs.
Run() error
// Stop is called for synchronous cleanup and must cause Run to return.
// If Init ran, Stop is guaranteed to run as part of cleanup.
Stop() error
}func CtxMod
func CtxMod(id string, runFn func(ctx context.Context) error) ModuleCtxMod wraps runFn into a Module that calls runFn during the run phase with a context that is canceled when Stop is called. Init is a no-op and runFn must return once its context is done. Useful for run loops that already take a context.
func IdleMod
func IdleMod(id string, initFn, stopFn func() error) ModuleIdleMod wraps initFn and stopFn into a Module that has no work of its own during the run phase. Run blocks until the service shuts down and nil functions are no-ops. Useful for resources that are set up on init and torn down on stop.
func InitMod
func InitMod(id string, initFn func() error) ModuleInitMod wraps initFn into a Module that calls initFn during the init phase. Run blocks until the service shuts down and Stop is a no-op. Useful for one time setup that has no long running work of its own.
func Optional
func Optional(enabled bool, m Module) ModuleOptional returns m when enabled is true. Otherwise m is skipped before the init phase, so none of its Init, Run or Stop functions are called. m is still constructed by the caller, so keep heavy work in Init. m may be nil when enabled is false.
func RunMod
func RunMod(id string, runFn func() error) ModuleRunMod wraps runFn into a Module that calls runFn during the run phase. Init and Stop are no-ops, so runFn must return on its own. When it returns the whole service moves to the stop sequence.
func StopMod
func StopMod(id string, stopFn func() error) ModuleStopMod wraps stopFn into a Module that calls stopFn during the stop phase. Init is a no-op and Run blocks until the service shuts down. Useful for cleanup that should always run when the service exits.
Examples
ExampleOptional
package main
import (
"fmt"
"github.com/go-srvc/srvc"
)
type printMod struct{}
func (m *printMod) ID() string { return "printMod" }
func (m *printMod) Init() error { return nil }
func (m *printMod) Run() error { fmt.Println("hello"); return nil }
func (m *printMod) Stop() error { return nil }
func main() {
metricsEnabled := false
_ = srvc.Run(
&printMod{},
srvc.Optional(metricsEnabled, srvc.IdleMod("metrics", nil, nil)),
)
}ExampleRun
package main
import (
"fmt"
"github.com/go-srvc/srvc"
)
type printMod struct{}
func (m *printMod) ID() string { return "printMod" }
func (m *printMod) Init() error { return nil }
func (m *printMod) Run() error { fmt.Println("hello"); return nil }
func (m *printMod) Stop() error { return nil }
func main() {
_ = srvc.Run(&printMod{})
}