Go标准库http server优雅启动深入理解
作者:凉凉的知识库
如何用最少的代码创建一个HTTP server?
package main import ( "net" "net/http" ) func main() { // 方式1 err := http.ListenAndServe(":8080", nil) if err != nil { panic(err) } }
点开http.ListenAndServe
可以看到函数只是创建了Server
类型并调用server.ListenAndServe()
所以下面的和上面的代码没有区别
package main import ( "net" "net/http" ) func main() { // 方式2 server := &http.Server{Addr: ":8080"} err := server.ListenAndServe() if err != nil { panic(err) } }
ListenAndServe()
如其名会干两件事
监听一个端口,即
Listen
的过程处理进入端口的连接,即
Serve
的过程
所以下面的代码和上面的代码也没区别
package main import ( "net" "net/http" ) func main() { // 方式3 ln, err := net.Listen("tcp", ":8080") if err != nil { panic(err) } server := &http.Server{} err = server.Serve(ln) if err != nil { panic(err) } }
一张图展示三种使用方式
路由?no!Handler!
按上面的代码启动HTTP Server没有太大意义,因为我们还没有设定路由,所以无法正常响应请求
$ curl 127.0.0.1:8080 404 page not found
暂停思考一下,服务器返回404是因为没有设定路由么?no,no,no,你需要转变一下思维。服务器返回404不是因为我们没有设置路由,而是因为没有设置请求的处理程序,这个处理程序在Go中叫作:Handler
!
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
🌲 怎么定义请求的处理程序?
由上可知,仅需要实现ServeHTTP(ResponseWriter, *Request)
接口即可
注意,示例代码没有判断任何路由(PATH)
type handlerImp struct { } func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { w.Write([]byte("Receive GET request")) return } if r.Method == "POST" { w.Write([]byte("Receive POST request")) return } return }
🌲 怎么设置请求的处理程序?
三种方式本质上都是把自定义的Handler
赋值到Server
的Handler
属性中
func main() { // 方式1 // err := http.ListenAndServe(":8080", handlerImp{}) // if err != nil { // panic(err) // } // 方式2 // server := &http.Server{Addr: ":8080", Handler: handlerImp{}} // err := server.ListenAndServe() // if err != nil { // panic(err) // } // 方式3 ln, err := net.Listen("tcp", ":8080") if err != nil { panic(err) } server := &http.Server{Handler:handlerImp{}} err = server.Serve(ln) if err != nil { panic(err) } }
🌲 设置请求的处理程序之后的效果
handlerImp
只针对Method做了不同的响应,没有对PATH做任何的判断,所以无论请求什么样的路径都能拿到一个预期的响应。
$ curl -X POST 127.0.0.1:8080/foo Receive POST request% $ curl 127.0.0.1:8080/foo/bar Receive GET request%
此时再体会一下这句话:我们设置的不是路由,而是设置请求的处理程序
再聊Handler
type handlerImp struct { } func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { w.Write([]byte("Receive GET request")) return } if r.Method == "POST" { w.Write([]byte("Receive POST request")) return } return }
如上所述,无论任何PATH,任何Method等,所有的请求都会被handlerImp.ServeHTTP
处理。
我们可以判断PATH、Method等,根据不同的请求特征执行不同的逻辑,并且全部在这一个函数中全部完成
很明显,这违反了高内聚,低耦合的编程范式
停下来思考下,如何编写一个高内聚,低耦合的handlerImp.ServeHTTP
,使之针对不同HTTP请求执行不同的逻辑呢
type handlerImp struct { } func (imp handlerImp) handleMethodGet(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive GET request")) return } func (imp handlerImp) handleMethodPost(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive POST request")) return } func (imp handlerImp) handlePathFoo(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) return } func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/foo" { imp.handlePathFoo(w, r) return } if r.Method == "GET" { imp.handleMethodGet(w, r) return } if r.Method == "POST" { imp.handleMethodPost(w, r) return } return }
如果你的答案和上面的代码类似,那么我对于这段代码的点评是:不太高明☹️
🌲 如何编写一个高内聚,低耦合的ServeHTTP
,针对不同HTTP请求执行不同的逻辑?
不知道你有没有听过设计模式中,组合模式。没有了解可以去了解下,或者看下图
经过组合模式重新设计的handlerImp
,已经不再包含具体的逻辑了,它先搜索有没有针对PATH处理的逻辑,再搜索有没有针对Method处理的逻辑,它专注于逻辑分派,它是组合模式中的容器。
容器(Container):容器接收到请求后会将工作分配给自己的子项目, 处理中间结果, 然后将最终结果返回给客户端。
type handlerImp struct { pathHandlers map[string]http.Handler methodHandlers map[string]http.Handler } func NewHandlerImp() handlerImp { return handlerImp{ pathHandlers: make(map[string]http.Handler), methodHandlers: make(map[string]http.Handler), } } func (imp handlerImp) AddPathHandler(path string, h http.Handler) { imp.pathHandlers[path] = h } func (imp handlerImp) AddMethodHandler(method string, h http.Handler) { imp.methodHandlers[method] = h } func (imp handlerImp) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h, ok := imp.pathHandlers[r.URL.Path]; ok { h.ServeHTTP(w, r) return } if h, ok := imp.methodHandlers[r.Method]; ok { h.ServeHTTP(w, r) return } return }
重新设计的handlerImp
不执行逻辑,实际的逻辑被分离到每一个叶子结点中,而每一个叶子结点也都实现了ServeHTTP
函数,即Handler
接口
type PathFoo struct { } func (m PathFoo) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) return } type MethodGet struct { } func (m MethodGet) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive GET request")) return } type MethodPost struct { } func (m MethodPost) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive POST request")) return }
再次强调,通过对组合模式的运用,我们把逻辑分派的功能聚合到handlerImp
,把具体的逻辑聚合到PathFoo
、MethodGet
、MethodPost
func main() { // 方式3 ln, err := net.Listen("tcp", ":8080") if err != nil { panic(err) } h := NewHandlerImp() h.AddMethodHandler("GET", MethodGet{}) h.AddMethodHandler("POST", MethodPost{}) h.AddPathHandler("/foo", PathFoo{}) server := &http.Server{Handler: h} err = server.Serve(ln) if err != nil { panic(err) } }
一些Handlers
上面实现的handlerImp
利用组合设计模式,已经能针对Path和Method设定和处理不同的逻辑,但整体功能略显简单。有哪些可以供我们使用且功能强大的Handlers
呢?
http.ServeMux
Go标准库中就提供了一个Handler
实现叫作http.ServeMux
⚠️ 当前(go1.21.*)版本仅支持匹配Path,但目前已经在讨论支持Method匹配和占位符了:net/http: add methods and path variables to ServeMux patterns #60227[1]
使用的方式如下
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() { // 方式3 ln, err := net.Listen("tcp", ":8080") if err != nil { panic(err) } mx := http.NewServeMux() mx.Handle("/bar/", PathBar{}) mx.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) }) server := &http.Server{Handler: mx} err = server.Serve(ln) if err != nil { panic(err) } }
代码mx.Handle("/bar/", PathBar{})
中/bar/
由/
结尾,所以它可以匹配/bar/*
所有的Path
关于http.ServeMux
的细节不是本篇重点,后续会单独介绍
🌲 默认的Handler
因为是标准库内置的实现,当没有设置http.Server.Handler
属性时,http.Server
就会使用一个全局的变量DefaultServeMux *ServeMux
来作为http.Server.Handler
的值
var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
http包同时提供了两个函数可以在DefaultServeMux
注册不同Path的处理函数
func main() { http.Handle("/bar/", PathBar{}) http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) }) // 方式1 err := http.ListenAndServe(":8080", nil) if err != nil { panic(err) } }
http.Handle
接收的是Handler
接口实现,对应的是
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
http.HandleFunc
接收的是匿名函数,对应的是
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
gorilla/mux
gorilla/mux是一个相当流行的第三方库,用法这里简单写下
除了经典的Handle
、HandleFunc
函数,gorilla/mux还提供了Methods
、Schemes
、Host
等非常复杂的功能
但无论多复杂,其一定包含了ServeHTTP
函数,即实现了Handler
接口
func main() { r := mux.NewRouter() r.Handle("/foo/{bar}", PathBar{}) r.Handle("/bar/", PathBar{}) r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive path foo")) }) r.Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive GET request")) }) // 方式1 err := http.ListenAndServe(":8080", r) if err != nil { panic(err) } }
其他
还有很多其他优秀的mux实现,具体可以参考各自的官方文档。
https://github.com/go-chi/chi star 15.9k
https://github.com/julienschmidt/httprouter star 15.6k
关于Go标准库、第三方库中这些结构的关系通过下图展示
再聊组合模式
无论是官方的http.ServeMux
,还是一些第三方库,实现上大多使用了组合设计模式
组合模式的魔力还不止于此。思考一下这个场景:目前已经存在路由servemux/*
,并且使用了ServeMux
mx := http.NewServeMux() mx.Handle("/servemux/bar/", PathBar{}) mx.HandleFunc("/servemux/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive servemux path foo")) })
但此时还有另外一组路由/gorilla/*
,使用了开源库gorilla/mux
r := mux.NewRouter() r.Handle("/gorilla/bar/", PathBar{}) r.HandleFunc("/gorilla/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive gorilla path foo")) })
如何启动这样的服务器呢?
func main() { mx := http.NewServeMux() mx.Handle("/servemux/bar/", PathBar{}) mx.HandleFunc("/servemux/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive servemux path foo")) }) r := mux.NewRouter() r.Handle("/gorilla/bar/", PathBar{}) r.HandleFunc("/gorilla/foo", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Receive gorilla path foo")) }) h := http.NewServeMux() h.Handle("/servemux/", mx) h.Handle("/gorilla/", r) // 方式1 err := http.ListenAndServe(":8080", h) if err != nil { panic(err) } }
利用组合设计模式,h := http.NewServeMux()
作为新的容器,将不同的路由分配给另外两个容器
mx := http.NewServeMux()
r := mux.NewRouter()
总结
本文主要介绍了Go http server的启动方式,重点介绍了http server的请求处理器
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
别看它仅包含一个方法,但在组合模式的加成下,可以实现千变万化的形态。
除了Go标准库中提供了http.ServeMux
还有一系列开源库gorilla/mux
、go-chi/chi
、julienschmidt/httprouter
对Handler
进行了实现。
每一个库具有的能力、使用方式、性能不同,但万变不离其宗,都绕不开组合模式和Handler
接口
以上就是Go标准库http server优雅启动深入理解的详细内容,更多关于Go标准库http server启动的资料请关注脚本之家其它相关文章!