Skip to content

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.

shutdown needs Go 1.26 or newer.

Terminal window
go get github.com/vardius/shutdown/v2

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:

examples/server/main.go
// 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/server
package 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")
}
  1. From a clone of the repository, start the server:

    Terminal window
    go run ./examples/server
  2. In a second terminal, make a request. It takes three seconds:

    Terminal window
    curl localhost:8080
  3. 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 prints stopped and exits with status 0.

  4. 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.

  5. 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.

  • GracefulStop blocked serve until the first Ctrl+C, a SIGINT.
  • It called the stops in order: srv.Shutdown closed the listener and waited for the request in flight, then srv.Close had nothing left to cut off.
  • Both got the same context, done 10 seconds after the signal. Had the request taken longer, srv.Shutdown would have returned context.DeadlineExceeded, and srv.Close would have cut the request off.
  • GracefulStop returned the stops’ errors, nil here, so main printed stopped and returned normally.