DEV Community

Cover image for Golang: A simple dockerized app to demonstrate the use of Handle, Handler, HandleFunc and HandlerFunc.
Isa
Isa

Posted on

Golang: A simple dockerized app to demonstrate the use of Handle, Handler, HandleFunc and HandlerFunc.

When trying to understand how to build a web application in Go, it took me a while to understand the logic behind Handle, Handler, HandleFunc and HandlerFunc. I had a hard time grasping the logic, so I decided to write an article to make sure I understood everything correctly (which I hadn't until the middle of the writing process) and to help out others who may also be struggling.

To demonstrate the concepts of Handle, Handler, HandleFunc and HandlerFunc, we will write a very simple Go code inside of a container to serve a couple of web pages. But before checking the code, we need to have at least a general understanding of what are handlers in the context of the net/http package in Go.

What are handlers?

On the net/http package, handlers are used to handle HTTP requests. This package has capabilities of initiating an HTTP server and the handlers' role is to handle the HTTP requests by receiving them and then responding back. So if the server receives a request to access the path "/products", a handler will handle that request by responding back with the code corresponding to that path. Technically speaking, a Handler is an interface that has a method called ServeHTTP which has two parameters: a ResponseWriter type (an interface) and a pointer to a Request type (a struct). So any object that has this ServeHTTP method with that signature is a handler.

Let's create our first page using Handle

Now let's jump to our code! We will build a page with the path "/handlepage":

package main

import (
  "fmt"
  "net/http"
)

type HandlePage struct {}

func (h HandlePage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")
  fmt.Fprint(w, "<h1>Welcome to the handle page!</h1>")
}

func main() {
  NewHandlePage := new(HandlePage)

  http.Handle("/handlepage", NewHandlePage)
  http.ListenAndServe(":3000", nil)
}
Enter fullscreen mode Exit fullscreen mode

If you run the code above, and visit the page localhost:3000/handlepage, you'll see the message "Welcome to the handle page!". But what does this code mean? Let's take a look at the main steps.

Understanding the code

First, we import the necessary packages to serve the page. Then we create a struct of type HandlePage. Next, we add the ServeHTTP method to the HandlePage struct (remember that any object that contains a ServeHTTP method with a ResponseWriter type and a pointer to a Request type as its parameters is a handler!). So by adding the ServeHTTP method to the HandlePage struct we have transformed it into a handler.

Let's look at what's inside of the ServeHTTP method.

  w.Header().Set("Content-Type", "text/html")
  fmt.Fprint(w, "<h1>Welcome to the handle page!</h1>")
Enter fullscreen mode Exit fullscreen mode

The goal of the code is to write a simple html text to the page. So we set the content type as text/html and then we use fmt.Fprint to insert the html text we want to write.

Next, we go to the main function. We start by initializing an instance of HandlePage and assign it to newHandlePage. Then we call the Handle method and pass two arguments: a string ("/handlepage") and a handler (newHandlePage). Let's take a closer look to what this handle method does.

This is the definition we find on the official Go documentation:

func Handle(pattern string, handler Handler)
// Handle registers the handler for the given pattern in the DefaultServeMux.
Enter fullscreen mode Exit fullscreen mode

So the Handle function accepts two arguments: a String type and a Handler type. But what is this Handler type? We've mentioned it briefly earlier, but let's take a closer look and understand how we can create a handler from a struct and then use it inside a Handle function. According to the documentation, the Handler type is an interface that responds to an HTTP request:

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}
Enter fullscreen mode Exit fullscreen mode

That Handler interface has the ServeHTTP behavior, which is the same behavior that we added to the HandlePage struct at the beginning of our code, so we can say that the HandlePage type implements the ServeHTTP method. When a type has all the methods defined by an interface, then we can use that type to implement that interface. What does that mean in our case? It means that we can use a HandlePage type as the Handler type in the second argument of the Handle function. So it's like we could rewrite the Handle function like this:

func Handle(pattern string, handler HandlePage)
Enter fullscreen mode Exit fullscreen mode

We can only make that replacement because our HandlePage type implements all the methods found on the Handler type (which is only the ServeHTTP method). A side note here: interfaces can be a little confusing, so if you're not familiar with it, my suggestion is that you take a look at this article from Digital Ocean, and then at this one from Jon Calhoun.

