GOscade is a library for managing the lifecycle and dependencies of concurrent components in Go.
go get github.com/ognick/goscade/v2To be managed by GOscade, a type must implement the Component interface:
type Component interface {
// Run starts the component.
// It must call readinessProbe when the component is ready to serve.
// It should block until ctx is canceled or a fatal error occurs.
Run(ctx context.Context, readinessProbe func(cause error)) error
}package main
import (
"context"
"log"
"time"
"github.com/ognick/goscade/v2"
)
type Worker struct {
Name string
}
func (w *Worker) Run(ctx context.Context, readinessProbe func(error)) error {
log.Printf("Worker %s starting...", w.Name)
// Simulate startup initialization
time.Sleep(100 * time.Millisecond)
readinessProbe(nil) // Signal readiness
log.Printf("Worker %s ready", w.Name)
<-ctx.Done() // Wait for shutdown signal
log.Printf("Worker %s stopped", w.Name)
return nil
}
// Simple logger adapter
type myLogger struct{}
func (l myLogger) Infof(f string, a ...interface{}) { log.Printf(f, a...) }
func (l myLogger) Errorf(f string, a ...interface{}) { log.Printf("ERROR: "+f, a...) }
func main() {
// 1. Create Lifecycle manager
lc := goscade.NewLifecycle(myLogger{}, goscade.WithShutdownHook())
// 2. Register components
goscade.Register(lc, &Worker{Name: "Core"})
// 3. Run
// Blocks until SIGINT/SIGTERM or error
err := goscade.Run(context.Background(), lc, func() {
log.Println("System is fully up and running!")
})
if err != nil {
log.Fatal(err)
}
}GOscade automatically detects dependencies between registered components if they are struct pointers or interfaces.
type Database struct { /* ... */ }
// Service depends on Database via interface
type Service struct {
DB interface{} // or specific interface like Storer
}
func main() {
lc := goscade.NewLifecycle(log)
db := &Database{}
service := &Service{DB: db}
// Order of registration doesn't matter
goscade.Register(lc, service)
goscade.Register(lc, db)
// GOscade detects that Service depends on Database.
// It ensures Database is ready before Service starts.
}To exclude a struct field from reflection-based dependency detection, tag it with goscade:"ignore". The field (and anything reachable through it) will not be traversed.
This is useful for externally-managed dependencies that GOscade does not own. Such third-party clients may spawn their own goroutines, so letting GOscade treat them as part of the managed lifecycle can lead to race conditions. Tagging them as ignored keeps them out of the dependency graph:
type Service struct {
DB *Database
PaymentAPI *stripe.Client `goscade:"ignore"` // externally-managed, may run its own goroutines
}If you need to declare dependencies that cannot be detected via reflection (e.g. hidden inside closures or non-struct fields), use explicit declaration:
// Register service and declare it depends on db explicitly
goscade.Register(lc, service, db)When a component has no reflectable field path to the components it depends on
(the link runs through a wiring struct, closures, etc.), use Link to point it
at an arbitrary struct. The struct is reflection-walked during graph building
and every registered component reachable inside it becomes a parent — as if the
component had a field referencing that struct. The struct itself is never a node
and is never run.
// wiring is a plain struct, not a Component.
type wiring struct {
DB *Database
Cache *Cache
}
w := &wiring{DB: db, Cache: cache}
// service depends on every registered component reachable inside w (db, cache),
// even though service has no field referencing them.
goscade.Link(lc, service, w)Use NewAdapter to wrap existing types (like http.Server) without defining a new struct.
srv := &http.Server{Addr: ":8080"}
adapter := goscade.NewAdapter(srv, func(ctx context.Context, s *http.Server, probe func(error)) error {
// Start server in background
errChan := make(chan error, 1)
go func() {
if err := s.ListenAndServe(); err != http.ErrServerClosed {
errChan <- err
}
}()
// Signal ready (optionally verify port is listening first)
probe(nil)
// Wait for context cancellation or server error
select {
case <-ctx.Done():
// Graceful shutdown
return s.Shutdown(context.Background())
case err := <-errChan:
return err
}
})
lc.Register(adapter)lc := goscade.NewLifecycle(logger,
// Handle system signals (SIGINT, SIGTERM)
goscade.WithShutdownHook(),
// Set timeout for components to become ready
goscade.WithStartTimeout(30 * time.Second),
// Set timeout for components to stop
goscade.WithShutdownTimeout(30 * time.Second),
// Allow circular dependencies (use with caution)
goscade.WithCircularDependency(),
// Export dependency graph to DOT file on startup
goscade.WithGraphOutput("graph.dot"),
)Lifecycle.Run returns the cause that initiated shutdown together with any
independent component and cleanup errors. Inspect the result with errors.Is;
multiple failures may be joined with errors.Join.
err := lc.Run(ctx, nil)
if errors.Is(err, context.Canceled) {
// Shutdown was requested by the caller.
}GOscade can export the component dependency graph in DOT format (Graphviz).
// Build graph structure
graph := lc.BuildGraph()
// Convert to DOT format
dotString := graph.ToDOT()
fmt.Println(dotString)// Graph will be saved to file when lifecycle starts
lc := goscade.NewLifecycle(logger,
goscade.WithGraphOutput("graph.dot"),
)# Generate PNG image
dot -Tpng graph.dot -o graph.png
# Generate SVG
dot -Tsvg graph.dot -o graph.svg
# Generate PDF
dot -Tpdf graph.dot -o graph.pdfExample DOT output:
digraph G {
rankdir=TB;
"Database" [label="Database", shape=box];
"Cache" [label="Cache", shape=box];
"APIServer" [label="APIServer", shape=box];
"Database" -> "Cache";
"Cache" -> "APIServer";
}This project is licensed under the MIT License - see the LICENSE file for details.


