Advanced Routing with Go 1.22

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