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
/status -> which will return 'online'
/timestamp -> which will return the current timestamp
/linkedin -> which will redirect to my linkedin profile (in.linkedin.com/in/saurabh-sikchi-50b807b8)
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
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.