Writing a service in Go with multiple HTTP REST servers

Ankur Agarwal
8 min readApr 24, 2021

A Go service that incorporates multiple HTTP REST servers using just the built in standard library.

Motivation

Most software engineers are likely to need to build HTTP REST services in their day job, or use them to explore ideas in their personal projects. We are likely to pick off the shelf libraries that take care of most of the boiler plate for us — for example where I work I only need to implement the endpoints and business logic, everything else is prepackaged, tested and maintained by another team. In most cases this is what you want — coding at the end of the day is a means to an end, and just like a hammer, you don’t need to know how it’s built, you just need it to work when it’s used.

It is still quite interesting to understand how lower level abstractions are built, which can help us become more aware of what’s going on at those levels, which in turn can influence how we code to get more out of the services we build (mechanical sympathy).

So this is an exploration on how to build a HTTP REST service in Go using just the standard library (so the go.mod file will not declare any external dependencies).

The RESTful Go Service

To follow along it’s best you have a basic understanding of Go. If you’re new to Go why not check out the following to get a better understanding of it:

Hello World

Yep, let’s start from the basics where we all start from when we’re learning something new:

Prints “Hello World” to the console

Pretty straight forward so far, but it’s missing the ability to serve REST APIs over HTTP.

Listen and Serve

Ok, so now let’s start by creating the basic server which will return 404 when we cURL the address:

Just start a server and nothing more

Here we’ve created a server that is listening on the loop back IP address 127.0.0.1 (also known as localhost) on port 8080. Try cURLing the server with:

$ curl 127.0.0.1:8080 -v
* Trying ::1...
* TCP_NODELAY set
* Connection failed
* connect to ::1 port 8080 failed: Connection refused
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Type: text/plain; charset=utf-8
< X-Content-Type-Options: nosniff
< Date: Sat, 24 Apr 2021 16:05:38 GMT
< Content-Length: 19
<
404 page not found
* Connection #0 to host localhost left intact
* Closing connection 0

That’s the basic server. You can shut it down by sending it a sigint(ctrl+c).

Hello World Server with a http.Handler

It’s not very friendly at the moment, so let’s make it return a familiar message on a new endpoint:

Returns “Hello World” on GET to /hello

We’ve created a http.Handler which implements ServeHTTP that will respond to a GET on the /hello endpoint. All other requests to anything but /hello, will return a 404.

This isn’t actually very maintainable, and requires quite a lot of boiler plate as well as having to handle the mapping of the endpoint to the handler code with if statements.

Later we will take a look at when http.Handler can be useful — for adding middleware components which can be used to intercept all incoming requests (and outgoing responses) and perform global operations on all requests such as:

  • authentication;
  • add trace and span ids to the request context (req.WithContext);
  • record duration of requests.

Hello World with http.Handle

We’re only interested in handling the /hello endpoint and nothing else, so let’s make this simpler and use http.Handle:

Using http.Handle

Instead of setting a http.Handler in ListenAndServe we’re mapping the endpoint /hello to the type that handles http.Handle. This is cleaner, more maintainable and less code than the previous example, it also has a nice side effect of making it clear as to what the endpoints this service handles.

Hello World with http.HandleFunc

We can make it even simpler still, and not have the requirement of having to create a type that implements ServeHTTP:

Using http.HandleFunc

The ServeHTTP method can be renamed to hello (to help clarify which endpoint it handles) and we need it to match the expected function signature that is required by http.HandleFunc (which happens to be the same as ServeHTTP).

Different RESTful Methods

Ok, so we can get a nice message back from our server, but sometimes we want to be able to POST or use other RESTful method types. We can do this using a switch statement in our hello function:

Handling POST and GET requests to hello endpoint

Now we’re getting closer to a fully functioning stateful server! At the moment POST just responds with another message to differentiate from theGET. Try it with cURL:

$ curl --request POST localhost:8080/hello                             
Thanks for posting to me

The Middleware Pattern

Let’s just quickly explore the middleware pattern that we came across earlier. As I mentioned before, it’s a useful pattern where by we can extend the functionality of something without changing the existing implementation — you may recognise this as one of the SOLID principles, the open closed principle.

Let’s simulate a scenario where we’re retrieving the user details given the request, and we also want to record the request duration; we will later get the user information from the context in the hello function. The start time will be set in the request context and later retrieved and used to calculate the request duration. This could/should be done in two different middleware types to separate the concerns — another SOLID principle, the single responsibility principle:

Adding a middleware that will add a value to the context and time the request

You might be wondering what the DefaultServeMux is and why we need it as a member variable in the middleware type. A ServeMux

“matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL” — from the official documentation as they explain things better.

