Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
*http.Server in Go 1.8 supports graceful shutdown. This is a small example.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
)
type Server struct {
logger *log.Logger
mux *http.ServeMux
}
func NewServer(options ...func(*Server)) *Server {
s := &Server{
logger: log.New(os.Stdout, "", 0),
mux: http.NewServeMux(),
}
for _, f := range options {
f(s)
}
s.mux.HandleFunc("/", s.index)
return s
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
func (s *Server) index(w http.ResponseWriter, r *http.Request) {
s.logger.Println("GET /")
w.Write([]byte("Hello, World!"))
}
func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
logger := log.New(os.Stdout, "", 0)
addr := ":" + os.Getenv("PORT")
if addr == ":" {
addr = ":2017"
}
s := NewServer(func(s *Server) { s.logger = logger })
h := &http.Server{Addr: addr, Handler: s}
go func() {
logger.Printf("Listening on http://0.0.0.0%s\n", addr)
if err := h.ListenAndServe(); err != nil {
logger.Fatal(err)
}
}()
<-stop
logger.Println("\nShutting down the server...")
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
h.Shutdown(ctx)
logger.Println("Server gracefully stopped")
}
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jan 28, 2017

You can install go1.8rc3 alongside your stable Go installation

go get golang.org/x/build/version/go1.8rc3
go1.8rc3 download
go1.8rc3 run graceful.go
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jan 28, 2017

Smaller example, with no mux or constructor

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
)

func main() {
	stop := make(chan os.Signal, 1)

	signal.Notify(stop, os.Interrupt)

	addr := ":" + os.Getenv("PORT")
	if addr == ":" {
		addr = ":2017"
	}

	h := &http.Server{Addr: addr, Handler: &server{}}

	logger := log.New(os.Stdout, "", 0)

	go func() {
		logger.Printf("Listening on http://0.0.0.0%s\n", addr)

		if err := h.ListenAndServe(); err != nil {
			logger.Fatal(err)
		}
	}()

	<-stop

	logger.Println("\nShutting down the server...")

	h.Shutdown(context.Background())

	logger.Println("Server gracefully stopped")
}

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, World!"))
}
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 8, 2017

Example with graceful function

EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it

package main

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

func main() {
	hs, logger := setup()

	go func() {
		logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)

		if err := hs.ListenAndServe(); err != http.ErrServerClosed {
			logger.Fatal(err)
		}
	}()

	graceful(hs, logger, 5*time.Second)
}

func setup() (*http.Server, *log.Logger) {
	addr := ":" + os.Getenv("PORT")
	if addr == ":" {
		addr = ":2017"
	}

	hs := &http.Server{Addr: addr, Handler: &server{}}

	return hs, log.New(os.Stdout, "", 0)
}

func graceful(hs *http.Server, logger *log.Logger, timeout time.Duration) {
	stop := make(chan os.Signal, 1)

	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

	<-stop

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	logger.Printf("\nShutdown with timeout: %s\n", timeout)

	if err := hs.Shutdown(ctx); err != nil {
		logger.Printf("Error: %v\n", err)
	} else {
		logger.Println("Server stopped")
	}
}

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	time.Sleep(5 * time.Second)
	w.Write([]byte("Hello, World!"))
}
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 13, 2017

Example with multiple endpoints

EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it

package main

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

type server struct {
	logger *log.Logger
	mux    *http.ServeMux
}

func newServer(options ...func(*server)) *server {
	s := &server{mux: http.NewServeMux()}

	for _, f := range options {
		f(s)
	}

	if s.logger == nil {
		s.logger = log.New(os.Stdout, "", 0)
	}

	s.mux.HandleFunc("/", s.index)
	s.mux.HandleFunc("/hello/", s.hello)

	return s
}

func (s *server) index(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!"))
}

func (s *server) hello(w http.ResponseWriter, r *http.Request) {
	message := "Hello " + strings.TrimPrefix(r.URL.Path, "/hello/")

	s.logger.Println(message)

	w.Write([]byte(message))
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Server", "example Go server")

	s.mux.ServeHTTP(w, r)
}

