Introduction

Fun fact: The HTTP server implementation in Go is under 4000 lines of code (including comments).

If you are a Go developer in 2019, you are in luck as you no longer need to read tons of cryptic documentation. Instead, you can focus on net/http package in the standard library and understand (almost) everything about HTTP at the server side.

An impressive feature of Go is the inbuilt HTTP server. It is production grade, asynchronous and packed with everything you need to spin up an HTTP server. This is a huge contrast in comparison to many other programming languages, where the server specs and implementation are external to the language.

Covering complete net/http package would be too much to cover in a single blog post. In this blog post, we will focus on understanding the mechanics of handling incoming HTTP requests.

Where to start

In my opinion, myriads of functionalities for the HTTP request handling in Go can be summarized by the http.Handler interface. The construct of the Handler interface is pretty simple.

// A Handler responds to an HTTP request.
// ....
type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

The core responsibility of the Handler is to write response headers and body. In order to process an incoming request, all you need is a type that implements this interface. The standard library provides some default types that implement the Handler interface and can be used out of the box. For example:

What is ServeMux

It is a router, that holds the mapping of URL path + HTTP method to the corresponding handler. It simply routes the incoming request to the correct handler. The standard library defines it as:

ServeMux is an HTTP request multiplexer. It 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.

It has a simple construct

 1 type ServeMux struct {
 2     mu    sync.RWMutex
 3     m     map[string]muxEntry
 4     hosts bool // whether any patterns contain hostnames
 5 }

 6 type muxEntry struct {
 7     h       Handler
 8     pattern string
 9 }

As evident from line 3, ServeMux holds a map of URL patterns and corresponding request handlers. This provides an easy way to collect all your requests and handlers at one place. The corresponding handler will be executed based on the incoming request pattern.

Now there are two choices that you should make.

  • Do you want to implement Handler for each request? This could be a viable option when you really want to take control of how you want to handle the incoming request. Just remember that you have to implement ServeHTTP for each handler that you register. If you decide to do this, you should implement your handler and register it using ServeMux.Handler.
  • Do you have a simple case which can be served using HandlerFunc? This typically would cover 99% of use cases. All you need to do is to create a function with signature (http.ResponseWriter, *http.Request) containing business logic and register it using ServeMux.HandleFunc

In the next few sections, let put what we have learned so far into practice.

Example: ServeMux.Handle

// myHandler implements ServeHTTP, so it is valid
type myHandler struct{}
func (m *myHandler) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
    fmt.Fprintf(w, "serving via mux-Handle")
}

func main() {
    // handler
    h := new(myHandler)

    // create mux and register handler
    mux := http.NewServeMux()
    mux.Handle("/", h)

    // register mux with server and listen for requests
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Example : ServeMux.HandleFunc

func main() {
    // create a mux to hold url and handlers
    mux := http.NewServeMux()
    log.Println("created mux")

    // register handlers using HandleFunc
    mux.HandleFunc("/", root)
    mux.HandleFunc("/foo", foo)
    mux.HandleFunc("/bar", bar)
    log.Println("registered handlers")

    // register mux with server and listen for requests
    log.Println("starting server")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// HandlerFunc to be registered
func root(w http.ResponseWriter, _ *http.Request) {
    fmt.Fprintf(w, "this is root")
}

func foo(w http.ResponseWriter, _ *http.Request) {
    fmt.Fprintf(w, "this is foo")
}

func bar(w http.ResponseWriter, _ *http.Request) {
    fmt.Fprintf(w, "this is bar")
}

What are http.Handle() and http.HandleFunc() ?

A lot of examples on internet use http.Handle and http.HandleFunc. How does that fit into the whole equation with ServeMux and http.Handler interface? For example you would see examples like below

1 func main() {
2    http.HandleFunc("/", myHandleFunc)
3    log.Fatal(http.ListenAndServe(":8080", nil))
4 }

5 func myHandleFunc(w http.ResponseWriter, _ *http.Request) {
6    fmt.Fprintf(w, "this is serving from http-HandleFunc")
7 }

Note that in this case:

  • We do not create a ServeMux. We directly use http.HandleFunc
  • We don’t pass any mux to http.ListenAndServe, instead we use nil

The reason is pretty simple, net/http provides a default implementation for ServeMux as DefaultServeMux. The http.Handle and http.HandleFunc, internally use the DefaultServeMux to provide routing capabilities.

You can check the implementations of these here and here.

My suggestion would be to always use a ServeMux if possible. That way you have more control on your routes and you are not dependent on the default mux provided by the standard library.

Conclusion

The standard library provides all you need to set up an HTTP Server. However, at times it could be too verbose to use inbuilt ServeMux. If you ever come across such a situation, you should have a look into the available frameworks within the community. Some of the popular router frameworks are:

Go provides a lot of powerful tools as part of the standard library that is easy to reason with. In this blog, we barely scratched the surface of what it has to offer. Watch this space for more content on Golang.

Note : All the code in this repository can be found here

References