
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
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() {
srv := &http.Server{Addr: ":8080"}
// Signal handling boilerplate
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err) // ❌ log.Fatal ignores cleanup!
}
}()
<-quit
log.Println("Shutting down server...")
// Manual cleanup with timeout handling
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
// What about database connections? File handles? Other resources?
// More cleanup code needed...
}
// ✅ 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
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"
"time"
"github.com/maxbolgarin/contem"
_ "github.com/lib/pq"
)
func main() {
contem.Start(run, slog.Default())
}
func run(ctx contem.Context) error {
logFile, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return err
}
// 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
slog.SetDefault(slog.New(slog.NewTextHandler(logFile, nil)))
db, err := sql.Open("postgres", "postgres://localhost/mydb?sslmode=disable")
if err != nil {
return err // Will log error and close logFile because it has been added to the context
}
// 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)
return nil
}
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 |
Q: My shutdown functions are timing out
// Increase timeout
ctx := contem.New(contem.WithShutdownTimeout(60*time.Second))
Q: I need custom signals
// Listen for custom signals
ctx := contem.New(contem.WithSignals(syscall.SIGUSR1, syscall.SIGUSR2))
Q: Shutdown is too slow
// Files close after other resources by default
// To close everything together:
ctx := contem.New(contem.RegularCloseFilesOrder())
Q: I want to see what's happening during shutdown
// Add logging
ctx := contem.New(contem.WithLogger(slog.Default()))
// 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!