Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Last active August 20, 2023 08:49
Show Gist options
  • Save peterhellberg/38117e546c217960747aacf689af3dc2 to your computer and use it in GitHub Desktop.
Save peterhellberg/38117e546c217960747aacf689af3dc2 to your computer and use it in GitHub Desktop.
*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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
Copy link
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
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
Copy link
Author

@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
Copy link
Author

@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
Copy link
Author

@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
Copy link

pci commented Jun 30, 2017

@peterhellberg Nice additions 👍

@wangjinbei
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
Copy link
Author

@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
Copy link

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

@xiazhibin
Copy link

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
Copy link

sunho commented Jun 18, 2018

There are more below lol. Great job!

@pseidemann
Copy link

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
Copy link

@peterhellberg Thanks for posting these examples. Really helpful.

@Jeiwan
Copy link

Jeiwan commented Jul 6, 2018

@peterhellberg Thanks for sharing this!

@dciccale
Copy link

dciccale commented Apr 3, 2020

Hi, great contribution.

There is one thing that could make it more complete in my opinion.

Everywhere this code is, it will always print "Listening on http://...." in the console, even if ListenAndServe() was unable to start actually listening.

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

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

If you start 1 server, you'll see this in the console:

Listening on http://0.0.0.0:2017

If you start a 2nd server without shutting down the 1st one, you will see this in the console:

Listening on http://0.0.0.0:2017
panic... listen tcp :2017: bind: address already in use

So it first shows "Listening on http://..." however this never actually happened.

I'm not a go expert, is there a way to implement this?

-- UPDATE

Apparently using http.Listen and http.Serve individually, it is possible to catch a port in use error (or any other error while trying to listen to a tcp network) before assuming the server is listening.

Source: https://stackoverflow.com/a/48250354/194630

	listener, err := net.Listen("tcp", ":"+config.Port)
	if err != nil {
		log.Fatal(err)
	}

	done := make(chan bool)
	go server.Serve(listener)

	// Log server started
	log.Printf("Server started at port %v", config.Port)
	<-done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment