Go: Handling http Requests for Beginners

Go's standard library provides net/http package for listening to and responding to HTTP requests. If you are writing a web application, you will most certainly be handling HTTP requests, and this functionality is built straight into the standard library of Go. Writing a basic HTTP server is easy in Go, as long as you understand a couple of underlying concepts.

The best way to learn something is to see it work, and an even better way is to make it work. Let us explore this package by building a simple server in Go which responds to these 3 endpoints as follows

Mux And Handlers

When a http request hits our go server, it will be taken care of in 2 parts:

  • mux (stands for multiplexer ) is a request router which decides part of logic to execute on a per request basis
  • handler = the logic which will actually serve the request

Go mux handlers.jpg

Creating a Mux

This is how a new mux is created (ref)

mux := http.NewServeMux()

Creating our First Handler - RedirectHandler

the first handler we will create is one for redirect because that is the easiest of the 3 (ref)

rh := http.RedirectHandler("https://in.linkedin.com/in/saurabh-sikchi-50b807b8", http.StatusPermanentRedirect)

Now let's glue these 2 parts together (ref)

mux.Handle("/linkedin", rh)

This Handle method on mux tells the mux to use our redirect handler for a request to "/linkedin"

The main func now looks like:

func main() {
    mux := http.NewServeMux()

    rh := http.RedirectHandler("https://in.linkedin.com/in/saurabh-sikchi-50b807b8", http.StatusPermanentRedirect)

    mux.Handle("/linkedin", rh)

    fmt.Println("Listening and serving on port 3000:")
    http.ListenAndServe(":3000", mux)
}

Execute: go run main.go

And navigate to localhost:3000/linkedin and you should be redirected to my linkedin profile.

If it worked, good job!

Onwards...

StatusHandler

Notice that the type of rh is Handler (in vscode you can hover over the variable to find it's type).

Let's check out exactly what the Handler type is from the docs (I really like this about Go) here

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

It's an interface with only 1 method - ServeHTTP.

We used a redirect handler previously. Let's write an handler of our own. We can do this by writing a method on our type.

type statusHandler struct {
}

func (s statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("online"))
}

Here we made our statusHandler type a Handler interface by writing the serveHTTP method on it. This statusHandler is now ready to serve HTTP requests. The handler will respond with "online" to all requests it receives.

We need to route requests to /status to this handler. Like previously, we do:

mux.Handle("/status", statusHandler)

So our main.go now looks like:

type statusHandler struct {
}

func main() {
    mux := http.NewServeMux()

    rh := http.RedirectHandler("https://in.linkedin.com/in/saurabh-sikchi-50b807b8", http.StatusPermanentRedirect)
    mux.Handle("/linkedin", rh)

    sh := statusHandler{} // create new statusHandler struct
    mux.Handle("/status", sh)

    fmt.Println("Listening and serving on port 3000:")
    http.ListenAndServe(":3000", mux)
}

func (s statusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("online"))
}

After running the server go to localhost:3000/status If you see 'online' give yourself another pat on the back.

Refactor

The /status endpoint works but feels a bit verbose. The statusHandler struct is empty and we are not using it in our ServeHTTP method at all. For complex scenarios, we might need the fields on a struct to generate our response but our aim here is far too simple for that. Is there an easier way? Can we define our handler function and do away with the empty struct? Yes we can. http package has a type called HandlerFunc which is

type HandlerFunc func(ResponseWriter, *Request)

which means we can convert any function with signature func(ResponseWriter, *Request) to this type. This type defines ServeHTTP method which calls the previously converted function. Neat!

If that was confusing, let's see it in action.

func statusHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("online"))
}

func main() {
    mux := http.NewServeMux()

    rh := http.RedirectHandler("https://in.linkedin.com/in/saurabh-sikchi-50b807b8", http.StatusPermanentRedirect)
    mux.Handle("/linkedin", rh)

    sh := http.HandlerFunc(statusHandler) // sh is of type HandlerFunc
    mux.Handle("/status", sh) // HandlerFunc implements Handler interface because it has method ServeHTTP

    fmt.Println("Listening and serving on port 3000:")
    http.ListenAndServe(":3000", mux)
}

Notice that our sh is of type HandlerFunc.

That felt much less verbose and wasteful than the previous example. This pattern is so common that there is a shortcut for this in the mux itself (ref)

mux.HandleFunc("/status", statusHandler)

Notice here we use HandleFunc instead of Handle. The second argument is our function.

TimestampHandler

And now to the final endpoint /timestamp Can I leave it as an exercise? Here's how the final code looks:

func statusHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("online"))
}

func timeHandler(w http.ResponseWriter, r *http.Request) {
    t := time.Now().String()
    w.Write([]byte(t))
}

func main() {
    mux := http.NewServeMux()

    rh := http.RedirectHandler("https://in.linkedin.com/in/saurabh-sikchi-50b807b8", http.StatusPermanentRedirect)
    mux.Handle("/linkedin", rh)

    mux.HandleFunc("/status", statusHandler)

    mux.HandleFunc("/timestamp", timeHandler)

    fmt.Println("Listening and serving on port 3000:")
    http.ListenAndServe(":3000", mux)
}

if you followed along, congrats!

PS: DefaultServeMux

DefaultServeMux is a mux instantiated by default in the http package. If the second argument to ListenAndServe(port, nil) is nil like so, this default mux will be used.

We register routes to default mux using

http.Handle
http.HanndleFunc

which do the same thing as

mux.Handle
mux.HandleFunc

Warning: Avoid using DefaultServeMux because it poses a security risk. Since DefaultServeMux is stored in a global variable, any package can access it and register a routes. If a third-party package is compromised, DefaultServeMux can expose malicious handler to the internet. Instead we should use our own locally-scoped ServeMux, like we have been using so far.

I hope the http package's mux and handler are clear to you.

Thanks for reading.