Book Image

Building Microservices with Go

By : Nic Jackson
Book Image

Building Microservices with Go

By: Nic Jackson

Overview of this book

Microservice architecture is sweeping the world as the de facto pattern to build web-based applications. Golang is a language particularly well suited to building them. Its strong community, encouragement of idiomatic style, and statically-linked binary artifacts make integrating it with other technologies and managing microservices at scale consistent and intuitive. This book will teach you the common patterns and practices, showing you how to apply these using the Go programming language. It will teach you the fundamental concepts of architectural design and RESTful communication, and show you patterns that provide manageable code that is supportable in development and at scale in production. We will provide you with examples on how to put these concepts and patterns into practice with Go. Whether you are planning a new application or working in an existing monolith, this book will explain and illustrate with practical examples how teams of all sizes can start solving problems with microservices. It will help you understand Docker and Docker-Compose and how it can be used to isolate microservice dependencies and build environments. We finish off by showing you various techniques to monitor, test, and secure your microservices. By the end, you will know the benefits of system resilience of a microservice and the advantages of Go stack.
Table of Contents (18 chapters)
Title Page
Credits
About the Author
About the Reviewers
www.PacktPub.com
Customer Feedback
Preface
Index

Routing in net/http


Even a simple microservice will need the capability to route requests to different handlers dependent on the requested path or method. In Go this is handled by the DefaultServeMux method which is an instance of ServerMux. Earlier in this chapter, we briefly covered that when nil is passed to the handler parameter for the ListenAndServe function then the DefaultServeMux method is used. When we call the http.HandleFunc("/helloworld", helloWorldHandler) package function we are actually just indirectly calling http.DefaultServerMux.HandleFunc(…).

The Go HTTP server does not have a specific router instead any object which implements the http.Handler interface is passed as a top level function to the Listen() function, when a request comes into the server the ServeHTTP method of this handler is called and it is responsible for performing or delegating any work. To facilitate the handling of multiple routes the HTTP package has a special object called ServerMux, which implements the http.Handler interface.

There are two functions to adding handlers to a ServerMux handler:

func HandlerFunc(pattern string, handler func(ResponseWriter, *Request)) 
func Handle(pattern string, handler Handler) 

The HandleFunc function is a convenience function that creates a handler who's ServeHTTP method calls an ordinary function with the func(ResponseWriter, *Request) signature that you pass as a parameter.

The Handle function requires that you pass two parameters, the pattern that you would like to register the handler and an object that implements the Handler interface:

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

Paths

We already explained how ServeMux is responsible for routing inbound requests to the registered handlers, however the way that the routes are matched can be quite confusing. The ServeMux handler has a very simple routing model it does not support wildcards or regular expressions, with ServeMux you must be explicit about the registered paths.

You can register both fixed rooted paths, such as /images/cat.jpg, or rooted subtrees such as /images/. The trailing slash in the rooted subtree is important as any request that starts with /images/, for example /images/happy_cat.jpg, would be routed to the handler associated with /images/.

If we register a path /images/ to the handler foo, and the user makes a request to our service at /images (note no trailing slash), then ServerMux will forward the request to the /images/ handler, appending a trailing slash.

If we also register the path /images (note no trailing slash) to the handler bar and the user requests /images then this request will be directed to bar; however, /images/ or /images/cat.jpg will be directed to foo:

http.Handle("/images/", newFooHandler())
http.Handle("/images/persian/", newBarHandler())
http.Handle("/images", newBuzzHandler())
/images                  => Buzz
/images/                 => Foo
/images/cat              => Foo
/images/cat.jpg          => Foo
/images/persian/cat.jpg  => Bar

Longer paths will always take precedence over shorter ones so it is possible to have an explicit route that points to a different handler to a catch all route.

We can also specify the hostname, we could register a path such as search.google.com/ and /ServerMux would forward any requests to http://search.google.com and http://www.google.com to their respective handlers.

If you are used to a framework based application development approach such as using Ruby on Rails or ExpressJS you may find this router incredibly simple and it is, remember that we are not using a framework but the standard packages of Go, the intention is always to provide a basis that can be built upon. In very simple cases the ServeMux approach more than good enough and in fact I personally don't use anything else. Everyone's needs are different however and the beauty and simplicity of the standard packages makes it incredibly simple to build your own route as all is needed is an object which implements the Handler interface. A quick trawl through google will surface some very good third party routers but my recommendation for you is to learn the limitations of ServeMux first before deciding to choose a third-party package it will greatly help with your decision process as you will know the problem you are trying to solve.

Convenience handlers

The net/http package implements several methods that create different types of convenience handlers, let's examine these.

FileServer

A FileServer function returns a handler that serves HTTP requests with the contents of the filesystem. This can be used to serve static files such as images or other content that is stored on the file system:

func FileServer(root FileSystem) Handler 

Take a look at the following code:

http.Handle("/images", http.FileServer(http.Dir("./images")))

This allows us to map the contents of the file system path ./images to the server route /images, Dir implements a file system which is restricted to a specific directory tree, the FileServer method uses this to be able to serve the assets.

NotFoundHandler

The NotFoundHandler function returns a simple request handler that replies to each request with a 404 page not found reply:

func NotFoundHandler() Handler 

RedirectHandler

