Liam Clarke brand
Back to the top
What will we build?
Who is this for?
Package
Initialising the Server
Creating the Shutdown method
Creating the ListenAndServe method
Example
Outro

Building a graceful HTTP Server in Go

Design Pattern
16 December 2021 3 minute read
12 Factor AppGoGolangGraceful Shut DownHttp Server

One of the 12-factor app methodologies is Disposability, in the context of an HTTP Server that means a Server can be started and stopped at any time during a service's lifecycle and should be able to gracefully shutdown without interrupting any active connections.

Processes shut down gracefully when they receive a SIGTERM signal from the process manager

source: The Twelve-Factor App: Disposability

What will we build?

We will be building a production-ready HTTP Server that is disposable and configurable.

Who is this for?

This article is intended for those who are already familiar with Go that would like to build a production-ready HTTP Server that conforms to the 12 Factor applicable methodologies.

Package

If you would like to check out the package that was built whilst writing this article, you can find it here

Initialising the Server

Let's start by initialising a basic Server that will wrap the net/http server with some configurable options as struct fields.

// Server is a wrapper around the net/http server
// to handle graceful behaviours.
type Server struct {
	Handler         http.Handler
	Address         string
	MaxHeaderBytes  int
	GracefulTimeout time.Duration

	server       *http.Server
	shutdownOnce sync.Once
	shutdownErr  chan error
}

// New initialises a new instance of Server.
func New(handler http.Handler) *Server {
	server := &Server{
		Handler:         handler,
		Address:         "",
		MaxHeaderBytes:  http.DefaultMaxHeaderBytes,
		GracefulTimeout: 10 * time.Second,

		shutdownErr: make(chan error, 1),
	}

	return server
}

Creating the Shutdown method

Next, we encapsulate the Shutdown logic into a method. This will allow us to handle the Shutdown when the Server receives an error, a signal from the process manager is received or a manual shutdown.

// Shutdown shuts down the Server gracefully
// until the GracefulTimeout duration expires.
func (s *Server) Shutdown() {
	s.shutdownOnce.Do(func() {
		ctx, cancel := context.WithTimeout(context.Background(), s.GracefulTimeout)
		defer cancel()

		if err := s.server.Shutdown(ctx); err != nil {
			s.shutdownErr <- errors.Wrap(ErrUnableToGracefulShutdown, err.Error())
			return
		}

		s.shutdownErr <- nil
	})
}

Creating the ListenAndServe method

Now to create the ListenAndServer method, which will listen and serve the HTTP server and start a goroutine that will wait for any interrupt signal from the process manager and then invoke the Shutdown method.

// ListenAndServe listens on the TCP network
// Server Address and then calls http.Serve.
func (s *Server) ListenAndServe() error {
	srv := &http.Server{
		Addr:           s.Address,
		Handler:        s.Handler,
		MaxHeaderBytes: s.MaxHeaderBytes,
	}

	s.server = srv

	go func(s *Server) {
		shutdownChan := make(chan os.Signal, 1)
		signal.Notify(shutdownChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)

		<-shutdownChan
		s.Shutdown()
	}(s)

	if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		s.shutdownErr <- errors.Wrap(ErrUnableToListenAndServe, err.Error())
	}

	return <-s.shutdownErr
}

Example

Now we have a Server that is configurable, disposable and can be accessed for admin processes, that is usable in a 12 Factor-App, we can use it on a service that requires an HTTP server.

Here is a basic example with a gin-gonic/gin router.

package serve

import (
	"github.com/gin-gonic/gin"
	"github.com/clarke94/serverfx"
)

// InitializeServer builds all the Server
// dependencies and invokes the Serve function.
func InitializeServer() error {
	router := gin.Default()
	server := serverfx.New(
		router,
		serverfx.WithAddress(fmt.Sprintf(":%s", os.Getenv("PORT"))),
	)

	appRoutes(router)

	return server.Serve()
}

// appRoutes applies all the application HTTP Routes.
func appRoutes(router *gin.Engine) {
	router.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
}

Outro

If you made it this far, thank you for reading, I hope you found the article useful.

If you have any questions, feedback or concerns, please feel free to comment or contact me directly.

You can find the complete example in the serverfx repository