srvc library provides a simple but powerful interface with zero external dependencies for running service modules.
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.
List of ready made modules can be found under github.com/go-srvc/mods
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")
})
}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" }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).
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.
RunAndExit calls os.Exit(1) if Run returns any error, and returns normally on success.
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.
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.
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.