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.
go get -u github.com/maxbolgarin/contem
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! π
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)
}
}
// β
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
}
Replace context.Context
with contem.Context
in your function signatures. Everything else works the same.
Pure Go standard library. No external dependencies to worry about.
- No more
log.Fatal()
ignoring your cleanup code - Automatic timeout handling for shutdown functions
- Proper error aggregation and reporting
- 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)
Special handling for files that need syncing before closing:
file, _ := os.Create("important.log")
ctx.AddFile(file) // Automatically syncs AND closes on shutdown
Fine-tune behavior with options:
ctx := contem.New(
contem.WithLogger(logger),
contem.WithShutdownTimeout(30*time.Second),
contem.WithExit(&err), // Exit with proper code
)
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
}
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 |
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 |
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 |
// 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.
}
// 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
}
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.
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!