This article is a translation of 自作HTTPルーターから新しいServeMuxへ.
Overview
Up until now, I have been using a homemade HTTP router called goblin in my application, but since the ServeMux functionality has been expanded in Go1.22, I have started using ServeMux. It became so.
In this article, we will summarize the functions and performance of ServeMux added in Go1.22, and think about selecting a Go HTTP router in the future.
ServeMux features added in Go1.22
I was looking into the new features of ServeMux when Go1.22rc was released, but I thought I'd look into it in more detail.
cf. ServeMux specifications changed in Go1.22rc
We will summarize the new features of ServeMux based on the following reference information.
- Release notes
-
Go 1.22 Release Notes - Enhanced routing patterns
- It states that the ServeMux pattern has been extended to accept methods and wildcards (dynamic path parameters, e.g. /items/{id})
-
Go 1.22 Release Notes - Enhanced routing patterns
- pkg.go.dev
- go.dev
-
go.dev - Routing Enhancements for Go 1.22
- Contains specifications for new features of ServeMux
-
go.dev - Routing Enhancements for Go 1.22
- Discussion
-
net/http: add methods and path variables to ServeMux patterns
- Discussion about ServeMux enhancements
-
net/http: add methods and path variables to ServeMux patterns
- proposal
-
net/http: enhanced ServeMux routing
- Proposal for ServeMux enhancements
-
net/http: enhanced ServeMux routing
Defining routing by HTTP method
By specifying a path that includes an HTTP method, it is now possible to define routing using an HTTP method. When using ServeMux, it is no longer necessary to write conditional branching for HTTP methods in the handler.
http.HandleFunc("GET /items", handleItems)
There is a constant for the HTTP method (ex. http.MethodGet), so I thought it would be a good idea to use it, but this is probably to keep the signature of the existing method in consideration for backward compatibility. I think it might look something like this.
Or maybe it's just a matter of adapting it to the HTTP request format.
Defining wildcard routing
By specifying a path using wildcards ({pathVal}
), it is now possible to define wildcard routing.
// GET matches /items/1, /items/foo, etc.
http.HandleFunc("GET /items/{id}", handleItems)
The path pattern can be specified in the following format. Even with Go1.22 or earlier, you can also specify the host name. (I found out relatively recently...)
[METHOD][HOST]/[PATH]
The value that matches the wildcard can be obtained by using http.Request's PathValue method.
func handleItems(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// id will contain the value that matches the wildcard
}
It is also possible to define routing using multiple wildcards.
// GET matches /items/1, /items/1/2, /itema/1/2/3, etc.
http.HandleFunc("GET /items/{id...}", handleItems)
If you want to match the exact path, use {$}
. Please keep this in mind when you want to define a route (/
).
http.HandleFunc("GET /{$}", handleIndex)
As an aside, third-party routers sometimes use *
as a wildcard in the pattern. Personally, I want to call it a path parameter because I'm drawn to that image...(It's not something I care about, though.)
Things to note with the new ServeMux
Defining patterns using HTTP methods
In many third-party HTTP routers, an HTTP method is often a method.
// ex.
mux.Get("/items", handleItems)
With ServeMux, there may be a small problem that it is difficult to detect typos because HTTP methods are included in patterns. It would be nice if I could check it with a linter, but I don't know what to do. This might be a good topic for creating your own static analysis tool. I feel like something will be done soon.
Priority rules
When defining routing using wildcards, pay attention to priorities.
cf.
With ServeMux, you can also define the following routing patterns:
// If both match, the former takes precedence
/items/new
/items/{id}
// If both match, the latter takes precedence
/items/{id...}
/items/{id}/category/{name}
In the case of such a pattern, you need to be careful about which pattern the expected request matches.
Some HTTP routers in the world do not allow duplication in this pattern, while others do, and ServeMux falls under the latter.
On the other hand, in the following cases where a conflict occurs, ServeMux detects the conflict and generates a panic.
// There are cases where both match, and cases where only one matches.
/items/{id}
/{category}/items
// if both match
/items/{id}
/items/{name}
In the case of a conflict, I think it is less troublesome than a duplicate, since the error can be detected at an early stage (testing, server startup, etc.).
By the way, in the self-made goblin, the pattern registered first is given priority. (It's actually quite complicated because it's not designed properly...)
For more information about ServeMux's conflict detection, see the following article.
cf. rhumie.github.io - ServeMux conflict detection and performance
Even third-party HTTP routers take conflict detection into consideration; for example, httprouter either matches one pattern or it doesn't. It is designed to become.
Only explicit matches: With other routers, like http.ServeMux, a requested URL path could match multiple patterns. Therefore they have some awkward pattern priority rules, like longest match or first registered, first matched. By design of this router, a request can only match exactly one or no route. As a result, there are also no unintended matches, which makes it great for SEO and improves the user experience.
cf. https://github.com/julienschmidt/httprouter?tab=readme-ov-file#features
The priority specification for pattern matching in an HTTP router is an important point that affects the quality of an HTTP router, so it is reassuring that this is properly designed.
Backward compatibility
There are some cases where backward compatibility is not maintained between Go1.22 and Go1.21.
cf. pkg.go.dev - Compatibility
In that case, you can return to Go1.21 behavior by setting httpmuxgo121=1
in the GODEBUG environment variable.
Internal implementation code reading
Routing pattern registration process
Below is the definition of the ServeMux structure.
// see: https://cs.opensource.google/go/go/+/master:src/net/http/server.go;l=2439;drc=960fa9bf66139e535d89934f56ae20a0e679e203;bpv=1;bpt=1
type ServeMux struct {
mu sync.RWMutex
tree routing Node
index routingIndex
patterns []*pattern // TODO(jba): remove if possible
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
A tree structure is generated so that the paths become nodes, which seems to be a common data structure for HTTP routers.
The index and patterns are data used to detect pattern conflicts.
Routing matching process
If you are not familiar with reading, it may be a good idea to read the code of an HTTP server as a prerequisite.
cf. Golang HTTP server code reading
Comparison of ServeMux and other HTTP routers
I compared ServeMux with other HTTP routers using a homemade benchmark marker.
The self-made bench marker uses a previously implemented one. Please refer to the link below for details.
Benchmark results are published at go-router-benchmark.
The above bench marker measures only the matching of pass patterns, and registration of pass patterns is not included in the measurement.
Benchmark results
Although there are some differences when compared to Echo, GIN, and httprouter, which seem to have good performance based on benchmark results, overall it seems to have above-average performance.
What was remarkable was that performance deteriorated as the number of path parameters increased.
I feel that higher-level HTTP routers are designed to suppress this deterioration.
I don't think there are many cases where you use multiple path parameters, so I don't think you need to worry too much about this point.
Comparison between goblin and ServeMux
Goblin wins for static routing test cases, but ServeMux wins for dynamic routing.
Goblin seems to be working hard to suppress the performance deterioration due to the increase in the number of path parameters.
Again, I don't think there are many cases where multiple path parameters are used, so I don't think the difference in performance is of much practical use.
About HTTP router performance
ServeMux (the implementer of it) seems to think about the performance of HTTP routers as follows.
Implementation is out of scope for this discussion/proposal. I think we'd be happy to have a more complex implementation if it could be demonstrated that the current one actually affects latency or CPU usage. For typical servers, that usually access some storage backend over the network, I'd guess the matching time is negligible. Happy to be proven wrong.
cf. Quoted from https://github.com/golang/go/discussions/60227#discussioncomment-5932822
Other related comments are also included.
cf. https://github.com/golang/go/issues/61410#issuecomment-1867191476
cf. https://github.com/golang/go/issues/61410#issuecomment-1867485864
cf. https://github.com/golang/go/issues/61410#issuecomment-1868615273
Third-party HTTP routers that feature performance as one of their characteristics often employ complex tree-structured algorithms (e.g. Radix Tree, which is optimized for memory efficiency).
When implementing ServeMux, the idea seems to be not to use complex data structures or algorithms, as they won't have a big impact on latency or CPU usage unless you use extremely bad data structures.
This is not a disproval, but gorilla/mux has comparatively poor benchmark results among popular (many stars) third-party HTTP routers. , used by many users.
Some comments mention the performance of not only path pattern matching but also path pattern registration.
Registration time is potentially more of an issue. With the precedence rules described here, checking a new pattern for conflicts seems to require looking at all existing patterns in the worst case. (Algorithm lovers, you are hereby nerd-sniped.) That means registering n patterns takes O(n2) time in the worst case. With the naive algorithm that loops through all existing patterns, that "worst case" is in fact every (successful) case: if there are no conflicts it will check every pattern against every other, for n(n-1)/2 checks. To see if this matters in practice, I collected all the methods from 260 Google Cloud APIs described by discovery docs, resulting in about 5000 patterns. In reality, no one server would serve all these patterns—more likely there are 260 separate servers—so I think this is a reasonable worst-case scenario. (Please correct me if I'm wrong.) Using naive conflict checking, it took about a second to register all the patterns—not too shabby for server startup, but not ideal. I then implemented a simple indexing scheme to weed out patterns that could not conflict, which reduced the time 20-fold, to 50 milliseconds. There are still sets of patterns that would trigger quadratic behavior, but I don't believe they would arise naturally; they would have to be carefully (maliciously?) constructed. And if you are being malicious, you are probably only hurting yourself: one writes patterns for one's own server, not the servers of others. If we do encounter real performance issues, we can index more aggressively.
cf. Quoted from https://github.com/golang/go/discussions/60227#discussioncomment-6204048
I had similar thoughts about the performance of HTTP routers, so I can agree with ServeMux's thoughts on performance.
goblin uses a data structure based on Trie Tree.
The reason why we did not choose Radix Tree, which is more memory efficient than Trie Tree, was because we felt that it would be complicated and difficult to understand and maintain, but the more complex the data structure, the better the performance benefits. Will I be able to enjoy it? Partly because I had doubts about that.
Go seems to pursue simplicity from the language philosophy, so I think the trend will be to optimize the current simplicity rather than adopting complex data structures and algorithms for ServeMux. I think so. (perhaps.)
Looking at the benchmark results, it seems like there are some tuning points, so I think the score will improve further in the future.
At that time, I would like to hold the 2nd Tenkaichi HTTP Router Fighting Tournament.
cf. Tenkaichi HTTP Router Fighting Association
Lessons learned from comparing ServeMux and a homemade HTTP router
What I noticed when looking at Go1.22's ServeMux implementation was that the routing algorithm was simple, yet had high performance.
I thought that as long as I didn't make a mistake in my selection, the performance would be guaranteed to a certain extent. (An algorithm amateur's view.)
routing_tree_test.go or pattern_test.go I thought the reason why the test case was clean was because the data structure was simple. .
goblin is quite tragic and is a point of concern.
The Trie Tree used by goblin is simple, but the way it is used is not simple, so there may be room for improvement.
Looking at the discussions and proposals, I wondered how much performance I expected from the data structure and algorithm selection. I once again felt that this perspective is important.
The more you pursue performance, the further you get from simplicity, so balance is probably important. (Basically, Uncle Balance.)
Personal opinion on Go router selection
When developing a new application from now on, I think the basic approach is to first consider ServeMux, and if there are any deficiencies, consider a third party.
On the other hand, in terms of whether you should consider migrating from the HTTP router used in your existing applications to ServeMux, I think there are the following points of view.
- Do you want to focus as much on the standard library as possible, reducing dependencies on third parties?
- If you were reluctantly using a third party because you couldn't use wildcards, you might want to actively consider switching.
- How compatible is your HTTP router with net/http?
- If you are using something that provides its own Handler definition or request parameter acquisition method, it may take some time to migrate.
- Do you need features or performance that ServeMux doesn't have?
- If you need middleware, regular expression routing, grouping, etc., it may make sense to continue using a third party.
- Does the routing priority have its own logic?
- There is no problem if it can be guaranteed by testing, but it may become one of the barriers when migrating.
I created a simple flowchart to decide whether to use ServeMux or a third party.
Reference
- net/http: add methods and path variables to ServeMux patterns
- net/http: enhanced ServeMux routing
- Go 1.22 Release Notes - Enhanced routing patterns
- go.dev - Go 1.22 is released!
- go.dev - Routing Enhancements for Go 1.22
- zenn.dev - About the new router in Go 1.22
- zenn.dev - michi routing library designed for Go 1.22's Enhanced ServeMux
- future-architect.github.io - I gave a presentation at the Go1.22 release party with the title "Conflict detection and performance of ServeMux"
- rhumie.github.io - ServeMux conflict detection and performance
- eli.thegreenplace.net - Better HTTP server routing in Go 1.22
- shijuvar.medium.com - Building REST APIs With Go 1.22 http.ServeMux
- www.calhoun.io - Go's 1.22+ ServeMux vs Chi Router
- www.alexedwards.net - Which Go router should I use? (with flowchart)
- www.youtube.com - Why The Golang 1.22 HTTP Router Is Not Great
- www.reddit.com - The proposal to enhance Go's HTTP router
Top comments (0)