func main() {
	hs, logger := setup()

	go func() {
		logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)

		if err := hs.ListenAndServe(); err != http.ErrServerClosed {
			logger.Fatal(err)
		}
	}()

	graceful(hs, logger, 5*time.Second)
}

func setup() (*http.Server, *log.Logger) {
	addr := ":" + os.Getenv("PORT")
	if addr == ":" {
		addr = ":2017"
	}

	logger := log.New(os.Stdout, "", 0)

	s := newServer(func(s *server) {
		s.logger = logger
	})

	hs := &http.Server{Addr: addr, Handler: s}

	return hs, logger
}

func graceful(hs *http.Server, logger *log.Logger, timeout time.Duration) {
	stop := make(chan os.Signal, 1)

	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

	<-stop

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	logger.Printf("\nShutdown with timeout: %s\n", timeout)

	if err := hs.Shutdown(ctx); err != nil {
		logger.Printf("Error: %v\n", err)
	} else {
		logger.Println("Server stopped")
	}
}
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 13, 2017

Example with separate main and server packages

EDIT: This example was actually wrong as pointed out by @pci below, I've now corrected it

main.go

package main

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

	"./server" // This import should be changed to something like github.com/<user>/<project>/server
)

func main() {
	hs, logger := setup()

	go func() {
		logger.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)

		if err := hs.ListenAndServe(); err != http.ErrServerClosed {
			logger.Fatal(err)
		}
	}()

	graceful(hs, logger, 5*time.Second)
}

func setup() (*http.Server, *log.Logger) {
	addr := ":" + os.Getenv("PORT")
	if addr == ":" {
		addr = ":2017"
	}

	logger := log.New(os.Stdout, "", 0)

	return &http.Server{
		Addr:    addr,
		Handler: server.New(server.Logger(logger)),
	}, logger
}

func graceful(hs *http.Server, logger *log.Logger, timeout time.Duration) {
	stop := make(chan os.Signal, 1)

	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

	<-stop

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	logger.Printf("\nShutdown with timeout: %s\n", timeout)

	if err := hs.Shutdown(ctx); err != nil {
		logger.Printf("Error: %v\n", err)
	} else {
		logger.Println("Server stopped")
	}
}

server/server.go

package server

import (
	"log"
	"net/http"
	"os"
	"strings"
)

type Server struct {
	logger *log.Logger
	mux    *http.ServeMux
}

func New(options ...func(*Server)) *Server {
	s := &Server{mux: http.NewServeMux()}

	for _, f := range options {
		f(s)
	}

	if s.logger == nil {
		s.logger = log.New(os.Stdout, "", 0)
	}

	s.mux.HandleFunc("/", s.index)
	s.mux.HandleFunc("/hello/", s.hello)

	return s
}

func Logger(logger *log.Logger) func(*Server) {
	return func(s *Server) {
		s.logger = logger
	}
}

func (s *Server) index(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello world!"))
}

func (s *Server) hello(w http.ResponseWriter, r *http.Request) {
	message := "Hello " + strings.TrimPrefix(r.URL.Path, "/hello/")

	s.logger.Println(message)

	w.Write([]byte(message))
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Server", "example Go server")

	s.mux.ServeHTTP(w, r)
}
$ go run main.go
Listening on http://0.0.0.0:2017

And now you should be able to curl http://0.0.0.0:2017/hello/yourname

@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 19, 2017

Using the github.com/TV4/graceful package

EDIT: This example is actually wrong as pointed out by @pci below, I've kept it as is in order for his comment to make sense.

You probably want to use graceful.ListenAndServe or graceful.LogListenAndServe instead of graceful.Shutdown

I have written the github.com/TV4/graceful package in order to reduce boilerplate code needed to set up a *http.Server with graceful shutdown.

package main

import (
	"log"
	"net/http"

	"github.com/TV4/graceful"
)

func main() {
	hs := &http.Server{Addr: ":2017", Handler: &server{}}

	go graceful.Shutdown(hs)

	log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)

	if err := hs.ListenAndServe(); err != http.ErrServerClosed {
		log.Fatal(err)
	}
}

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello!"))
}
@pci

