Table of contents
Before Go 1.22, if you wanted advanced routing features like method routing, path parameters, etc., you wrote a lot of boilerplate code or reached out to an external library like Gorilla Mux or another web framework.
Starting from Go 1.22, the standard library's net/http
package introduced many advanced routing features; in this post, we'll take a look at a few of them.
To make use of these features, make sure that the version in your
go.mod
file is 1.22.X
Path Parameters
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/product/{id}", func(w http.ResponseWriter, r *http.Request) {
productId := r.PathValue("id")
fmt.Fprintf(w, "displaying properties for product %s", productId)
})
fmt.Println("Server is listening on port 8000")
http.ListenAndServe(":8000", mux)
}
In the above example, we define a route with a path parameter, "/product/{id}"
We have to wrap the parameter around {}
. To get the parameter value in our handler, we can use the PathValue
method in the request object.
Method Routing
We can also have separate handlers for each HTTP method.
mux := http.NewServeMux()
mux.HandleFunc("GET /product/{id}", func(w http.ResponseWriter, r *http.Request) {
productId := r.PathValue("id")
fmt.Fprintf(w, "displaying properties for product %s", productId)
})
mux.HandleFunc("POST /product/{id}", func(w http.ResponseWriter, r *http.Request) {
productId := r.PathValue("id")
fmt.Fprintf(w, "creating product with id %s", productId)
})
Now, our route pattern has been expanded to include an HTTP verb
"[METHOD ][PATH]"
There should exactly a single space between the method and the path.
Sub-routing
Sub-routing allows us to group common routes. For example, you can group "customer" and "admin" routes under different prefixes, /customer
and /admin
respectively.
We can also use this to version our API, eg /v1
, /v2
package main
import (
"fmt"
"net/http"
)
func main() {
customerRouter := http.NewServeMux() // 3
adminRouter := http.NewServeMux() // 2
// customer handlers
customerRouter.HandleFunc("/sign-in", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Customer Sign in")
})
// admin handlers
adminRouter.HandleFunc("/sign-in", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Admin Sign in")
})
router := http.NewServeMux() // 1 - root router
router.Handle("/customer/", http.StripPrefix("/customer", customerRouter))
router.Handle("/admin/", http.StripPrefix("/admin", adminRouter))
fmt.Println("Server is running on port 8000")
http.ListenAndServe(":8000", router)
}
If you have worked with a framework like Express JS
, this should look familiar.
We create three routers with http.NewServeMux()
.
Notice that we first strip out the path group prefix in our root router. This is necessary, or else none of the routes in our sub-routers will be matched against.
Also, our route prefixes must end in a "/"
Middlewares
Middleware is code that independently acts on a request before or after your normal application handlers.
In Go, the building block of HTTP handling is the Handler
interface.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
http.ServeMux
and http.Handler
(the second parameter to http.HandleFunc
) implement this interface.
With this in mind, let's create a Logger
middleware.
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Println(time.Since(start), r.Method, r.URL.Path)
})
}
Middlewares generally have the following signature:
func Middleware(next http.Handler) http.Handler
They take in an HTTP handler and return an HTTP handler.
In the Logger
middleware, on line 4, we call the next handler's ServeHTTP
method.
For the Express JS devs, this is akin to calling the next
function in an express middleware.
Now, how do we use this middleware?
http.ListenAndServe(":8000", middleware.Logger(router)) // 1
// OR
router.Handle("/admin/", http.StripPrefix("/admin", middleware.Logger(adminRouter))) // 2
// OR
router.Handle("/customer/", http.StripPrefix("/customer", middleware.Logger(customerRouter))) // 3
With this, we can enable logging for all routes or a sub-route.
You can access all the code examples here