Microservices in Golang, pt1 - Basic HTTP server

Go language has a built in standard library to deal with HTTP, while basic, we can build complete microservices with it.

We start by creating a main.go file that has the main function and import the net/http package. For the basics, we use the http package ListenAndServe function, that takes two parameters, an address to bind to ( such as 127.0.0.1:9000 or any ip by writing it down as :9000) and a handler.

The most basic structure that listens and serve on the port 9000:

package main

import (
  "log"
  "net/http"
)

func main() {
  http.ListenAndServe(":9000", nil)
}

We can run the command go run main.go to start the http server on the port 9000. To make an http request to test the service, either open your browser and request you local machine ip and port addresses, or in the command line run:

curl -v localhost:9000

Since we’re not handling the request by returning a response, we’ll get “404 page not found”.

What happened is that ListenAndServe starts an HTTP server with a given address and handler. The handler is nil, which means to use DefaultServeMux. Since we haven’t declared Handle or HandleFunc to add handlers to DefaultServeMux, the request is not fullfiled and returns a 404.

With that being said, if we declare a HandleFunc to a particular pathname such as / and log we should expect to see data once we execute the curl request.

package main

import (
  "log"
  "net/http"
)

func main() {
  http.HandleFunc("/", func(http.ResponseWriter, *http.Request) {
    log.Println("Hello world")    
  })
  http.ListenAndServe(":9000", nil)
}

Make sure the previous service is stopped (CTRL+C on MacOS command line) and re-run go run main.go.

The output should look like:

1970/1/1 17:21:11 Hello world

The log data is not displayed if you test in a browser client.

What’s the DefaultServeMux?#

The default ServeMux is a Go struct that acts as a HTTP request multiplexer (abbreviated mux, is a device that has multiple inputs and one output) that 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.

Sources https://golang.org/src/net/http/server.go, https://golang.org/pkg/net/http/

About Handle and HandleFunc#

A Handler responds to an HTTP request, it’s an interface that defines the ServeHTTP function, called by the ServeMux (that processes Http requests and responses). See ln 62 in https://golang.org/src/net/http/server.go

package main
 
import (
 "fmt"
 "net/http"
)
 
func rootHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprint(w, "Hello world")
}
 
func main() {
 http.HandleFunc("/", rootHandler)
 http.ListenAndServe(":9000", nil)
}

There are two downsides to the builtin mux (see benchmarks https://github.com/julienschmidt/go-http-routing-benchmark#conclusions).

Multiple handlers#

We can add as many handlers as we want. Since it’ll be hard to maintain, we’ll look into organising the code a bit better later on.

For the moment, all you have to know is that when the request path has a match, it runs the specific handler otherwise the default / handler.

package main
 
import (
  "fmt"
 "log"
 "net/http"
)
 
func rootHandler(w http.ResponseWriter, r *http.Request) {
  log.Println("Hello world")
}

func otherHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprint(w, "OtherHandler response!")
}
 
func main() {
 http.HandleFunc("/", rootHandler)
 http.HandleFunc("/other", otherHandler)
 http.ListenAndServe(":9000", nil)
}

Note that so far we’ve executed our code with go run, we are not compiling at this point as go run is good enough for testing. And also, we have a different way of logging by using the fmt package function instead. By default the log package offers more functionality, but fmt writes by default to standard out (stdout).

Find more about logging here .

How to read from the HTTP request body#

