Graceful shutdown for Go services: register cleanup hooks and shut down safely on OS signals or custom events - without losing state in your stateful apps/services.
The gracefully package is designed to simplify graceful shutdowns in Go applications. The core concept revolves around a thread-safe registry that manages objects implementing a simple interface for shutdown logic. This allows you to register components like servers, databases, or any resources that require proper cleanup before the program exits.
Key ideas:
- Registry-based management: A central registry (global by default) holds references to shutdownable objects. It's safe for concurrent use and prevents duplicate registrations.
- Trigger-based shutdown: Shutdown can be triggered by OS signals (e.g., SIGINT, SIGTERM), custom channels, or manually. It respects contexts for timeouts and collects errors from failed shutdowns.
- Error handling: Uses a multi-error type to aggregate issues, with global access for post-shutdown checks.
- Flexibility: Supports custom registries, priorities (future), and extensions for universal use in web apps, CLI tools, or services.
This approach ensures your application handles interruptions politely, avoiding data corruption or abrupt terminations, especially in production environments like Docker or Kubernetes.
To install the package, run:
go get github.com/lif0/go-gracefully@latestImport it in your code:
import "github.com/lif0/go-gracefully"Any object that needs graceful shutdown must implement this interface:
type GracefulShutdownObject interface {
GracefulShutdown(ctx context.Context) error
}Example implementation for a custom batcher:
type MyBatcher struct {
// Your batcher fields, e.g.
}
func (s *MyBatcher) GracefulShutdown(ctx context.Context) error {
// Flush data to disk,db, etc.
select {
case <-ctx.Done():
return ctx.Err()
default:
s.StopRecv()
return s.FlushData()
}
}Use the global registry:
import "github.com/lif0/go-gracefully"
type MyBatcher struct {
// some fields
}
func (mb *MyBatcher) GracefulShutdown() {...}
func (mb *MyBatcher) Closer() {...}
// order is important
// at first will be called GracefulShutdown()
// at second will be called Closer()
myBatcher := &MyBatcher{}
gracefully.MustRegister(myBatcher) // will be register myBatcher.GracefulShutdown()
gracefully.RegisterFunc(myBatcher.Closer)or
import "github.com/lif0/go-gracefully"
myBatcher := &MyBatcher{}
gracefully.MustRegister(myBatcher)Launch a goroutine to listen for triggers.
gracefully.SetShutdownTrigger(context.Background())or
gracefully.SetShutdownTrigger(
context.Background(),
gracefully.WithSysSignal(),
gracefully.WithTimeout(time.Hour)
)Registers signal.Notify for SIGINT and SIGTERM signals on the signal channel. This option is enabled by default.
A repeated signal will invoke os.Exit(130), which immediately terminates the application without waiting for any ongoing processes.
Provides your own custom signal channel for handling OS signals.
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGUSR1 /* or any other signals */)
gracefully.SetShutdownTrigger(ctx, gracefully.WithCustomSystemSignal(ch))Allows you to pass one or more custom channels. When any of these channels is closed or receives a value, the graceful shutdown process will be triggered.
A repeated signal will invoke os.Exit(130), which immediately terminates the application without waiting for any ongoing processes.
chShutdown := make(chan struct{})
gracefully.SetShutdownTrigger(ctx, gracefully.WithUserChanSignal(chShutdown))
chShutdown <- struct{}{} // to trigger the shutdown
chShutdown <- struct{}{} // to trigger the os.ExitSets the maximum duration for the graceful shutdown. By default, no timeout is applied - the service waits for all tasks to finish. A non-positive timeout disables the shutdown deadline.
The trigger will call Shutdown automatically. Manually:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
errs := gracefully.DefaultRegisterer.Shutdown(shutdownCtx)
if errs != nil {
// Check gracefully.GlobalErrors for details
}Wait for completion:
gracefully.WaitShutdown()unregistered := gracefully.Unregister(server) // Returns true if removedUse gracefully.GetStatus() for check status.
Statuses:
| Name | Description |
|---|---|
StatusRunning |
The service is running and accepting new requests. |
StatusDraining |
The service is shutting down gracefully; it no longer accepts new requests but continues processing existing ones. |
StatusStopped |
Graceful shutdown has fully finished; all resources are released and the process can safely exit. |
import (
"fmt"
"github.com/lif0/go-gracefully"
)
func handleServiceStatus() {
switch gracefully.GetStatus() {
case gracefully.StatusRunning:
// ignore
case gracefully.StatusDraining:
// For example: return error for any request
case gracefully.StatusStopped:
// For Example: log and finish app
}
}Or use gracefully.WatchStatus(ctx, func(newStatus Status)) for subscribe status.
import (
"fmt"
"github.com/lif0/go-gracefully"
)
func handleServiceStatus(newStatus gracefully.Status) {
switch newStatus {
case gracefully.StatusRunning:
// ignore
case gracefully.StatusDraining:
// For example: return error for any request
case gracefully.StatusStopped:
// For Example: log and finish app
}
}
func main() {
ctx := context.Background()
gracefully.WatchStatus(ctx, handleServiceStatus)
}Use generics for quick creation:
batcher := gracefully.NewInstance(func() *MyBatcher {
return &MyBatcher{}
})- Check
gracefully.GlobalErrorsafter shutdown.
For full details, see the GoDoc: pkg.go.dev/github.com/lif0/go-gracefully.
package main
import (
"context"
"fmt"
"time"
"github.com/lif0/go-gracefully"
)
var stopChan chan struct{}
var isProcessing atomic.Bool
func main() {
isProcessing.Store(true)
// configure
gracefully.SetShutdownTrigger(
context.Background(),
gracefully.WithSysSignal(),
gracefully.WithUserChanSignal(stopChan),
)
gracefully.WatchStatus(ctx, func(newStatus Status) {
switch newStatus {
case gracefully.StatusRunning:
// ignore
case gracefully.StatusDraining:
isProcessing.Store(false)
case gracefully.StatusStopped:
isProcessing.Store(false)
}
})
counter := NewCounter()
gracefully.MustRegister(counter)
go func() {
for isProcessing.Load() {
time.Sleep(500 * time.Millisecond)
counter.Inc()
fmt.Printf("counter: %v\n", counter.val)
}
}()
go func() {
time.Sleep(time.Hour)
close(stopChan)
}()
gracefully.WaitShutdown() // Wait finish stop all registered objects
fmt.Println("App finish")
}Check out the examples directory for complete, runnable demos, including HTTP server shutdown and custom triggers.
- Add an interface for validating the structure being registered
- Add object registration/deregistration
- Add options for the GracefulShutdown trigger (WithUserChanSignal, WithCustomSystemSignal, WithSysSignal, WithTimeout)
- Add the ability to register functions
- Reach >90% test coverage
- Write benchmarks
- (Internal) Improve the deduplication algorithm (add an OrderedMap)
- Add func: gracefully.Status.
- Add func: gracefully.WatchStatus().
MIT License. See LICENSE for details.