This comment has been minimized.

Copy link

pci commented Jun 22, 2017

@peterhellberg Hey these are really useful, thanks for putting them together. One thing though, I was testing with some slow requests and it seems like: when server.Shutdown() is called server.ListenAndServe returns straight away, even if there are ongoing requests, which then means that the main function exits and the requests are still killed early. In testing a request of a second or two is enough to see the effect.

My workaround was to pass a done channel to graceful so it can report back once the server has shutdown, but would there be any better ways?

Thanks again for these, in my searching they were definitely the best examples online of how to actually use server.Shutdown in practice.

@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 29, 2017

@pci Yes, you are absolutely correct. And the example I gave above is actually wrong.

If you block on graceful.Shutdown instead it seems to work as intended:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/TV4/graceful"
)

func main() {
	hs := &http.Server{Addr: ":2017", Handler: &server{}}

	go func() {
		log.Printf("Listening on http://0.0.0.0%s\n", hs.Addr)

		if err := hs.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatal(err)
		}
	}()

	graceful.Shutdown(hs)
}

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	time.Sleep(2 * time.Second)
	w.Write([]byte("Hello!"))
}

(And if you change the sleep from 2 to 20 seconds you'll hit the graceful.DefaultTimout of 15 seconds and get Error: context deadline exceeded in the log)

@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 29, 2017

@pci I have now simplified it even further by adding a ListenAndServe function to the graceful package.

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/TV4/graceful"
)

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	time.Sleep(2 * time.Second)
	w.Write([]byte("Hello!"))
}

func main() {
	addr := ":2017"

	log.Printf("Listening on http://0.0.0.0%s\n", addr)

	graceful.ListenAndServe(&http.Server{
		Addr:    addr,
		Handler: &server{},
	})
}
@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Jun 29, 2017

@pci Since I do logging on the listening URL in almost every service I write I've now also added LogListenAndServe:

package main

import (
	"net/http"
	"time"

	"github.com/TV4/graceful"
)

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	time.Sleep(2 * time.Second)
	w.Write([]byte("Hello!"))
}

func main() {
	graceful.LogListenAndServe(&http.Server{
		Addr:    ":2017",
		Handler: &server{},
	})
}
$ go run example.go
Listening on http://0.0.0.0:2017
^C
Shutdown with timeout: 15s
Server stopped
@pci

This comment has been minimized.

Copy link

pci commented Jun 30, 2017

@peterhellberg Nice additions 👍

@wangjinbei

This comment has been minimized.

Copy link

wangjinbei commented Sep 8, 2017

hi~~
The line 62 of first demo "graceful.go" "logger.Fatal(err)" maybe lead to last print (" Server gracefully stopped" )not display

@peterhellberg

This comment has been minimized.

Copy link
Owner Author

peterhellberg commented Sep 18, 2017

@wangjinbei: Yes, this gist mainly contains improvements to the original code in the comments. I didn't want to change it too much in order to not confuse people :)

@pcasaretto

This comment has been minimized.

Copy link

pcasaretto commented Oct 12, 2017

@peterhellberg Great examples!
Go vet would indicate a leak on the cancel function for the first one on context.WithTimeout.

@xiazhibin

This comment has been minimized.

Copy link

xiazhibin commented May 11, 2018

I got Error: context deadline exceeded when I first to request http://0.0.0.0:2017 and then shutdown in Example with graceful function

@sunho

This comment has been minimized.

Copy link

sunho commented Jun 18, 2018

There are more below lol. Great job!

@pseidemann

This comment has been minimized.

Copy link

pseidemann commented Jun 19, 2018

hey,
I created a package which does all the work for you with a simple api:
https://github.com/pseidemann/finish
let me know what you think!

@fr-sgujrati

This comment has been minimized.

Copy link

fr-sgujrati commented Jun 20, 2018

@peterhellberg Thanks for posting these examples. Really helpful.

@Jeiwan

This comment has been minimized.

Copy link

Jeiwan commented Jul 6, 2018

@peterhellberg Thanks for sharing this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.