The HTTP request body is of type io.ReadCloser (https://golang.org/pkg/io/#ReadCloser) and ReadCloser is the interface that groups the basic Read and Close methods.

type ReadCloser interface {
    Reader
    Closer
}

The package ioutil implements some I/O utility functions.

So, we’ll use it to read the data from the Http request body and assign its valud to the variable d. Will omit the error returned along the data for now and Printf the output of d.

func rootHandler(rw http.ResponseWriter, r *http.Request) {
	log.Println("Hello world!")
	d, _ := ioutil.ReadAll(r.Body)

	log.Printf("Data %s\n", d)
}

Use the flag -d to pass data to the curl request:

curl -v -d 'hell yeah' localhost:9000

The program output is:

1970/1/1 17:50:52 Data hell yeah

While the curl response:

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST / HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 9 out of 9 bytes
< HTTP/1.1 200 OK
< Date: Tue, 1 Jan 1970 17:45:52 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
* Closing connection 0

How to write to the HTTP response?#

The first argument of our handler is a http.ResponseWriter interface, used by an HTTP handler to construct an HTTP response. An http.ResponseWriter value assembles the HTTP server’s response; by writing to it, we send data to the HTTP client.

type ResponseWriter interface {
    Header() Header
    Write([]byte) (int, error)
    WriteHeader(statusCode int)
}

A ResponseWriter may not be used after the Handler.ServeHTTP method has returned.

If we inspect the ln95 of the source-code https://golang.org/src/net/http/server.go, will realise that it has a method called Write that responds with (int, error) that corresponds to io.Writer, which means we can use a ResponseWriter anywhere we have io.Writer.

func rootHandler(rw http.ResponseWriter, r *http.Request) {
	log.Println("Hello world!")
	d, _ := ioutil.ReadAll(r.Body)

	fmt.Fprintf(rw, "Hello %s", d)
}

This time instead of printing to the Log it’ll print it back to the ResponseWriter, which means it’ll be returned to the HTTP client.

If we’d run the curl command this time, it’d output:

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST / HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 9
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 9 out of 9 bytes
< HTTP/1.1 200 OK
< Date: Tue, 1 Jan 1970 17:45:52 GMT
< Content-Length: 15
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello hell yeah* Closing connection 0

Error handling#

The WriteHeader allow us to specify the http status code back to the caller. The HTTP package has a bunch of useful status code available as constants (see https://golang.org/pkg/net/http/).

func rootHandler(rw http.ResponseWriter, r *http.Request) {
	log.Println("Hello world!")
  d, err := ioutil.ReadAll(r.Body)
  
	if err != nil {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Oops!"))
		return
	}

	fmt.Fprintf(rw, "Hello %s", d)
}

That’s the most basic way we could handle write the response error, but the http package provide us with a function http.Error that is easier.

func rootHandler(rw http.ResponseWriter, r *http.Request) {
	log.Println("Hello world!")
  d, err := ioutil.ReadAll(r.Body)
  
	if err != nil {
		http.Error(rw, "Oops!", http.StatusBadRequest)
		return
	}

	fmt.Fprintf(rw, "Hello %s", d)
}

We still need the return statement since http.Error does not terminate the flow of the application.

Patterns and structuring the code#

We’re going to think about testing at some point, so how would we test this code? Other then manually? These are some of the things we need to think about, when things start getting too complex, it’s important to find a good structure to make it easier to maintain, to start with.

The first step that I think we should want to do, is to get all of the content of our handlers into an independent object. Before we understand how that’d work, we’re going to have to understand that Go is a Class-less programming language;

Although, Golang is considered an Object oriented programming language, so we can consider that in Go an Object is a value, or a variable that has methods; and a method is simply a function that is associated to a type.

Method declarations#

Given the example of a type called Message:

type Message struct {
  m *string
}

A method declaration for a type follows the pattern, function declaration that takes the argument type (the argument takes the first character of its type name, as its name by convention in the Golang community), and a method name and the return type.

func (c *Message) SayMessage(m *string) string {
  return m
}

Refactoring#

Now that we understand how to create method declarations, we’re set to refactor the existing source-code to make it easier to maintain.

Our main goal is to start by moving out all the code related to the handlers. You’ll want to keep a copy of the current handler code to refactor:

package main

import (
	"net/http"
)

func main() {
	http.ListenAndServe(":9000", nil)
}

We’ll want to create a new package under a new directory, for example handlers and create a new file hello.go. The names don’t necessarily need to be the ones I’ve selected but are used here as an example.

Also, you should understand that to make packages importable across the project you have to run the go.mod init command.

go mod init <module-name>

At any time you can run the go help command, such as go help mod init to find help

Init initializes and writes a new go.mod to the current directory,
in effect creating a new module rooted at the current directory.
The file go.mod must not already exist.

Our goal is to follow the method declaration pattern explained above, but also make our code reusable where possible (so bare that in mind as we’ll find about dependency injection a bit later).

We’ll also have to create a method that conforms to the type:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

Given that our http.Handle function signature is:

func Handle(pattern string, handler Handler) {
  DefaultServeMux.Handle(pattern, handler)
}

This might look a bit too complicated, but as long you get used to reading the type definitions, function signatures, it’s a matter of conforming to the correct patterns where it applies.

I’ve left the code commented out, as I feel it’s easier to follow:

package handlers

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

// Hello is the handler and its type will conform to the HTTP Handler interface
// thus should have a method declaration ServeHTTP coming up after
type Hello struct {
	l *log.Logger
}

// NewHello creates a new instance of Hello handler
// as we expect to do dependency injection (for testing, mocking for example)
func NewHello(l *log.Logger) *Hello {
	return &Hello{l}
}

// The function signature takes a method `ServeHTTP`, similarily to
// the `Handler` type explained above
func (h *Hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	h.l.Println("Hello world!")
	d, err := ioutil.ReadAll(r.Body)

	if err != nil {
		http.Error(rw, "Oops!", http.StatusBadRequest)
		return
	}

	fmt.Fprintf(rw, "Hello %s", d)
}

In the function body I have refactored the code a bit. We use the h object to access the l log.logger.

Refactoring main#

In the example below, we have a new instance of Hello Handler that we’ve just created; that requires a new instance of log; outputs to the operating system standard out. As you can tell, you can have different loggers outputing to anywhere conformant to io.Writer, as discussed above.

The Hello Handler need to be registered to our server, so how to do that?

Originally the http.HandlerFunc registers the handler function for the given pattern to the DefaultServeMux. We know that the DefaultServeMux is a multiplexer (similar to a router) that allow us to have multiple handlers that contains logic to be able to determine which one to call based on the path. Remember that when we initialise the HTTP listener, we didn’t specify the HTTP Handler and the server defaults to the DefaultServeMux.

We can have our own serve mux to make things a bit more clear! Create a new serve mux, since the serve mux implements the Handler interface. We then register paths in our serve mux Handle.

ServeMux

Handle func(pattern string, handler Handler)

Handle registers the handler for the given pattern in the DefaultServeMux.
The documentation for ServeMux explains how patterns are matched.

After refactoring it’d look like:

package main

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

	"example.com/go-intro-microservices/handlers"
)

func main() {
	// We create a new instance of logger that outputs to Stdout
	// prefixed by `product-api` and the flag is `standard flags`
	l := log.New(os.Stdout, "product-api", log.LstdFlags)
	// Creates a new reference of Handlers and it requires a new logger
	hh := handlers.NewHello(l)

	// Creates a new server mux
	sm := http.NewServeMux()
	// Registers a handler for a path to our server mux
	sm.Handle("/", hh)

	log.Println("Server running on port 9000")

	// We specificy my own server mux that we created above
	http.ListenAndServe(":9000", sm)
}

To test we can run:

go run main.go

And expect the same result as before when we run:

curl -v -d 'Jackson' localhost:9000

Outputs:

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 9000 (#0)
> POST / HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 7 out of 7 bytes
< HTTP/1.1 200 OK
< Date: Thu, 1 Jan 1970 14:39:52 GMT
< Content-Length: 13
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello Jackson* Closing connection 0

While the program outputs for each request:

product-api1970/1/1 15:51:48 Hello world!

To summarize, I’ll break down the creation of a Goodbye handler following the same rules.

First, add to package handlers a new type Goodbye on its own separate file, such as handlers/goodbye.go. Since generics are not supported at the time of writing in golang we’ll copy the Hello struc. And the method declaration follows.

package handlers

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type Goodbye struct {
	l *log.Logger
}

func (g *Goodbye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	g.l.Println("Goodbye world!")
	d, _ := ioutil.ReadAll(r.Body)

	fmt.Fprintf(rw, "Goodbye %s", d)
}

Second, we check if errors exist and thrown an http error, and break the request flow. And a new getter function.

package handlers

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

type Goodbye struct {
	l *log.Logger
}

func NewGoodbye(l *log.Logger) *Goodbye {
	return &Goodbye{l}
}

func (g *Goodbye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	g.l.Println("Goodbye world!")
	d, err := ioutil.ReadAll(r.Body)

	if err != nil {
		http.Error(rw, "Ooops!", http.StatusBadRequest)
		return
	}

	fmt.Fprintf(rw, "Goodbye %s", d)
}

// or alternatively
// func (g *Goodbye) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// 	rw.Write([]byte("Goodbye world!"))
// }

To complete, our main function would look like:

func main() {
	l := log.New(os.Stdout, "product-api", log.LstdFlags)

	hh := handlers.NewHello(l)
	gh := handlers.NewGoodbye(l)

	sm := http.NewServeMux()

	sm.Handle("/", hh)
	sm.Handle("/goodbye", gh)

	http.ListenAndServe(":9000", sm)
}

Congrats! We’ve successfully managed to keep our handlers in different files, and consume it in our main function by registering each handler and a pathname to our serve mux. But what’d happen if we run out of resources in our server? Or the requests start taking too long causing a denial of service?

How to tune the HTTP server?#

When requests start targeting our HTTP server, resources are only enough to a certain capacity, to prevent failures from breaking our service we should tune our HTTP server and optimize it.

The http.ListenAndServe is a convenience function, pretty basic, that opens a socket and calls Serve, ideally we want to control the server options to fine tune it to our needs. For this reason, we’ll create our own HTTP Server.

In Golang the http.Server is a type.

A Server defines parameters for running an HTTP server.
The zero value for Server is a valid configuration.

type Server struct {
  ...
}

You can find more about the type here .

Spend some time getting used to the golang documentation, as you should fine the correct parameters that you can pick to fine tune your server. As an example:

s := http.Server{
  Addr:         ":9000",
  Handler:      sm,
  IdleTimeout:  120 * time.Second,
  ReadTimeout:  1 * time.Second,
  WriteTimeout: 1 * time.Second,
}

Now that we have a new server object, we don’t need to rely in the http.ListenAndServe that calls the Server and have our own configured version.


func main() {
	l := log.New(os.Stdout, "product-api", log.LstdFlags)

	hh := handlers.NewHello(l)
	gh := handlers.NewGoodbye(l)

	sm := http.NewServeMux()

	sm.Handle("/", hh)
	sm.Handle("/goodbye", gh)

	s := &http.Server{
		Addr:         ":9000",
		Handler:      sm,
		IdleTimeout:  120 * time.Second,
		ReadTimeout:  1 * time.Second,
		WriteTimeout: 1 * time.Second,
	}

	s.ListenAndServe()
}

Shutting down the HTTP Server gracefully#

Shutting down an HTTP Server gracefully might not be obvious at this point, but let’s say that we want to allow 30 seconds to attempt to gracefully shutdown the work that is processing, if the Handlers are still working during the period forcefully close it.

In Golang http.Server.Shutdown gracefully shuts down the server without interrupting any active connections. It will wait until the requests that are currently handled by the server completed, and it’ll then shutdown. Read more about it here .

Before we move on calling the Shutdown, we need to refactor our current code a bit since it’s obvious that the call for s.ListenAndServe is a blocker; making any next calls unreachable, as the program flow is stopped at that point. To fix that we can take advance of goroutines by simply wrapping the s.ListenAndServe in a function.

go func() {
  err := s.ListenAndServe()
  if err != nil {
    l.Fatal(err)
  }
}()

With that done, the call to s.ListenAndServe is non-blocker; but consequently, it also means it’ll run the next proccess after that is s.Shutdown

What we can do is use the os.Signal package and register the notification of a certain signal. And since this is a block call, we can keep it between our serve initialisation goroutine and our Shutdown processes.

An example of this pattern follows:

sigChan := make(chan os.Signal)
signal.Notify(sigChan, os.Interrupt)
signal.Notify(sigChan, os.Kill)

sig := <-sigChan
l.Println("Terminate received, gracefully shuttingdown...", sig)

Now going back to our original intend, Shutdown takes a context, so we need to create that first.

tc, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

If you fail to cancel the context the goroutine that WithCancel or WithTimeout created will be retained in memory indefinitely (until the program shuts down) causing a memory leak.

To complete, we pass the context to our Shutdown call.

package main

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

	"example.com/go-intro-microservices/handlers"
)

func main() {
	// We create a new instance of logger that outputs to Stdout
	// prefixed by `product-api` and the flag is `standard flags`
	l := log.New(os.Stdout, "product-api", log.LstdFlags)
	// Creates a new reference of Handlers and it requires a new logger
	hh := handlers.NewHello(l)
	gh := handlers.NewGoodbye(l)

	// Creates a new server mux
	sm := http.NewServeMux()
	// Registers a handler for a path to our server mux
	sm.Handle("/", hh)
	sm.Handle("/goodbye", gh)

	log.Println("Server running on port 9000")

	s := &http.Server{
		Addr:         ":9000",
		Handler:      sm,
		IdleTimeout:  120 * time.Second,
		ReadTimeout:  1 * time.Second,
		WriteTimeout: 1 * time.Second,
	}

	go func() {
		err := s.ListenAndServe()
		if err != nil {
			l.Fatal(err)
		}
	}()

	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, os.Interrupt)
	signal.Notify(sigChan, os.Kill)

	sig := <-sigChan
	l.Println("Terminate received, gracefully shuttingdown...", sig)

	tc, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	// If you fail to cancel the context
	// the goroutine that WithCancel or WithTimeout created
	// will be retained in memory indefinitely (until the program shuts down)
	// causing a memory leak
	defer cancel()
	s.Shutdown(tc)
}

References

Go by example: HTTP servers

Building Microservices with Go

Understanding Go Standard HTTP libraries

Essential go

The Go Programming Language

comments powered by Disqus