Can You Handle This
Welcome to part 2 of my ongoing series, “A Gentle Intro to Golang Web Development.” I hope you enjoy it!
Last time, we took a look at muxes. We know that they act as routers that route requests based on matching endpoint patterns. Now it’s time to look at how the requests get handled.
Handlers, what are they?
Handlers are objects that “handle” HTTP requests. All handlers implement the Handler
interface shown below:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
type Request struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error)
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{}
Response *Response
}
So for an object to become a handler, it needs a ServeHTTP()
function. Responsewriter
is an interface built on io.writer
that lets you write HTTP responses. *Request
is a pointer to the Request
struct from which you can read HTTP request data by accessing Request.Body
.
If you are familiar with the concept of MVC, handlers are conceptually similar to controllers.
Creating your custom Handler
It’s easier to understand the concept when I show you how it works in an example code.
package main
import (
"log"
"net/http"
)
type homeHandler struct {
}
func (hh homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome!"))
}
func main() {
mux := http.NewServeMux()
hh := homeHandler{}
mux.Handle("/home", hh)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Let’s take this apart and tackle it bit by bit.
type homeHandler struct {
}
func (hh homeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome!"))
}
-
homeHandler
is a struct that implements theHandler
interface, because it has a struct methodServeHTTP()
defined. -
Our
ServeHTTP()
method just writes a string"Welcome!"
to the HTTP response.
func main() {
mux := http.NewServeMux()
hh := homeHandler{}
mux.Handle("/home", hh)
log.Fatal(http.ListenAndServe(":8080", mux))
}
-
We define our custom mux in the main function by calling the
http.NewServeMux()
. -
Then we create an instance of our
homeHandler
and name ithh
. -
We now register a route to our mux by calling
mux.Handle()
. This registration associates the route/home
with the handlerhh
, so that whatever request that is sent to/home
will be handled byhh
. -
Finally, the last line hosts the web server at port 8080, using our custom mux as a handler to handle incoming requests (yes, a mux is a special type of
Handler
that returns aHandler
instead of writing tohttp.ResponseWriter
).
A quicker way to create Handlers
The method describe above is good and all, but it is somewhat verbose. Making a custom struct and defining ServeHTTP()
for it, then creating an instance of the handler to handle one route seems like overkill.
Fortunately, there is an easier way to do it. We can just use functions as handlers instead of creating structs with ServeHTTP()
methods.
package main
import (
"log"
"net/http"
)
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome!"))
}
func main() {
mux := http.NewServeMux()
hh := http.HandlerFunc(homeHandler)
mux.Handle("/home", hh)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Keen viewers may have picked up something odd. homeHandler()
doesn’t have ServeHTTP()
defined anywhere. So this must not be a handler, right?
Yesn’t.
You are right, homeHandler
isn’t technically a handler. However, it is something else; a HandlerFunc
.
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
The definition of the HandlerFunc
type might be a bit weird at first. It just means that any function that accepts ResponseWriter
and *Request
is a HandlerFunc
.
A HandlerFunc
has its own ServeHTTP()
method, so it implements the Handler
interface. The method calls the function that is defined by the HandlerFunc
. In our example, we are just writing "Welcome!"
to the HTTP response.
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome!"))
}
func main() {
mux := http.NewServeMux()
hh := http.HandlerFunc(homeHandler)
mux.Handle("/home", hh)
log.Fatal(http.ListenAndServe(":8080", mux))
}
Going back to our example, look at where we define hh
. Because homeHandler
isn’t a handler on its own, we need to cast it to a HandlerFunc
type, which does implement the Handler
interface. We can then pass hh
to mux.Handle()
.
There’s an even faster way to do this:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/home", homeHandler)
log.Fatal(http.ListenAndServe(":8080", mux))
}
HandleFunc
is mux.Handle
for functions. It takes an HandlerFunc
as an input, so you don’t even have to cast your function to the HandlerFunc
type.
Conclusion
I hope you enjoyed reading this post! This series aims to help new Go web developers familiarize themselves with web development concepts. I want to share my knowledge by writing easy-to-understand tutorials, while also explaining the “why” along with the “how”.
Storytime: I started my Go journey by just ripping off tutorials. However, this made me uncomfortable because I still didn’t know what the heck I was doing. I got lost immediately if a tutorial code was written in a different style using a different approach. That’s when I decided to look into the code and understand why things are the way they are. And I thought to myself, “There are probably other people in similar shoes as I am. I should write beginner-focused tutorials to help spread the knowledge.” My posts may be but several grains of sand in a beach of tutorials. But I believe that after reading my blog posts, someone out there might have an “aha” moment.
Be on the lookout for the next article coming next week. See you then!