The RedirectHandler function returns a request handler that redirects each request it receives to the given URI using the given status code. The provided code should be in the 3xx range and is usually StatusMovedPermanently, StatusFound, or StatusSeeOther:

func RedirectHandler(url string, code int) Handler 

StripPrefix

The StripPrefix function returns a handler that serves HTTP requests by removing the given prefix from the request URL's path and then invoking h handler. If a path does not exist, then StripPrefix will reply with an HTTP 404 not found error:

func StripPrefix(prefix string, h Handler) Handler 

TimeoutHandler

The TimeoutHandler function returns a Handler interface that runs h with the given time limit. When we investigate common patterns in Chapter 6, Microservice Frameworks, we will see just how useful this can be for avoiding cascading failures in your service:

func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler 

The new handler calls h.ServeHTTP to handle each request, but if a call runs for longer than its time limit, the handler responds with a 503 Service Unavailable response with the given message (msg) in its body.

The last two handlers are especially interesting as they are, in effect, chaining handlers. This is a technique that we will go into more in-depth in a later chapter as it allows you to both practice clean code and also allows you to keep your code DRY.

I may have lifted most of the descriptions for these handlers straight from the Go documentation and you probably have already read these because you have read the documentation right? With Go, the documentation is excellent and writing documentation for your own packages is heavily encouraged, even enforced, if you use the golint command that comes with the standard package then this will report areas of your code which do not conform to the standards. I really recommend spending a little time browsing the standard docs when you are using one of the packages, not only will you learn the correct usage, you may learn that there is a better approach. You will certainly be exposed to good practice and style and you may even be able to keep working on the sad day that Stack Overflow stops working and the entire industry grinds to a halt.

 

Static file handler

Whilst we are mostly going to be dealing with APIs in this book, it is a useful illustration to see how the default router and paths work by adding a secondary endpoint.

As a little exercise, try to modify the code in reading_writing_json_5/reading_writing_json_5.go to add an endpoint /cat, which returns the cat picture specified in the URI. To give you a little hint, you are going to need to use the FileServer function on the net/http package and your URI will look something like http://localhost:8080/cat/cat.jpg.

Did it work the first time or did you forget to add the StripPrefix handler?

Example 1.7 reading_writing_json_6/reading_writing_json_6.go:

21 cathandler := http.FileServer(http.Dir("./images")) 
22 http.Handle("/cat/", http.StripPrefix("/cat/", cathandler)) 

In the preceding example, we are registering a StripPrefix handler with our path /cat/. If we did not do this, then the FileServer handler would be looking for our image in the images/cat directory. It is also worth reminding ourselves about the difference with /cat and /cat/ as paths. If we registered our path as /cat then we would not match /cat/cat.jpg. If we register our path as /cat/, we will match both /cat and /cat/whatever.

Creating handlers

We will now finish off our examples here by showing how you can create a Handler rather than just using HandleFunc. We are going to split the code that performs the request validation for our helloworld endpoint and the code that returns the response out into separate handlers to illustrate how it is possible to chain handlers.

Example 1.8 chapter1/reading_writing_json_7.go:

31 type validationHandler struct { 
32   next http.Handler 
33 } 
34  
35 func newValidationHandler(next http.Handler) http.Handler { 
36   return validationHandler{next: next} 
37 } 

The first thing we need to do when creating our own Handler is to define a struct field that will implement the methods in the Handlers interface. Since in this example, we are going to be chaining handlers together, the first handler, which is our validation handler, needs to have a reference to the next in the chain as it has the responsibility for calling ServeHTTP or returning a response.

For convenience, we have added a function that returns us a new handler; however, we could have just set the next field. This method, however, is better form as it makes our code a little easier to read and when we need to pass complex dependencies to the handler using a function to create, it keeps things a little neater:

37 func (h validationHandler) ServeHTTP(rw http.ResponseWriter, r  
*http.Request) {
38   var request helloWorldRequest
39   decoder := json.NewDecoder(r.Body)
40
41   err := decoder.Decode(&request)
42   if err != nil {
43     http.Error(rw, "Bad request", http.StatusBadRequest)
44     return
45   }
46
47   h.next.ServeHTTP(rw, r)
48 } 

The previous code block illustrates how we would implement the ServeHTTP method. The only interesting thing to note here is the statement that begins at line 44. If an error is returned from decoding the request, we write a 500 error to the response, the handler chain would stop here. Only when no error is returned do we call the next handler in the chain and we do this simply by invoking its ServeHTTP method. To pass the name decoded from the request, we are simply setting a variable:

53 type helloWorldHandler struct{} 
54  
55 func newHelloWorldHandler() http.Handler { 
56   return helloWorldHandler{} 
57 } 
58  
59 func (h helloWorldHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 
60   response := helloWorldResponse{Message: "Hello " + name} 
61  
62   encoder := json.NewEncoder(rw) 
63   encoder.Encode(response) 
64 } 

The helloWorldHandler type that writes the response does not look too different from when we were using a simple function. If you compare this to example 1.6, you will see that all we really have done is remove the request decoding.

Now the first thing I want to mention about this code is that it is purely to illustrate how you can do something, not that you should do something. In this simple case, splitting the request validation and response sending into two handlers adds a lot of needless complexity and it is not really making our code DRYer. The technique, however, is useful. When we examine authentication in a later chapter, you will see this pattern as it allows us to centralize our authentication logic and share it among handlers.