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
│   └── client
│   └── main.go
├── go.mod
└── internal
├── app
│   ├── client.go
│   ├── handler.go
│   └── server.go
└── user
└── get.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.


main.go


package main

import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"internal/app"
)

func main() {
// Create the application instance.
application := app.App{
Server: app.Server(app.Handler()),
}

// idleChan channel is dedicated for shutting down all active connections.
// Once actual shutdown occurred by closing this channel, the main goroutine
// is shutdown.
idleChan := make(chan struct{})

go func() {
// signChan channel is used to transmit signal notifications.
signChan := make(chan os.Signal, 1)
// Catch and relay certain signal(s) to signChan channel.
signal.Notify(signChan, os.Interrupt, syscall.SIGTERM)
// Blocking until a signal is sent over signChan channel.
sig := <-signChan

log.Println("shutdown:", sig)

// Create a new context with a timeout duration. It helps allowing
// timeout duration to all active connections in order for them to
// finish their job. Any connections that wont complete within the
// allowed timeout duration gets halted.
ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()

if err := application.Stop(ctx); err == context.DeadlineExceeded {
log.Println("shutdown: halted active connections")
}

// Actual shutdown trigger.
close(idleChan)
}()

if err := application.Start(); err == http.ErrServerClosed {
log.Println("shutdown: started")
}

// Blocking until the shutdown to complete then inform the main goroutine.
<-idleChan

log.Println("shutdown: completed")
}

client.go


package app

import (
"context"
"net/http"
)

type App struct {
Server *http.Server
}

func (a App) Start() error {
return a.Server.ListenAndServe()
}

func (a App) Stop(ctx context.Context) error {
return a.Server.Shutdown(ctx)
}

handler.go


package app

import (
"net/http"

"internal/user"
)

func Handler() http.Handler {
handler := http.NewServeMux()
handler.HandleFunc("/client/api/v1/users", user.Get)

return handler
}

server.go


package app

import (
"net/http"
)

func Server(handler http.Handler) *http.Server {
return &http.Server{
Addr: ":8080",
Handler: handler,
}
}

get.go


package user

import (
"fmt"
"net/http"
"time"
)

func Get(w http.ResponseWriter, _ *http.Request) {
time.Sleep(15 * time.Second)

_, _ = w.Write([]byte(fmt.Sprintf("Response: %d", 1)))
}

Test


When there is live connections.


^C
2020/03/22 20:54:59 shutdown: interrupt
2020/03/22 20:54:59 shutdown: started
2020/03/22 20:55:04 shutdown: halted active connections
2020/03/22 20:55:04 shutdown: completed

When there isn't any live connections.


^C
2020/03/22 20:54:34 shutdown: interrupt
2020/03/22 20:54:34 shutdown: started
2020/03/22 20:54:39 shutdown: completed