ServeMux reimplemented, moved from server.go to file servemux.go.
The Request object has a new attribute PathValues []string and a method func (r *Request) GetPathValue(key string) string.
MethodNotAllowed adds a new 405 status code processing function, named the same as the http.NotFound function.
servemux_test.go Unit tests for new features.
benchmarkServeMux adds PathValues reset to Request of performance test.
Compatibility
When the original path has the suffix /, the equivalent rule will match /*.
If the first character of the path is not /, it is registered with host mode. When matching, it will try to match the host mode.
panic when registering the same path again.
When requesting the path /tree, a redirect will be returned if it cannot be matched and there is a /tree/ rule.
When requesting a path such as //, redirect after cleaning the path.
New features
PathValues, use :name, *, *name matching variables or wildcards in the path, such as /:name/*resourec and /*.
Path matching priority, priority matching is performed when performing matching, according to: Constant > Variable > Wildcard, and has nothing to do with the order during registration.
To specify method matching, you can add methods in the pattern parameter of Handle, such as GET, POST /index.
ANY method priority. When ANY and specific methods are supported in the same path, specific methods are returned first, such as ANY /index and GET /index. Different request methods return different Handlers, and the matching method is the same as The registration order does not matter; when matching the ANY method, the method is the global variable MuxAnyMethod.
Customize the router's allowed methods. The 9 request methods in RFC are allowed by default. You can modify the global variable MuxAllMethod to extend customization, such as Webdav's non-standard methods.
405 status response. When the path matches and there is no matching method, 405 status and Allow Header can be returned.
Customize Hander for 404 and 405 status, for example: mux.HandleFunc("404", serve(403)).
Sub-route, allowing ServeMux to be used as http.Hander to perform multiple matches, using / + {last PathValue} as the next matching path.
Implemented but not added functionality
Default PathValue constants, allowing constants to be added after the path during registration, such as /src/* autoindex=true, and the autoindex parameter can be obtained in the PathValue.
Regular sum verification function, allowing the use of /id|^\d+$ or /id|num when registering, adding function verification function to variables and wildcards, and matching performance will not be affected when verification rules are not registered.
Obtain registered routing information. Record the method, path, and function name when registering. You can use the method to obtain all routing information.
Route cutting uses block patterns, for example: {/:id}, which does not match variables, but matches the request path /:id.
Fixed the ANY method. After modifying the global variable MuxAnyMethod during the registration process, the Any method has a method and only the MuxAnyMethod value at the time of registration. Subsequent modification of the variable MuxAnyMethod will not affect the registered ANY method. Not sure which behavior is more appropriate.
Route deletion allows you to delete the path function for Radix. This function has no effect.
Problems exist:
Whether to add a new func (*ServeMux) HandleMethod(string, string, Handler) method and register specific method routes; the current Handle method registration: path, host+path, method+path, 404 and other rules.
Whether to add func (*Request) GetPathValues(string) []string method to read all values of the same key.
/:name will not match the empty string, / or /* can match the empty string, is it necessary to unify the standard?
Route splitting allows /api/user:get to be split into [/api/user :get], without forcing variables and wildcards to follow /.
The registered path //a does not clear the path and becomes /a, otherwise it will cause a registration conflict when registering /a and //a again. Should the path be cleared?
The sub-route uses / + {last PathValue} as the matching path. The first route /api/*api matches, and the sub-route matching path is /{api}; but when using /user/: The name/list rule makes the sub-route matching path /{name}. How to define the sub-route path rule?
Since some other test cases cannot be executed, use CGO_ENABLED=1 go test -v -timeout=1m -race -cover -coverprofile=coverage.out -coverpkg=net/http -run='(Mux|Slash|Redirect|PathV)'The command executed ServeMux related tests, but 100% coverage test of ServeMux has been completed.
Match performance and code complexity
The new version of ServeMuximplementation does not use map types and goto keywords.
Use BenchmarkServeMux to test the old and new versions. The static test performance of the new and old versions is similar.
Tested using Github Api with httprouter v1.3.0 and chi v1.5.5, the new version has at least 50% of the performance of httprouter and uses less memory.
New ServeMux code complexity Top 3: 16 12 10
httprouter v1.3.0 code complexity Top3: 46 33 28
chi v1.5.5 code complexity Top3: 31 18 17
example
func main() {
serve := func(code int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
}
}
ok := serve(200)
// add webdav method.
http.MuxAllMethod = append(http.MuxAllMethod, "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK")
mux := http.NewServeMux2()
// set 404 and 405 status hadler.
mux.HandleFunc("404", serve(403))
mux.HandleFunc("NOTFOUND", serve(403))
mux.HandleFunc("405", serve(403))
mux.HandleFunc("METHODNOTALLOWED", serve(403))
mux.HandleFunc("404,405", serve(403)) // error: split to [ANY] 404,405.
// The same path is registered by different methods.
// ANY does not cover fixed methods and has nothing to do with the registration order.
mux.HandleFunc("ANY /method/*", ok)
// mux.HandleFunc("/method/*", ok) // Register the same rule panic again.
mux.HandleFunc("GET,POST /method/*", ok)
mux.HandleFunc("/method/any", ok) // to [ANY] /method/any.
// Constants, variables, and wildcards registered within the same parent have path matching priority, regardless of the registration order.
mux.HandleFunc("/var/*", ok) // Set the wildcard name to "*" and allow matching of empty strings.
mux.HandleFunc("/var/*v", ok) // The name of the override wildcard is "v".。
mux.HandleFunc("/var/v1", ok)
mux.HandleFunc("/var/v2", ok)
mux.HandleFunc("/var/:", ok) // Set the name of the variable to ":" and cannot match the empty string.
mux.HandleFunc("/var/:v", ok) // The variable name "v" is not overwritten and considered to be two different patterns. The "/var/:v" pattern will never be matched.
mux.HandleFunc("/var/:v1/v1", ok)
mux.HandleFunc("/var/:v2/v2", ok) // Two different patterns have different PathValue names when matching.
// Create a sub-route and add a Middleware.
admin := http.NewServeMux()
admin.HandleFunc("/*", ok)
admin.HandleFunc("/index", ok)
admin.HandleFunc("/pprof/", ok)
admin.HandleFunc("GET /src/*path", ok)
mux.Handle("/admin/*", NewMiddlwareBaiscauth(admin))
// compatibility
mux.HandleFunc("/src/", ok) // Registering `[ANY] /src/` and `[ANY] /src/*` shows pattern as "/src/", and then registering a new version of the rule "/src/*" conflicts.
mux.HandleFunc("golang.org/src/", ok) // Host matching and registration.
// mux.HandleFunc("golang.org/src/", ok) // Register the same rule panic again.
// Request "/src" redirected to "/src/".
// Request "//src/" redirected to "/src/".
}
func NewMiddlwareBaiscauth(h http.Handler) http.Handler {
passwords := map[string]string{"eudore": ""}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Determine the permissions and return 403 if the user does not exist.
if passwords == nil {
w.WriteHeader(403)
return
}
h.ServeHTTP(w, r)
})
}
Router matching performance
Use the original BenchmarkServeMux method to test 180 static paths and compare the test results of version 1.20 ServeMux:
The old ServeMux uses Map to perform matching, and the new ServeMux uses Radix to implement it. The old and new versions have similar performance for static path testing.
The new ServeMux, httprouter, and chi are all implemented using Radix. Compared with httprouter, they have about 50% static performance and 70% Api performance at worst, and use less memory.
The Radix tree algorithm will not be described again. In Eudore Router, three Kinds are distinguished: constants/variables/wildcards based on muxNode.
When inserting, it is judged that the Kind attribute is added to different children groups. When matching, children are processed in order to achieve matching priority and better logic.
In the previous stdNode implementation, stdNode also had the kind int32 attribute, which was added to different children according to Kind in the insertNode method; the new muxNode will remove the Kind attribute and determine the muxNode.kind value through data , perform adding different children; hide the Kind logic and delete a storage field.
stdNode is the previous implementation method, muxNode is the implementation method submitted to net/http.ServeMux.
muxNode attributes:
path saves the path of Const Node, also in the Radix path.
name saves Param Node and Wildcard Node variable names.
route saves the current routing pattern and marks this Node as a Data Node with a Handler.
children saves various types of children.
methods saves registered methods.
handlers saves registered Handlers.
When registering /api/:v1/v1 and /api/:v2/v2, since different variable names are considered to be different Param Nodes, they need to be saved using Pchildren []*muxNode.
type muxNode struct {
path string // constant path
name string // variable name
route string // routing pattern
// children
Wchildren *muxNode
Cchildren []*muxNode
Pchildren []*muxNode
// handlers
anyHandler Handler
methods []string
handlers []Handler
}
func (r *stdNode) insertNode(path string, nextNode *stdNode) *stdNode {
if len(path) == 0 {
return r
}
switch nextNode.kind {
case stdNodeKindConst:
return r.insertNodeConst(path, nextNode)
case stdNodeKindParam:
...
r.Pchildren = append(r.Pchildren, nextNode)
case stdNodeKindParamValid:
...
r.PVchildren = append(r.PVchildren, nextNode)
case stdNodeKindWildcardValid:
...
r.WVchildren = append(r.WVchildren, nextNode)
case stdNodeKindWildcard:
...
r.Wchildren = nextNode
}
return nextNode
}
func (r *stdNode) lookNode(searchKey string, params *Params) *stdNode {
// constant match, return data
if len(searchKey) == 0 && r.route != "" {
return r
}
for _, child := range r.Cchildren {...}
for _, child := range r.PVchildren {...}
for _, child := range r.Pchildren {...}
for _, child := range r.WVchildren {...}
if r.Wchildren != nil {...}
return nil
}
Route split
In Router Radix, Node is classified into at least 3 types. You need to use a function to accurately convert the pattern into routes, and then create a type Node and add it to Radix.
Path cutting is implemented in the muxSplitRoutes function; for /: and /* it is cut into variables and wildcard Node, used for Radix to add children.
In the insertRoute method, use the insertNode method to add the new Node that has been cut to the end of the current Radix until the end Node of the last path, and then set the routing pattern information.
In the insertNode method, the addition and reuse of Node and the splitting of Radix Node will be dealt with in detail.
Use : for variable parameters and * for wildcard parameters. Consider the following factors:
Compatible with popular frameworks such as echo and gin.
{} is retained as a block pattern matching complex rule and is not added to ServeMux.
For example: /id/{:id|^/\\d+$}, with / in the regular rule, use block mode to force the cut Routes result.
Request.PathValues
Add PathValues []string to the Request object to save the path parameters, and use the []string{"route", "route pattern", "key1", "val1", "key2", "val2"} format to save it For key-value data, my personal habit is that the first place is the routing pattern.
There are following factors when using the []string type:
When there is relatively little data, the performance of using []string to operate KV is higher than that of map and avoid GC.
When used by a third-party framework, it can be used with sync.Pool for memory reuse.
Compatible with third-party high-performance frameworks, the framework matching path parameters can be passed to the Request object for http.Handler to use the third-party framework path parameters.
type Request struct {
PathValues []string
...
}
You can also add a new PathValue type, using the following definition, both have the same memory model.
type Request struct {
PathValues []PathValue
...
}
type PathValue struct {
Path string
Value string
}
Group Router
When using group middleware in net/http.ServeMux, you may only be able to use sub-routes without modifying the API, and execute middleware and multiple route matching on the sub-routes.
In order to avoid conflicts with the Method and Host in the pattern used by ServeMux.Handle, the path registered using the sub-route is prefixed with /, allowing the method to be specified but not the Host.
In the ServeMux.Handler method, the number of Request.PathValues parameters is used to determine whether it is a sub-route; when it is a sub-route, the Host and redirection are no longer processed, and Request.URL.Path is not used as the route matching path. , instead obtain the path from PathValues; the matching path used by the corresponding sub-route is / + {last PathValue}, which also uses / as the prefix.
If allowed, I can add Basic auth, Black list, Compress, Cors, Rate request, Rate speed, Recover, Referer Handlers.
func (mux *ServeMux) Handler(r *Request) (Handler, string) {
if len(r.PathValues) > 1 {
path := "/"
if r.PathValues[len(r.PathValues)-2] != MuxValueRoute {
path += r.PathValues[len(r.PathValues)-1]
}
h, shouldRedirect := mux.match(r.Method, path, r)
if h == nil || shouldRedirect {
h = mux.handler404
}
return h, r.GetPathValue(MuxValueRoute)
}
return mux.handler(r)
}
Describe
ServeMux
reimplemented, moved fromserver.go
to fileservemux.go
.Request
object has a new attributePathValues []string
and a methodfunc (r *Request) GetPathValue(key string) string
.MethodNotAllowed
adds a new 405 status code processing function, named the same as thehttp.NotFound
function.servemux_test.go
Unit tests for new features.benchmarkServeMux
adds PathValues reset toRequest
of performance test./
, the equivalent rule will match/*
./
, it is registered with host mode. When matching, it will try to match the host mode./tree
, a redirect will be returned if it cannot be matched and there is a/tree/
rule.//
, redirect after cleaning the path.:name
,*
,*name
matching variables or wildcards in the path, such as/:name/*resourec
and/*
.Constant > Variable > Wildcard
, and has nothing to do with the order during registration.pattern
parameter ofHandle
, such asGET, POST /index
.ANY
method priority. WhenANY
and specific methods are supported in the same path, specific methods are returned first, such asANY /index
andGET /index
. Different request methods return different Handlers, and the matching method is the same as The registration order does not matter; when matching theANY
method, the method is the global variableMuxAnyMethod
.MuxAllMethod
to extend customization, such as Webdav's non-standard methods.405 status
response. When the path matches and there is no matching method,405 status
andAllow
Header can be returned.404 and 405 status
, for example:mux.HandleFunc("404", serve(403))
.ServeMux
to be used ashttp.Hander
to perform multiple matches, using/ + {last PathValue}
as the next matching path./src/* autoindex=true
, and theautoindex
parameter can be obtained in the PathValue./id|^\d+$
or/id|num
when registering, adding function verification function to variables and wildcards, and matching performance will not be affected when verification rules are not registered.{/:id}
, which does not match variables, but matches the request path/:id
.ANY
method. After modifying the global variableMuxAnyMethod
during the registration process, the Any method has a method and only theMuxAnyMethod
value at the time of registration. Subsequent modification of the variableMuxAnyMethod
will not affect the registeredANY
method. Not sure which behavior is more appropriate.Radix
. This function has no effect.func (*ServeMux) HandleMethod(string, string, Handler)
method and register specific method routes; the currentHandle
method registration:path
,host+path
,method+path
,404
and other rules.func (*Request) GetPathValues(string) []string
method to read all values of the same key./:name
will not match the empty string,/
or/*
can match the empty string, is it necessary to unify the standard?/api/user:get
to be split into[/api/user :get]
, without forcing variables and wildcards to follow/
.//a
does not clear the path and becomes/a
, otherwise it will cause a registration conflict when registering/a
and//a
again. Should the path be cleared?/ + {last PathValue}
as the matching path. The first route/api/*api
matches, and the sub-route matching path is/{api}
; but when using/user/: The name/list
rule makes the sub-route matching path/{name}
. How to define the sub-route path rule?CGO_ENABLED=1 go test -v -timeout=1m -race -cover -coverprofile=coverage.out -coverpkg=net/http -run='(Mux|Slash|Redirect|PathV)'
The command executed ServeMux related tests, but 100% coverage test of ServeMux has been completed.ServeMux
implementation does not usemap
types andgoto
keywords.BenchmarkServeMux
to test the old and new versions. The static test performance of the new and old versions is similar.Github Api
withhttprouter v1.3.0
andchi v1.5.5
, the new version has at least 50% of the performance ofhttprouter
and uses less memory.ServeMux
code complexity Top 3: 16 12 10httprouter v1.3.0
code complexity Top3: 46 33 28chi v1.5.5
code complexity Top3: 31 18 17example
Router matching performance
Use the original
BenchmarkServeMux
method to test 180 static paths and compare the test results of version 1.20ServeMux
:The old
ServeMux
usesMap
to perform matching, and the newServeMux
usesRadix
to implement it. The old and new versions have similar performance for static path testing.Use
Github Api
performance test to comparehttprouter v1.3.0
andchi v1.5.5
:For test methods, refer to vishr/web-framework-benchmark to perform routing matching testing.
The new
ServeMux
,httprouter
, andchi
are all implemented usingRadix
. Compared withhttprouter
, they have about 50% static performance and 70% Api performance at worst, and use less memory.Code complexity
In the new
ServeMux
implementation, thegoto
keywords is not used, and thegocyclo
tool is used to count the top 10 code complexity:Design
Radix tree
The Radix tree algorithm will not be described again. In Eudore Router, three Kinds are distinguished: constants/variables/wildcards based on muxNode.
When inserting, it is judged that the Kind attribute is added to different children groups. When matching, children are processed in order to achieve matching priority and better logic.
In the previous stdNode implementation,
stdNode
also had thekind int32
attribute, which was added to different children according to Kind in theinsertNode
method; the new muxNode will remove the Kind attribute and determine themuxNode.kind
value through data , perform adding different children; hide the Kind logic and delete a storage field.muxNode attributes:
path
saves the path ofConst Node
, also in theRadix
path.name
savesParam Node
andWildcard Node
variable names.route
saves the current routing pattern and marks this Node as aData Node
with a Handler.children
saves various types of children.methods
saves registered methods.handlers
saves registered Handlers.When registering
/api/:v1/v1
and/api/:v2/v2
, since different variable names are considered to be different Param Nodes, they need to be saved usingPchildren []*muxNode
.Route split
In
Router Radix
, Node is classified into at least 3 types. You need to use a function to accurately convert the pattern into routes, and then create a type Node and add it to Radix.Path cutting is implemented in the
muxSplitRoutes
function; for/:
and/*
it is cut into variables and wildcard Node, used for Radix to add children.In the
insertRoute
method, use theinsertNode
method to add the new Node that has been cut to the end of the current Radix until the end Node of the last path, and then set the routing pattern information.In the
insertNode
method, the addition and reuse of Node and the splitting of Radix Node will be dealt with in detail.Parameter symbols
Use
:
for variable parameters and*
for wildcard parameters. Consider the following factors:{}
is retained as a block pattern matching complex rule and is not added to ServeMux./id/{:id|^/\\d+$}
, with/
in the regular rule, use block mode to force the cut Routes result.Request.PathValues
Add
PathValues []string
to the Request object to save the path parameters, and use the[]string{"route", "route pattern", "key1", "val1", "key2", "val2"}
format to save it For key-value data, my personal habit is that the first place is the routing pattern.There are following factors when using the
[]string
type:[]string
to operate KV is higher than that ofmap
and avoid GC.sync.Pool
for memory reuse.You can also add a new
PathValue
type, using the following definition, both have the same memory model.Group Router
When using group middleware in
net/http.ServeMux
, you may only be able to use sub-routes without modifying the API, and execute middleware and multiple route matching on the sub-routes.example:
In order to avoid conflicts with the Method and Host in the pattern used by
ServeMux.Handle
, the path registered using the sub-route is prefixed with/
, allowing the method to be specified but not the Host.In the
ServeMux.Handler
method, the number ofRequest.PathValues
parameters is used to determine whether it is a sub-route; when it is a sub-route, the Host and redirection are no longer processed, andRequest.URL.Path
is not used as the route matching path. , instead obtain the path from PathValues; the matching path used by the corresponding sub-route is/ + {last PathValue}
, which also uses/
as the prefix.