深入理解 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[2]
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
接口
參考資料
[1]
net/http: add methods and path variables to ServeMux patterns #60227: https://github.com/golang/go/discussions/60227
[2]
gorilla/mux: https://github.com/gorilla/mux#gorillamux
[3]
設計模式 / 結構型模式 / 組合模式: https://refactoringguru.cn/design-patterns/composite
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/s3BWg6iiCcDZRFh7oZJj5Q