π contem - Awesome Go Library for Utilities

Drop-in context.Context replacement for graceful shutdown Go applications
Detailed Description of contem
contem - Graceful Shutdown Made Simple
contem is a zero-dependency, drop-in replacement for context.Context that makes graceful shutdown trivial. Stop worrying about signal handling, resource cleanup, and shutdown coordinationβjust add your cleanup functions and let contem handle the rest.
π Quick Start
go get -u github.com/maxbolgarin/contem
The Simplest Example
package main
import (
"log/slog"
"net/http"
"github.com/maxbolgarin/contem"
)
func main() {
contem.Start(run, slog.Default())
}
func run(ctx contem.Context) error {
srv := &http.Server{Addr: ":8080"}
ctx.Add(srv.Shutdown) // That's it! Server will shutdown gracefully on Ctrl+C
// Make run() function non-blocking
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("Server failed", "error", err)
ctx.Cancel() // Trigger graceful shutdown of the whole application on error in an another goroutine
}
}()
return nil
}
Press Ctrl+C and watch your server shutdown gracefully! π
π Why contem?
The Problem
Traditional Go applications require boilerplate for graceful shutdown:
// β Traditional approach - lots of boilerplate
func main() {
file, err := os.OpenFile("important.log", ...) // Open file
if err != nil {
log.Fatalf("Cannot open file: %v", err)
}
defer func() {
if err := file.Close(); err != nil {
log.Fatalf("Cannot close file: %v", err)
}
}()
db, err := sql.Open("postgres", "...")
if err != nil {
// log.Fatal() ignores defer statements, GC closes file after returning from main()), but it is not a good practice!
log.Fatalf("Cannot open database: %v", err)
}
defer func() {
// Close should be idempotent
if err := db.Close(); err != nil {
log.Fatalf("Cannot close database: %v", err)
}
}()
app, err := some.Init(db, file)
if err != nil {
// log.Fatal() ignores defer statements, so we need to close database manually
if err := db.Close(); err != nil {
log.Fatalf("Cannot close database: %v", err)
}
log.Fatalf("Cannot initialize application: %v", err)
}
// Signal handling boilerplate
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
go some.Run(ctx) // Run some other goroutine
<-ctx.Done()
log.Println("Shutting down application...")
// Manual cleanup with timeout handling
ctx2, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx2); err != nil {
// log.Fatal() ignores defer statements, so we need to close database manually
if err := db.Close(); err != nil {
log.Fatalf("Cannot close database: %v", err)
}
log.Fatalf("Server forced to shutdown: %v", err)
}
}
The Solution
// β
contem approach - clean and simple
func main() {
contem.Start(run, slog.Default())
}
func run(ctx contem.Context) error {
file, err := os.OpenFile("important.log", ...) // Open file
if err != nil {
return err
}
ctx.AddFile(file) // File will be synced and closed on shutdown
db, err := sql.Open("postgres", "...")
if err != nil {
return err
}
ctx.AddClose(db.Close) // Database will close gracefully too
app, err := some.Init(db, file)
if err != nil {
return err
}
ctx.Add(app.Shutdown) // Shutdown will be called on Ctrl+C
go some.Run(ctx) // Run some other goroutine
return nil
}
π Key Benefits
π― Drop-in Replacement
Replace context.Context with contem.Context in your function signatures. Everything else works the same.
β‘ Zero Dependencies
Pure Go standard library. No external dependencies to worry about.
π‘οΈ Bulletproof Error Handling
- No more
log.Fatal()ignoring your cleanup code - Automatic timeout handling for shutdown functions
- Proper error aggregation and reporting
π Resource Management
- Automatic cleanup of databases, files, connections
- Proper shutdown order (general resources first, files last)
- Parallel shutdown for better performance (you can disable it with
contem.WithNoParallel()option)
π File Safety
Special handling for files that need syncing before closing:
file, _ := os.Create("important.log")
ctx.AddFile(file) // Automatically syncs AND closes on shutdown
ποΈ Flexible Configuration
Fine-tune behavior with options:
ctx := contem.New(
contem.WithLogger(logger),
contem.WithShutdownTimeout(30*time.Second),
contem.WithExit(&err), // Exit with proper code
)
π Complete Example: Web Server with Database without Start() function
package main
import (
"database/sql"
"log/slog"
"net/http"
"os"
"github.com/maxbolgarin/contem"
)
func main() {
var err error
// Create a new context with Exit option to exit with code 1 in case of error (no need for log.Fatal())
ctx := contem.New(contem.Exit(&err), contem.WithLogger(slog.Default()))
defer ctx.Shutdown()
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
slog.Error("cannot open log file", "error", err)
return
}
// Automatically syncs and closes file in the last order to log every error during shutdown
ctx.AddFile(logFile)
// Set default logger to use the file
fileLogger := slog.New(slog.NewTextHandler(logFile, nil))
fileLogger.Info("Starting application")
db, err := sql.Open("sqlite3", "app.db")
if err != nil {
slog.Error("cannot open database", "error", err)
return
}
// Will close database connection gracefully on shutdown
ctx.AddClose(db.Close)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
go func() {
slog.Info("Server starting", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("Server failed", "error", err)
ctx.Cancel() // Trigger graceful shutdown and release all added resources
}
}()
// Will shutdown HTTP server gracefully on Ctrl+C
ctx.Add(srv.Shutdown)
ctx.Wait() // Wait for Ctrl+C
// it will start defered shutdown function, which will make graceful shutdown of the whole application
}
π§ API Reference
Core Functions
| Function | Description |
|---|---|
contem.Start(run, logger, opts...) | Easiest way to start an app with graceful shutdown |
contem.New(opts...) | Create a new context with options |
contem.NewEmpty() | Create empty context for testing |
Context Methods
| Method | Description |
|---|---|
Add(ShutdownFunc) | Add function that accepts context and returns error |
AddClose(CloseFunc) | Add function that returns error (like io.Closer) |
AddFunc(func()) | Add simple function with no return |
AddFile(File) | Add file that needs sync + close |
Wait() | Block until interrupt signal received |
Cancel() | Manually trigger shutdown (not recommended) |
Shutdown() | Execute all shutdown functions |
Configuration Options
| Option | Description |
|---|---|
WithLogger(logger) | Add structured logging |
WithShutdownTimeout(duration) | Set shutdown timeout (default: 15s) |
WithExit(&err, code...) | Exit process after shutdown |
WithAutoShutdown() | Auto-shutdown when context cancelled |
WithNoParallel() | Disable parallel shutdown |
WithSignals(signals...) | Custom signals (default: SIGINT, SIGTERM) |
WithDontCloseFiles() | Skip file closing |
WithRegularCloseFilesOrder() | Close files with other resources |
π Migration Guide
From Standard Context
// Before
func MyFunction(ctx context.Context) error {
// ...
}
// After - just change the type!
func MyFunction(ctx contem.Context) error {
// All context.Context methods still work
// Plus you get Add(), AddClose(), etc.
}
From Manual Signal Handling
// Before
func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// ... setup code ...
<-quit
// Manual cleanup
}
// After
func main() {
contem.Start(run, logger)
}
func run(ctx contem.Context) error {
// Setup code + add cleanup functions
return nil
}
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
π License
This project is licensed under the MIT License. See the LICENSE file for details.
β Star this repo if it helped you build better Go applications!