3 minutes
New Pattern property in http.Request
In Go 1.23, a new property Pattern
in net/http.Request
was added. It contains the route pattern used to handle the request. This improvement may have gone unnoticed by many, but it can simplify request processing and improve performance in cases where we need to obtain the matched routing pattern for the current request.
Let’s see how we can use this feature. Suppose we have an HTTP server and want to collect metrics using Prometheus, such as the total number of requests:
requestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "total request count",
}, []string{"path", "method", "code"})
Obviously, for the path
label, we should store the path template, not the specific value, for example, /test/{id}
instead of /test/123
. Otherwise, we risk a cardinality explosion, which can significantly increase the amount of stored data. You can read how to choose labels properly in “Prometheus best practices”.
Before v1.23
Previously, to extract the route pattern, we had to pass the multiplexer to a middleware function:
func metricsMiddleware(next http.Handler, mux *http.ServeMux) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, pattern := mux.Handler(r)
...
requestsTotal.WithLabelValues(pattern, method, statusCode).Inc()
}
}
In this approach, we matched the request to the route a second time, and that negatively affected performance.
Since 1.23
Starting with Go 1.23, we can get the route pattern directly from the request, without passing the multiplexer to the middleware:
func middleware(f http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pattern := r.Pattern
...
requestsTotal.WithLabelValues(pattern, method, statusCode).Inc()
})
}
This significantly simplifies the middleware, making the code cleaner and more readable. We are no longer dependent on ServeMux
and no longer need to re-match the request.
Limitations
It is important to remember that Request.Pattern
is only available after the request has been matched to a route. For example:
func main() {
mux := http.NewServeMux()
mux.Handle("/test/{id}", routeMiddleware(handler()))
srv := http.Server{
Addr: ":8080",
Handler: globalMiddleware(mux),
}
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server error: %s", err)
}
}
func routeMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("ROUTE pattern: %s", r.Pattern)
h.ServeHTTP(w, r)
})
}
func globalMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("GLOBAL pattern: %s", r.Pattern)
h.ServeHTTP(w, r)
log.Printf("GLOBAL pattern after: %s", r.Pattern)
})
}
If we send a request GET http://localhost:8080/test/123
, in logs we will see:
2024/11/10 13:20:37 GLOBAL pattern:
2024/11/10 13:20:37 ROUTE pattern: /test/{id}
2024/11/10 13:20:37 GLOBAL pattern after: /test/{id}
This is the expected behavior. The multiplexer implements the http.Handler
interface, and inside the ServeHTTP
method, it searches for the handler that matches the request. Starting from Go 1.23, it fills the Pattern
property of the request with the matching route pattern:
// net/http/server.go
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
h.ServeHTTP(w, r)
}
Thus, the r.Pattern
is filled only after the ServeHTTP
method of the multiplexer has been called, and the logs confirm this behavior.
Conclusion
In the last two Go releases, the net/http
package has received significant improvements that reduced the gap with third-party packages, like gorilla/mux
. Now, net/http.ServeMux
supports HTTP methods and wildcards in routing patterns, bringing its functionality closer to what third-party packages previously provided.
Additionally, the introduction of the net/http.Request.Pattern
property in Go 1.23 has simplified middleware development by allowing us to access the route pattern without needing to re-match the request, improving performance and reducing code complexity.
With each of these improvements, there are fewer reasons to choose third-party routers. While the choice between the standard package and third-party routers used to be more obvious, today I would recommend starting with the standard net/http
package.