Getting started
shutdown has one function, GracefulStop. It blocks until the process gets SIGINT, SIGTERM or SIGHUP, then runs your
stop functions with a deadline and returns their errors. A second signal ends the process at once.
Install
Section titled “Install”shutdown needs Go 1.26 or newer.
go get github.com/vardius/shutdown/v2Your first graceful shutdown
Section titled “Your first graceful shutdown”This program serves HTTP. Each request takes three seconds to answer, so that there’s something to wait for. On a signal it stops taking connections, gives the requests in flight 10 seconds to finish, and cuts off any still running. If the server fails, it shuts down too:
// Command server answers HTTP requests until it gets SIGINT, SIGTERM or SIGHUP. Then it stops taking connections,// gives the requests in flight 10 seconds to finish, and cuts off any still running.//// go run ./examples/serverpackage main
import ( "context" "errors" "io" "log" "net" "net/http" "time"
"github.com/vardius/shutdown/v2")
// serve answers requests on ln until the process gets a shutdown signal, ctx is done, or the server fails.func serve(ctx context.Context, ln net.Listener, h http.Handler, grace time.Duration) error { srv := &http.Server{Handler: h, ReadHeaderTimeout: 10 * time.Second}
ctx, cancel := context.WithCancel(ctx) defer cancel() served := make(chan error, 1) go func() { served <- srv.Serve(ln) cancel() // if the server failed, shut down too }()
err := shutdown.GracefulStop(ctx, grace, srv.Shutdown, // close the listener, and wait for the requests in flight func(context.Context) error { return srv.Close() }, // cut off any still running ) if serveErr := <-served; !errors.Is(serveErr, http.ErrServerClosed) { return errors.Join(serveErr, err) } return err}
// work takes three seconds to answer, so that there is something to wait for.func work(w http.ResponseWriter, r *http.Request) { select { case <-time.After(3 * time.Second): io.WriteString(w, "done\n") case <-r.Context().Done(): // cut off, or the client went away }}
func main() { log.SetFlags(0) ln, err := net.Listen("tcp", "localhost:8080") if err != nil { log.Fatal(err) } log.Printf("listening on http://%s", ln.Addr())
if err := serve(context.Background(), ln, http.HandlerFunc(work), 10*time.Second); err != nil { log.Fatal(err) } log.Print("stopped")}Run it
Section titled “Run it”-
From a clone of the repository, start the server:
Terminal window go run ./examples/server -
In a second terminal, make a request. It takes three seconds:
Terminal window curl localhost:8080 -
While it waits, press Ctrl+C in the server’s terminal. The server stops taking connections, but the request finishes: curl prints
done, then the server printsstoppedand exits with status 0. -
Start the server again, make a request, and press Ctrl+C twice. The second Ctrl+C ends the process at once, with exit status 1, and curl reports an empty reply.
-
Start it once more and press Ctrl+\. Go prints every goroutine’s stack and exits with status 2: SIGQUIT is left to Go, so you can see what a hung shutdown waits for.
What just happened
Section titled “What just happened”GracefulStopblockedserveuntil the first Ctrl+C, a SIGINT.- It called the stops in order:
srv.Shutdownclosed the listener and waited for the request in flight, thensrv.Closehad nothing left to cut off. - Both got the same context, done 10 seconds after the signal. Had the request taken longer,
srv.Shutdownwould have returnedcontext.DeadlineExceeded, andsrv.Closewould have cut the request off. GracefulStopreturned the stops’ errors, nil here, somainprintedstoppedand returned normally.