In this example I am going to show you how you can gracefully handle HTTP server shutdown in Go applications. The point here is to wait for certain amount of duration before shutting down the server rather than killing all "live" requests/connections by just shutting it down straight away. Read the code comments to understand what exactly lines and functions do. There are different ways of handling graceful server shutdown so this is just one of those examples which is mainly based on the Golang Shutdown documentation. I am just going to keep the necessary code to keep the example as relevant as possible.


Structure


.
├── cmd
│   └── game
│   └── main.go
├── internal
│   ├── app
│   │   └── game.go
│   ├── ...
│   └── ...
├── ...
└── ...

Logic


A "known" shutdown signal occurs. e.g. Ctrl-C, kill PID, docker stop or docker down. If there is no "live" request/connection at that moment, block new requests coming in and shutdown the server without waiting for 10 seconds. However, if there was one or many "live" requests/connection, block new requests coming in and allow them 10 seconds to finish what they are doing before shutting it down. If any "live" request/connection exceeds 10 seconds then it gets killed.


Files


The whole code could be kept in a single file or organised differently so it's up to you to improve/refactor it.


cmd/game/main.go


package main

import (
"context"
"log"

"internal/app"
)

func main() {
// Obtain database instance.
db := ...

// Obtain HTTP server instance.
srv := ...

log.Println("app: starting")

ctx, cancel := context.WithCancel(context.Background())

// Dedicated for shutting down all active connections. Once actual shutdown
// occurred, the main goroutine is shutdown.
shutChan := make(chan struct{})

// Start the application.
if err := app.New(srv, db).Start(ctx, cancel, shutChan); err != nil {
log.Fatal(err.Error())
}

// Waiting for the shutdown to finish then inform the main goroutine.
<-shutChan

log.Println("app: shutdown complete")
}

internal/app/game.go


package app

import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)

type App struct {
srv *http.Server
db *sql.DB
}

func New(srv *http.Server, db *sql.DB) App {
return App{srv, db}
}

func (a App) Start(ctx context.Context, cancel context.CancelFunc, shutChan chan<- struct{}) error {
// signChan channel is responsible for transmitting signals.
signChan := make(chan os.Signal, 1)

go signalHandler(cancel, signChan)
go shutdownHandler(ctx, shutChan, a)

if err := a.db.Ping(); err != nil {
return fmt.Errorf("db: failed to connect [%v]", err)
}

if err := a.srv.ListenAndServe(); err != http.ErrServerClosed {
return fmt.Errorf("http: failed to start [%v]", err)
}

return nil
}

// signalHandler triggers shutdown after catching certain signals.
func signalHandler(cancel context.CancelFunc, signChan chan os.Signal) {
// Pass certain signals to signChan channel.
signal.Notify(signChan, os.Interrupt, syscall.SIGTERM)

// Wait for a signal and pass it on the signChan channel in order to start
// shutdown.
sig := <-signChan

log.Printf("shutdown started with %v signal\n", sig)

// Help application to terminate processes.
cancel()
}

// shutdownHandler controls shutdown.
func shutdownHandler(ctx context.Context, shutChan chan<- struct{}, a App) {
// If any shutdown request was triggered execute following lines.
<-ctx.Done()

// Wait connections 10 seconds to finish what they are doing.
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10) * time.Second)
defer cancel()

if err := a.srv.Shutdown(ctx); err != nil {
log.Printf("http: interrupted active connections [%v]\n", err)
} else {
log.Println("http: served all active connections")
}

if err := a.db.Close(); err != nil {
log.Printf("db: connection closing error [%v]\n", err)
} else {
log.Println("db: closed connections")
}

// Actual shutdown.
close(shutChan)
}