So in our example before with serverHandler we were essentially creating a custom ServeMux where we handle the mappings ourselves. The DefaultServeMux is the default ServeMux that is used to register new endpoints when we use http.HandleFunc. By passing ListenAndServe a http.Handler we’re letting Go know that we want all requests to go through our handler instead of DefaultServeMux. What we want is to place a couple of values on the request context, and still use the DefaultServeMux to handle the endpoint mapping.

Why are we passing DefaultServeMux in to middleware and not just retrieving it when we need it with http.DefaultServeMux?DefaultServeMux implements http.Handler, and this means we can chain many different middleware components as long as they all implement http.Handler, so middleware doesn’t care or know what’s happening next, and is just concerned about it’s own job — just good bit of decoupling.

Custom HTTP Server

The default http server that is created when we use http.ListenAndServe is probably good enough for our side projects, but probably not for things we want to productionise. We can create a custom http server by creating a new http.Server in the main function:

Custom http server

There’s many more arguments that can be set in the http.Server type, take a look in the documentation. We’re still using the default ServeMux but in the next section we’ll create our own.

Multiple RESTful Servers

Ok so now we have our hello service up and running, but what if we wanted to grab diagnostics, metrics and expose an admin endpoint? We could easily add those to endpoints to the existing server, but generally it’s safer to add a new server that listens on a different port and firewall that port to only be accessible locally and not over the public internet — this is a bit far fetched for our simple server, but this is a fun exercise. Let’s do this one step at a time:

Create Our Own ServeMux

Let’s first step away from using the DefaultServeMux and use our own so that we don’t accidentally mix the hello endpoint code with the admin endpoint code:

Creating a custom ServeMux for the hello endpoint

Create an Admin ServeMux and Server

Almost a C&P of the above. Let’s change the port to 8081, and create a new endpoint /ping that will respond with pong, so it’s a simple liveness check:

Creating a custom ServeMux and endpoint to access admin endpoints

Depending on the order the servers are declared in the main function, you might only be able to get a response from one and not the other. I’ve declared the admin server last and I can’t cURL on ping:

$ curl localhost:8081/ping                                               
curl: (7) Failed to connect to localhost port 8081: Connection refused

Goroutines

Ok, time to bring out the big guns, or rather the tiny goroutines. Let’s place the two servers in their own goroutine, that’ll allow both the servers to run:

Running servers in their own goroutines

That’s not going to work — the main goroutine that started the sub goroutines exits and forces the service to shutdown. So let’s remove the go keyword from the last ListenAndServe.

Great, and now we can cURL both the hello and ping endpoints on the two different servers via different ports:

$ curl localhost:8080/hello                                              
Hello unknown
$ curl localhost:8081/ping
pong

WaitGroups

There’s a bit of a smell here — it feels wrong to block on the last ListenAndServe, although it works, it’s just not explicit or clear enough as to what’s going on. We should place the servers in their own goroutines and have the main goroutine wait until they shutdown before shutting the whole service down, we can then also add logs and other cleanup code just before the service shuts down:

Using WaitGourp to orchestrate the running of the servers in the own goroutines

Now both servers run, we can cURL the endpoints and when we sigint it shuts down, but wait, where’s the log line after wg.Wait()? A quick bit of debugging shows that the there was an exit code 1 when the server shutdown, so ListenAndServe must be calling os.Exit(1) somewhere.

$ go run main.go                                                                                    
Starting server
$ $?
zsh: command not found: 1

Signals

We want more control over the shutdown process so that we can log a message before the service shuts down. So we will need to use the signal package to help us with this:

Using signal to control the shutdown process

So we’ve removed the WaitGroup as it wasn’t actually helping to orchestrate the shutting down of the goroutines. Instead we’re orchestrating the shutdown with the use of a sigterm signal and the context. After creating the context we register the sigint signal onto the new channel of type os.Signal. The service is now registered and hooked into sigterm signals whereby it can then action on any cleanup code and logging before gracefully shutting down the service — now we’re in charge of actioning on sigterm.

We’ve deferred the shutdown of the servers so that they are called at the end. The issue with Shutdown is that it may wait indefinitely for live connections to close. To help prevent this we’ve used the context.WithCancel to force it to showdown. We could use context.WithDeadline and give the servers a sensible amount of time to let the live connections to complete what they’re doing and gracefully close.

The Complete File

The completed service implementation:

Two HTTP REST services in Go

We’re still missing many parts that may interest you, such as:

  • Path parameters;
  • Query parameters;
  • Reading from the request body;
  • Writing to the response body;
  • JSON/XML parsing;
  • TLS;
  • Go HTTP client;

Like with the server code, all this (and more) can be achieved using Go’s standard library.

--

--