Go标准库-ServeMux的使用与模式匹配深入探究
作者:凉凉的知识库
引言
本篇为【深入理解Go标准库】系列第二篇
第一篇:http server的启动
第二篇:ServeMux的使用与模式匹配👈
根据 Golang 文档 中的介绍,ServeMux
是一个 HTTP 请求多路复用器(HTTP Request multiplexer
)。它按照一定规则匹配请求URL和已注册的模式,并执行其中最匹配的模式的Handler
如果你还不知道什么是Handler
,强烈建议你先阅读下:第一篇:http server的启动
基本使用
http.ServeMux
实现了Handler
接口
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
http.ServeMux
提供两个函数用于注册不同Path的处理函数
ServeMux.Handle
接收的是Handler
接口实现ServeMux.HandleFunc
接收的是匿名函数
type PathBar struct { } func (m PathBar) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path bar")) return } func main() { mx := http.NewServeMux() mx.Handle("/bar/", PathBar{}) mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) }) http.ListenAndServe(":8009", mx) }
🌲 通过类型转换实现接口
值得一提的是ServeMux.HandleFunc
的实现,底层还是调用了ServeMux.Handle
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) }
HandlerFunc(handler)
这里并不是函数调用,而是类型转换
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
通过把handler func(ResponseWriter, *Request)
转换成类型HandlerFunc
,而类型HandlerFunc
实现了Handler
接口
🌲 全局默认值
当没有设置http.Server.Handler
属性时,http.Server
就会使用一个全局的变量DefaultServeMux *ServeMux
来作为http.Server.Handler
的值
下面的代码和上面的没有区别
func main() { http.Handle("/bar/", PathBar{}) http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) }) http.ListenAndServe(":8009", nil) }
Pattern匹配
预处理
预处理的是请求的url,以方便匹配,在注册时是不会做任何处理的
移除host中的端口号
针对 URL 中包含
..
或者.
的请求,ServeMux 会对其 Path 进行整理,并匹配到合适的路由模式上针对 URL 中包含重复
/
的请求,ServeMux 会对其进行重定向
func main() { mx := http.NewServeMux() mx.HandleFunc("/abc/def", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.Host, request.URL.Path) }) http.ListenAndServe(":8009", mx) }
🌲 预处理的是请求的url
pattern是不会被处理的,而请求的url都是被处理成标准格式
所以如果注册如下的pattern,无论如何也是无法被命中的
func main() { mx := http.NewServeMux() mx.HandleFunc("/abc//def", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.Host, request.URL.Path) }) }
无论是/abc/def
还是/abc//def
都无法被命中
$ curl 127.0.0.1:8009/abc/def 404 page not found $ curl 127.0.0.1:8009/abc//def <a href="/abc/def" rel="external nofollow" rel="external nofollow" >Moved Permanently</a>.
🌲 带 ..
或者.
请求与重复/
请求的处理不同
包含..
或者.
整理之后匹配到合适的路由模式上,并不会重定向
$ curl 127.0.0.1:8009/ccc/../abc/./def 127.0.0.1:8009 /abc/def
含重复/
,会重定向
$ curl -v 127.0.0.1:8009/abc//def * Trying 127.0.0.1:8009... * Connected to 127.0.0.1 (127.0.0.1) port 8009 (#0) > GET /abc//def HTTP/1.1 > Host: 127.0.0.1:8009 > User-Agent: curl/7.79.1 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 301 Moved Permanently < Content-Type: text/html; charset=utf-8 < Location: /abc/def < Date: Thu, 10 Nov 2022 16:05:13 GMT < Content-Length: 43 < <a href="/abc/def" rel="external nofollow" rel="external nofollow" >Moved Permanently</a>. * Connection #0 to host 127.0.0.1 left intact
路径匹配
ServeMux 注册路由模式的方式有两种,固定根路径
例如"/favicon.ico",与以根路径开始的子树
,例如"/images/"
🌲 固定路径(fixed, rooted paths)
固定根路径
就是指定一个固定的 URL 和请求进行精确匹配
🌲 以根路径开始的子树(rooted subtrees)
以根路径开始的子树
是符合最长路径匹配的原则的,例如我们注册了两个子路径,/image/gif/
和/image/
,URL 为/image/gif/
的请求会优先匹配第一个路由模式,其他路径会匹配/image/
⚠️ 注意:
1、凡是/
结尾的路径都被看作以根路径开始的子树,因此 /
也被看作以根路径开始的子树,它不仅匹配/
,而且也会匹配所有未被其他路由模式匹配的请求。
func main() { mx := http.NewServeMux() mx.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.URL.EscapedPath()) }) http.ListenAndServe(":8009", mx) }
$ curl 127.0.0.1:8009/abc /abc
2、如果只注册了一个子树路径(/
结尾)并且请求URL没有/
结尾,ServeMux会返回重定向。如果再增加一个没有/
结尾的模式的话,就会精确匹配,也就不会有这种行为了
例如我们只注册了子路径/abc/
,服务器会自动将/abc
请求重定向为/abc/
。
func main() { mx := http.NewServeMux() mx.HandleFunc("/abc/", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.URL.EscapedPath()) }) http.ListenAndServe(":8009", mx) }
$ curl -v 127.0.0.1:8009/abc * Trying 127.0.0.1:8009... * Connected to 127.0.0.1 (127.0.0.1) port 8009 (#0) > GET /abc HTTP/1.1 > Host: 127.0.0.1:8009 > User-Agent: curl/7.79.1 > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 301 Moved Permanently < Content-Type: text/html; charset=utf-8 < Location: /abc/ < Date: Thu, 10 Nov 2022 15:30:13 GMT < Content-Length: 40 < <a href="/abc/" rel="external nofollow" >Moved Permanently</a>. * Connection #0 to host 127.0.0.1 left intact
如果我们不想让服务器自动重定向的话,只需要再添加一个/abc
模式就好了
func main() { mx := http.NewServeMux() mx.HandleFunc("/abc/", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.URL.EscapedPath()) }) mx.HandleFunc("/abc", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.URL.EscapedPath()) }) http.ListenAndServe(":8009", mx) }
$ curl 127.0.0.1:8009/abc /abc
域名匹配(Host-specific patterns)
ServeMux 还支持根据主机名精确匹配,匹配时会严格匹配host,path的匹配则还遵循上面的原则
⚠️ 注意:
有域名的优先级会更高,所以可以注册一个带域名的路径和不带域名的路径
func main() { mx := http.NewServeMux() mx.HandleFunc("example01.com/abc/", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.Host, request.URL.EscapedPath()) }) mx.HandleFunc("/abc/", func(writer http.ResponseWriter, request *http.Request) { fmt.Fprintln(writer, request.Host, request.URL.EscapedPath()) }) http.ListenAndServe(":8009", mx) }
example01.com会匹配第一个handler,其他域名则匹配第二个
$ curl -H 'HOST:example01.com' 127.0.0.1:8009/abc/ example01.com /abc/ $ curl -H 'HOST:example02.com' 127.0.0.1:8009/abc example02.com /abc
Method和路径参数匹配(method, path specificity patterns)
最新的特性还在讨论中,大致的patterns会像下面这样
https://github.com/golang/go/discussions/60227
/item/ POST /item/{user} /item/{user} /item/{user}/{id} /item/{$} POST alt.com/item/{user}
以上就是Go标准库-ServeMux的使用与模式匹配深入探究的详细内容,更多关于Go ServeMux模式匹配的资料请关注脚本之家其它相关文章!