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)
}
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>")
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.
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)
}
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)
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>")
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)
}
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))
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)
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)
}
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)
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!
Top comments (0)