Running multiple HTTP servers in Go
By: Uday Hiwarale
Tags: Programming, Golang, Go Programming Language, Golang Tutorial, Go Programming

Server type

To create and configure a custom HTTP server, http package provides Server structure. This structure has below public fields (from the doc).

type Server struct { // TCP address to listen on, ":http" or ":https" if empty

Addr string

Handler // http.DefaultServeMux if `nil`Handler Handler

TLSConfig *Config // TLS configuration for HTTPS protocolTLSConfig * tls

// request, including the body.

ReadTimeout Duration // ReadTimeout is the maximum duration for reading the entire// request, including the body.ReadTimeout time

// writes of the response.

WriteTimeout Duration // WriteTimeout is the maximum duration before timing out// writes of the response.WriteTimeout time

// the base context for incoming requests on this server.

// If BaseContext is nil, the default is context.Background()

BaseContext func(Listener) context.Context // BaseContext optionally specifies a function that returns// the base context for incoming requests on this server.// If BaseContext is nil, the default is context.Background()BaseContext func( net // ErrorLog specifies an optional logger for errors accepting

// connections, unexpected behavior from handlers, and

// underlying FileSystem errors.

ErrorLog *log.Logger

}

We are familiar with the Addr and Handler fields. The ReadTimeout and WriteTimeout fields can be important for added safety. ErrorLog is crucial to log any errors thrown by the server while processing a request.

💡 There are other public fields that are not mentioned above but could be crucial for your use case.

The Server structure implements basic methods like ListenAndServe , SetKeepAlivesEnabled , Shutdown , etc. to configure and control a server.

Let’s create some custom servers and run them concurrently.

In the above program, the createServer function creates a ServerMux object and implements a fresh routing mechanism. The server object contains the implementation of a brand new HTTP server based on input arguments.

It then returns a pointer to the newly created Server object since most of the methods implemented by the Server structure has a pointer receiver.

In the main method, things are not so different. Instead of using http.ListenAndServe method to spawn an HTTP server with the default configuration, we are creating a brand new instance of an HTTP server inside goroutines and spawning it using Server.ListenAndServe() method call.

💡 Unlike http.ListenAndServe(addr string, handler Handler) method call, the server.ListenAndServe() method call does not need any arguments since the configuration of the server is present in the server struct itself. The server.ListenAndServe function internally creates a tcp listener on address addr using net.Listen function which returns a net.Listener and uses it with server.Serve function to listen to incoming connections using the server.Handler field value (a ServeMux object).

Let’s look at the common methods that can be invoked on the Server object.

SetKeepAlivesEnabled method

The Server.SetKeepAliveEnabled method enables the HTTP persistent connection. By default, this is enabled for better performance.

server.SetKeepAlivesEnabled(false)

Close method

Once we no longer need a server, we can close it without terminating the main program. This is achieved using the server.Close method. This method forcefully shutdown a server without gracefully closing the active TCP connection.

error := server.Close()

Hence, this method can only be used when a server needs to be shut down immediately without caring about active TCP connections.

In the above program, we have created a bare minimal server and spawned it in the main method. We have also created a exitSignal channel to block the main goroutine unless the server is closed. Even after server exits, the main goroutine is blocked until unless some goroutine writes a value to or closes the exitSignal channel.

Using time.AfterFunc function, we have launched another goroutine that executes a function after 3 seconds. In this function, we are calling server.Close() method which closes the server. This function call may return an error but before that happens, the server may close already and exit with an error.

This is why exitSignal channel is so important in this scenario. Once server.Close() is returned, we are closing the exitSignal channel using built-in close function.

Once the exitSignal channel is closed, this goroutine will be killed and control will pass back to main goroutine. Since exitSignal channel is now closed, main goroutine is no longer blocked and it will start executing the code below the read from channel ( <- chan ) expression.

Once you run this Go program, after 3 seconds, you will be able to see this result in your console.

$ go run go-server.go (took 3s)

ListenAndServe(): http: Server closed

Close(): completed! <nil>

Main(): exit complete!

The only drawback of Close method is that it can not close hijacked connections like WebSockets. But we will look into that in other lessons.

Shutdown method

The server.Shutdown method behaves exactly like server.Close method but with some added safety. If you want to gracefully close active TCP connections, then Shutdown() is better compared to Close method.

One good advantage of using Shutdown method over Close is that we can perform some cleanup operations after a server shutdown is initiated. This can be done by registering one or more cleanup functions using RegisterOnShutdown method.

server.RegisterOnShutdown(f func())

These cleanup functions will be run as separate goroutines and they will be invoked as soon as the shutdown process is started. Like Close method, Shutdown method can not close upgraded or hijacked connections. Hence this is a good place to close these connections gracefully.

This method needs a base context of the incoming requests. If you haven’t configured the BaseContext field of the Server structure, you can use the context.Background() since it is the default value for the BaseContext field.

The Shutdown method does not wait for cleanup job to be completed. Also, the server will return immediately once the Shutdown call is made. Hence it is mandatory to keep main goroutine alive until the server shutdown and server cleanup jobs are completed. For that, we can use channels or a WaitGroup like we did in the previous examples.

Let’s modify our earlier example and register a cleanup function.

In the above example, instead of using two channels to signal exit, we are using a WaitGroup . WaitGroup wg will wait for 2 goroutines to finish their jobs, one that closes the server and the other that performs the cleanup.

The ideal output of the above program should be as following. However, results may vary depending upon your system. But in all the cases, Main(): exit complete! will be the last statement.

ListenAndServe(): http: Server closed

Shutdown(): completed! <nil>

RegisterOnShutdown(): completed!

Main(): exit complete!