So just to recap, why did we add the ServeHTTP method to our HandlePage struct? So that we can use the HandlePage type as an implementation of the Handler interface.

What about the Handle function? The Handle function's job is to register the handler for the given pattern on the DefaultServeMux (more about this ahead).

Finally, ListenAndServe is executed using port 3000. ListenAndServe starts an HTTP server, and when the second parameter is nil, the DefaultServeMux is used. DefaultServeMux is an instance of ServeMux that is used as the default multiplexer. A multiplexer's job is to redirect a request to a handler.

One interesting thing to notice is that the type of the second parameter of ListenAndServe is a Handler type. Hence, if a multiplexer (DefaultServeMux) is being used in the second parameter, it means it must have a ServeHTTP method to enable it to act as a handler. So the DefaultServeMux object is nothing but a handler that redirects requests made to an URL to the registered handler.

Now let's try to get the big picture of what happens when http.Handle("/handlepage", NewHandlePage) and http.ListenAndServe(":3000", nil) are executed. When http.Handle("/handlepage", NewHandlePage)is executed, the NewHandlePage handler for the "/handlepage" path is registered in the DefaultServeMux. When http.ListenAndServe(":3000", nil) is executed, the HTTP server is started and the DefaultServeMux can then redirect requests according to what was registered on it (so far we only registered the relationship between the "/handlepage" path and the NewHandlePage handler). When the "/handlepage" path is visited, DefaultServeMux redirects the request to the NewHandlePage handler and the ServeHTTP method is called, executing the code inside of it. So in our case, when we visit http://localhost:3000/handlepage, the code inside the ServeHTTP method,

w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, "<h1>Welcome to the handle page!</h1>")
Enter fullscreen mode Exit fullscreen mode

is executed and we see the "Welcome to the handle page!" written on the page.

Ok, so now we have a general understanding of what handle and handlers do and how they relate to each other: the first is a function that uses the second, an interface (or an object that implements that interface), as one of its parameters. So since a Handle uses a Handler as one of its parameters, can we infer that HandleFunc uses a HandlerFunc as one of its parameters too? Well, not necessarily. So what is the difference between a HandleFunc and a Handle and between a Handler and a HandlerFunc?

Let's continue with our example to have a better understanding and make a brief recap on what we've done so far to create our single page on "/handlepage" using only Handle and Handler:
1- Created a HandlePage struct;
2- Added the ServeHTTP method to the HandlePage struct;
3- Created an instance of HandlePage and name it NewHandlePage;
4- Executed http.Handle having NewHandlePage as its handler.

Now let's assume we want to create several pages. If we follow the logic above, we would need to create a struct for every page and then add the ServeHTTP method to each. HandleFunc and HandlerFunc can help us simplify this flow. But how? Let's see!

The second page: using HandleFunc

We will now create a different page, with a path "/handlefuncpage". Let's add to the previous code all the steps necessary to create and serve this new page, but now using HandleFunc instead of Handle. Here's how our new code looks like:

package main

import (
  "fmt"
  "net/http"
)

type HandlePage struct {}

func (h HandlePage) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")
  fmt.Fprint(w, "<h1>Welcome to the handle page!</h1>")
}

func HandleFuncPage(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")
  fmt.Fprint(w, "<h1>Welcome to the handlefunc page!</h1>")
}

func main() {
  NewHandlePage := new(HandlePage)

  http.Handle("/handlepage", NewHandlePage)
  http.HandleFunc("/handlefuncpage", HandleFuncPage)
  http.ListenAndServe(":3000", nil)
}
Enter fullscreen mode Exit fullscreen mode

So which steps did we have to take to create the new page on "/handlefuncpage" path?
1- Created a HandleFuncPage function;
2- Executed http.HandleFunc having the HandleFuncPage function as its second parameter.

When comparing the steps made to create the page on "/handlepage" and the ones made to create the new page on "/handlefuncpage", we will see that we can replace the first's three steps by the second's first step: instead of i) creating a struct, ii) adding the ServeHTTP method to it and iii) creating an instance of the struct, all we had to do was to create a function having a ResponseWriter type and a pointer of an Request type as its parameters. But how is this simplification possible? Let's compare HandleFunc to Handle and understand the differences.

func Handle(pattern string, handler Handler)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
Enter fullscreen mode Exit fullscreen mode

