ctrl
provides a set of control functions for assertions, error handling, HTTP server management, and graceful shutdown handling in Go applications. Built for Go 1.21+, it offers a clean API with flexible configuration options.
- Runtime assertions with optional formatted messages
- Error-returning validation alternatives to assertions
- HTTP server lifecycle management
- Graceful shutdown with signal handling
- Context-based cancellation
- Configurable timeouts and callbacks
- Comprehensive error handling
- No external dependencies except for testing
Here's a practical example showing how to implement graceful shutdown in an HTTP server:
func main() {
// Set up graceful shutdown
ctx, cancel := ctrl.GracefulShutdown(
ctrl.WithTimeout(10*time.Second),
ctrl.WithOnShutdown(func(sig os.Signal) {
log.Printf("shutdown initiated by signal: %v", sig)
}),
)
defer cancel()
// Create a simple HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server with context-aware shutdown
errCh := ctrl.RunHTTPServerWithContext(
ctx,
server,
func() error {
return server.ListenAndServe()
},
ctrl.WithHTTPShutdownTimeout(5*time.Second),
)
// Wait for server to exit and check for errors
if err := <-errCh; err != nil {
log.Fatalf("Server error: %v", err)
}
}
The package provides assertion functions that panic when conditions are not met, useful for runtime invariant checking:
// Simple assertion
ctrl.Assert(user.IsAuthenticated())
// Formatted assertion
ctrl.Assertf(count > 0, "expected positive count, got %d", count)
// Function-based assertions
ctrl.AssertFunc(func() bool {
return database.IsConnected()
})
ctrl.AssertFuncf(func() bool {
return cache.Size() < maxSize
}, "cache size exceeded: %d/%d", cache.Size(), maxSize)
The package provides variants of assertion functions that return errors instead of panicking. These are useful for validations where you want to return an error to the caller rather than crash the program:
// Basic condition checking
if err := ctrl.ErrorOr(user.IsAuthenticated()); err != nil {
return err
}
// With formatted error message
if err := ctrl.ErrorOrf(count > 0, "expected positive count, got %d", count); err != nil {
return err
}
// Function-based variants
if err := ctrl.ErrorOrFunc(func() bool {
return database.IsConnected()
}); err != nil {
return err
}
// With custom error
customErr := ErrDatabaseNotConnected
if err := ctrl.ErrorOrWithErr(database.IsConnected(), customErr); err != nil {
return err // Will return customErr if condition fails
}
These functions are particularly useful in validators, middleware, and other scenarios where returning an error is more appropriate than panicking.
The package helps manage HTTP server lifecycle, particularly graceful shutdown:
// Shutdown an HTTP server with a timeout
err := ctrl.ShutdownHTTPServer(ctx, server,
ctrl.WithHTTPShutdownTimeout(5*time.Second))
// Run a server with context-aware shutdown
errCh := ctrl.RunHTTPServerWithContext(ctx, server,
func() error {
return server.ListenAndServe()
},
ctrl.WithHTTPShutdownTimeout(5*time.Second),
ctrl.WithHTTPLogger(logger),
)
The package provides a robust way to handle process termination signals:
// Basic setup
ctx, cancel := ctrl.GracefulShutdown()
defer cancel()
// With custom configuration
ctx, cancel := ctrl.GracefulShutdown(
ctrl.WithTimeout(30*time.Second),
ctrl.WithSignals(syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP),
ctrl.WithExitCode(2),
ctrl.WithOnShutdown(func(sig os.Signal) {
log.Printf("shutting down due to %s signal", sig)
database.Close()
}),
ctrl.WithOnForceExit(func() {
log.Printf("force exiting after timeout")
}),
ctrl.WithLogger(logger),
)
go get -u github.com/go-pkgz/ctrl
func processItems(items []Item) {
// Ensure we have items to process
ctrl.Assertf(len(items) > 0, "no items to process")
for _, item := range items {
// Ensure each item is valid
ctrl.Assert(item.IsValid())
// Process the item
process(item)
}
}
func validateRequest(req Request) error {
// Return error if ID is empty
if err := ctrl.ErrorOrf(req.ID != "", "request ID cannot be empty"); err != nil {
return err
}
// Return custom error if size exceeds limit
maxSizeErr := errors.New("max size exceeded")
if err := ctrl.ErrorOrWithErr(req.Size <= maxSize, maxSizeErr); err != nil {
return err
}
return nil
}
func startServer() error {
// Set up graceful shutdown
ctx, cancel := ctrl.GracefulShutdown(
ctrl.WithTimeout(10*time.Second),
ctrl.WithLogger(logger),
)
defer cancel()
// Create server
server := &http.Server{
Addr: ":8080",
Handler: router,
}
// Run server
errCh := ctrl.RunHTTPServerWithContext(ctx, server, server.ListenAndServe)
// Wait for shutdown
return <-errCh
}
func main() {
// Create shutdown context
ctx, cancel := ctrl.GracefulShutdown(
ctrl.WithOnShutdown(func(sig os.Signal) {
log.Printf("shutdown sequence initiated by %v", sig)
}),
)
defer cancel()
// Create multiple services with the same shutdown context
httpServer := setupHTTPServer(ctx)
grpcServer := setupGRPCServer(ctx)
cacheService := setupCache(ctx)
// Wait for context cancellation (shutdown signal)
<-ctx.Done()
// Context was canceled, services will be shutting down
log.Println("waiting for all services to complete shutdown")
// Additional cleanup if needed
database.Close()
}
The package uses functional options pattern for configuration:
// WithHTTPShutdownTimeout sets the maximum time to wait for server shutdown
WithHTTPShutdownTimeout(timeout time.Duration)
// WithHTTPLogger sets a custom logger for HTTP server operations
WithHTTPLogger(logger *slog.Logger)
// WithSignals sets which signals trigger the shutdown
WithSignals(signals ...os.Signal)
// WithTimeout sets the maximum time to wait for graceful shutdown
WithTimeout(timeout time.Duration)
// WithoutForceExit disables the forced exit after timeout
WithoutForceExit()
// WithExitCode sets the exit code used for forced exits
WithExitCode(code int)
// WithOnShutdown sets a callback function called when shutdown begins
WithOnShutdown(fn func(os.Signal))
// WithOnForceExit sets a callback function called right before forced exit
WithOnForceExit(fn func())
// WithLogger sets a custom logger for shutdown messages
WithLogger(logger *slog.Logger)
-
Assertions vs ErrorOr: Choose based on failure severity
// Use Assert for internal invariants that should never fail ctrl.Assert(len(buffer) >= headerSize) // Use ErrorOr for validating external input if err := ctrl.ErrorOr(len(userInput) < maxLength); err != nil { return err }
-
Graceful Shutdown: Allow sufficient time for connections to close
// Shorter timeout for development ctrl.WithTimeout(5*time.Second) // Longer timeout for production with many connections ctrl.WithTimeout(30*time.Second)
-
HTTP Server Context: Create a separate context for each server
// Each server gets its own timeout and configuration apiErrCh := ctrl.RunHTTPServerWithContext(ctx, apiServer, apiServer.ListenAndServe, ctrl.WithHTTPShutdownTimeout(10*time.Second)) adminErrCh := ctrl.RunHTTPServerWithContext(ctx, adminServer, adminServer.ListenAndServe, ctrl.WithHTTPShutdownTimeout(5*time.Second))
-
Shutdown Callbacks: Use for resource cleanup
ctrl.WithOnShutdown(func(sig os.Signal) { // Close database connections db.Close() // Flush logs logger.Sync() // Release other resources cache.Clear() })
The package provides clear error handling patterns:
// HTTP server shutdown
if err := ctrl.ShutdownHTTPServer(ctx, server); err != nil {
log.Printf("error during server shutdown: %v", err)
}
// Running HTTP server with context
errCh := ctrl.RunHTTPServerWithContext(ctx, server, server.ListenAndServe)
if err := <-errCh; err != nil {
log.Fatalf("server error: %v", err)
}
Contributions to ctrl
are welcome! Please submit a pull request or open an issue for any bugs or feature requests.
ctrl
is available under the MIT license. See the LICENSE file for more info.