When comparing Handle and HandleFunc, we see that the second parameter, the handler, is different: whereas in Handle it is a Handler type, in HandleFunc it is a function with a ResponseWriter type and a pointer of Request type as parameters (yes, the same signature of the ServeHTTP method). So this means that to use HandleFunc, we need to have a function with the proper parameters to make it work like a handler. Once this is done, HandleFunc, when executed, will convert this function into a handler and register it on the DefaultServeMux.

Right, but where does HandlerFunc enter after all? It's nowhere to be seen in our code! To unfold this mystery, we first need to understand what a HandlerFunc is.

What is HandlerFunc and where is it?

A HandlerFunc is a function type that can convert any function with the right signature into a handler. But what is the right signature the function must have? Yes, the same signature of the ServeHTTP method: a ResponseWriter type and a pointer to a Request type. So when we have a function with that signature, we can use HandlerFunc to convert this function into a handler, allowing us to use it as the second parameter of a Handle function, for example.

One thing to notice is that HandlerFunc converts the function to a HandlerFunc type, and not a Handler type. So how can we use this HandlerFunc type as a handler? Because a HandlerFunc type implements the ServeHTTP method, the same method that the Handler type implements. Remember the interface logic we mentioned earlier: when a type has all the methods defined by an interface, then we can use that type to implement that interface.

Let's check the ServeHTTP method added to a HandlerFunc type and see if there's anything interesting:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  f(w, r)

Enter fullscreen mode Exit fullscreen mode

When we convert a function to a handler using HandlerFunc, a ServeHTTP method is added to it. Calling ServeHTTP on this new handler will simply call the underlying function (f(w, r)). Think of when we added the ServeHTTP method to the HandlePage struct: calling ServeHTTP on this struct simply executes the code we wrote inside of it. The ServeHTTP method inside of a HandlerFunc type was built to have the same effect: execute the underlying function and hence the code inside of it.

Being able to convert any function with the appropriate signature into a handler is quite handy 😃, but going back to our mystery, we don't see this HandlerFunc being used anywhere. When we call HandleFunc in our code using our HandleFuncPage function as its second parameter, everything works as expected, as if the function is already a handler. How can this be?

To understand what is happening behind the hood, we must check the source code of HandleFunc:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  if handler == nil {
    panic("http: nil handler")
  }
  mux.Handle(pattern, HandlerFunc(handler))
}


func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  DefaultServeMux.HandleFunc(pattern, handler)
}
Enter fullscreen mode Exit fullscreen mode

Ah! Now we can see HandlerFunc in action! Notice in the last line of the first block that HandlerFunc is being used to convert the function called handler into an actual HandlerFunc type (HandlerFunc(handler)). It is very important to understand that whenever you see something like var nf http.Handler = http.HandlerFunc(notFound), it means that you are converting a function into a handler type (in this case, we are converting the function notFound into a handler). Some people who are not familiar with how HandlerFunc works might think that it is a function call having notFound as an argument and that it might return a different thing.

Going back to our code, that's why when we called http.HandleFunc("/handlefuncpage", HandleFuncPage) everything worked: Handlefunc converted the HandleFuncPage function into a handler under the hood.

Right, so we now understand what a HandlerFunc does and how it is used by HandleFunc to convert functions into handlers. In our case, we didn't have to use HandlerFunc explicitly, but there might be cases where we would want to convert our functions into handlers and then use them elsewhere. For example, if for some reason we wanted to keep using Handle instead of HandleFunc, we could have converted our function HandleFuncPage using HandlerFunc and then used Handler to register it on DefaultServeMux, like this:

//Convert HandleFuncPage function into a handler:
hfp := http.HandlerFunc(HandleFuncPage)
//Use the converted function as a handler using Handle:
http.handle("/handlefuncpage", hfp)
Enter fullscreen mode Exit fullscreen mode

Remember that the Handle function doesn't convert functions to handlers, so that's why we need to convert the function to a handler and then pass it inside Handle. In fact, HandleFunc was a shortcut created to avoid having to explicitly convert functions into handlers.

The end

And that's it! I hope you enjoyed this article and that your understanding of all those handy stuff is now a little bit better.

You can find the code here. You can open it in a devcontainer if you are using VSCode.

Thanks!

Latest comments (0)