原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-01-introduction.html
因?yàn)?Go 的 net/http
包提供了基礎(chǔ)的路由函數(shù)組合與豐富的功能函數(shù)。所以在社區(qū)里流行一種用 Go 編寫(xiě) API 不需要框架的觀點(diǎn),在我們看來(lái),如果你的項(xiàng)目的路由在個(gè)位數(shù)、URI 固定且不通過(guò) URI 來(lái)傳遞參數(shù),那么確實(shí)使用官方庫(kù)也就足夠。但在復(fù)雜場(chǎng)景下,官方的 http 庫(kù)還是有些力有不逮。例如下面這樣的路由:
GET /card/:id
POST /card/:id
DELTE /card/:id
GET /card/:id/name
...
GET /card/:id/relations
可見(jiàn)是否使用框架還是要具體問(wèn)題具體分析的。
Go 的 Web 框架大致可以分為這么兩類(lèi):
在框架的選擇上,大多數(shù)情況下都是依照個(gè)人的喜好和公司的技術(shù)棧。例如公司有很多技術(shù)人員是 PHP 出身,那么他們一定會(huì)非常喜歡像 beego 這樣的框架,但如果公司有很多 C 程序員,那么他們的想法可能是越簡(jiǎn)單越好。比如很多大廠的 C 程序員甚至可能都會(huì)去用 C 語(yǔ)言去寫(xiě)很小的 CGI 程序,他們可能本身并沒(méi)有什么意愿去學(xué)習(xí) MVC 或者更復(fù)雜的 Web 框架,他們需要的只是一個(gè)非常簡(jiǎn)單的路由(甚至連路由都不需要,只需要一個(gè)基礎(chǔ)的 HTTP 協(xié)議處理庫(kù)來(lái)幫他省掉沒(méi)什么意思的體力勞動(dòng))。
Go 的 net/http
包提供的就是這樣的基礎(chǔ)功能,寫(xiě)一個(gè)簡(jiǎn)單的 http echo server
只需要 30s。
//brief_intro/echo.go
package main
import (...)
func echo(wr http.ResponseWriter, r *http.Request) {
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
wr.Write([]byte("echo error"))
return
}
writeLen, err := wr.Write(msg)
if err != nil || writeLen != len(msg) {
log.Println(err, "write len:", writeLen)
}
}
func main() {
http.HandleFunc("/", echo)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
如果你過(guò)了 30s 還沒(méi)有完成這個(gè)程序,請(qǐng)檢查一下你自己的打字速度是不是慢了(開(kāi)個(gè)玩笑 :D)。這個(gè)例子是為了說(shuō)明在 Go 中寫(xiě)一個(gè) HTTP 協(xié)議的小程序有多么簡(jiǎn)單。如果你面臨的情況比較復(fù)雜,例如幾十個(gè)接口的企業(yè)級(jí)應(yīng)用,直接用 net/http
庫(kù)就顯得不太合適了。
我們來(lái)看看開(kāi)源社區(qū)中一個(gè) Kafka 監(jiān)控項(xiàng)目中的做法:
//Burrow: http_server.go
func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
...
server.mux.HandleFunc("/", handleDefault)
server.mux.HandleFunc("/burrow/admin", handleAdmin)
server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList})
server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka})
server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList})
...
}
上面這段代碼來(lái)自大名鼎鼎的 linkedin 公司的 Kafka 監(jiān)控項(xiàng)目 Burrow,沒(méi)有使用任何 router 框架,只使用了 net/http
。只看上面這段代碼似乎非常優(yōu)雅,我們的項(xiàng)目里大概只有這五個(gè)簡(jiǎn)單的 URI,所以我們提供的服務(wù)就是下面這個(gè)樣子:
/
/burrow/admin
/v2/kafka
/v2/kafka/
/v2/zookeeper
如果你確實(shí)這么想的話(huà)就被騙了。我們?cè)龠M(jìn) handleKafka()
這個(gè)函數(shù)一探究竟:
func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) {
pathParts := strings.Split(r.URL.Path[1:], "/")
if _, ok := app.Config.Kafka[pathParts[2]]; !ok {
return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r)
}
if pathParts[2] == "" {
// Allow a trailing / on requests
return handleClusterList(app, w, r)
}
if (len(pathParts) == 3) || (pathParts[3] == "") {
return handleClusterDetail(app, w, r, pathParts[2])
}
switch pathParts[3] {
case "consumer":
switch {
case r.Method == "DELETE":
switch {
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4])
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case r.Method == "GET":
switch {
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleConsumerList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
// Consumer detail - list of consumer streams/hosts? Can be config info later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
case pathParts[5] == "topic":
switch {
case (len(pathParts) == 6) || (pathParts[6] == ""):
return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4])
case (len(pathParts) == 7) || (pathParts[7] == ""):
return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6])
}
case pathParts[5] == "status":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false)
case pathParts[5] == "lag":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true)
}
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case "topic":
switch {
case r.Method != "GET":
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleBrokerTopicList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4])
}
case "offsets":
// Reserving this endpoint to implement later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
// If we fell through, return a 404
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
因?yàn)槟J(rèn)的 net/http
包中的 mux
不支持帶參數(shù)的路由,所以 Burrow 這個(gè)項(xiàng)目使用了非常蹩腳的字符串 Split
和亂七八糟的 switch case
來(lái)達(dá)到自己的目的,但卻讓本來(lái)應(yīng)該很集中的路由管理邏輯變得復(fù)雜,散落在系統(tǒng)的各處,難以維護(hù)和管理。如果讀者細(xì)心地看過(guò)這些代碼之后,可能會(huì)發(fā)現(xiàn)其它的幾個(gè) handler
函數(shù)邏輯上較簡(jiǎn)單,最復(fù)雜的也就是這個(gè) handleKafka()
。而我們的系統(tǒng)總是從這樣微不足道的混亂開(kāi)始積少成多,最終變得難以收拾。
根據(jù)我們的經(jīng)驗(yàn),簡(jiǎn)單地來(lái)說(shuō),只要你的路由帶有參數(shù),并且這個(gè)項(xiàng)目的 API 數(shù)目超過(guò)了 10,就盡量不要使用 net/http
中默認(rèn)的路由。在 Go 開(kāi)源界應(yīng)用最廣泛的 router 是 httpRouter,很多開(kāi)源的 router 框架都是基于 httpRouter 進(jìn)行一定程度的改造的成果。關(guān)于 httpRouter 路由的原理,會(huì)在本章節(jié)的 router 一節(jié)中進(jìn)行詳細(xì)的闡釋。
再來(lái)回顧一下文章開(kāi)頭說(shuō)的,開(kāi)源界有這么幾種框架,第一種是對(duì) httpRouter 進(jìn)行簡(jiǎn)單的封裝,然后提供定制的中間件和一些簡(jiǎn)單的小工具集成比如 gin,主打輕量,易學(xué),高性能。第二種是借鑒其它語(yǔ)言的編程風(fēng)格的一些 MVC 類(lèi)框架,例如 beego,方便從其它語(yǔ)言遷移過(guò)來(lái)的程序員快速上手,快速開(kāi)發(fā)。還有一些框架功能更為強(qiáng)大,除了數(shù)據(jù)庫(kù) schema 設(shè)計(jì),大部分代碼直接生成,例如 goa。不管哪種框架,適合開(kāi)發(fā)者背景的就是最好的。
本章的內(nèi)容除了會(huì)展開(kāi)講解 router 和中間件的原理外,還會(huì)以現(xiàn)在工程界面臨的問(wèn)題結(jié)合 Go 來(lái)進(jìn)行一些實(shí)踐性的說(shuō)明。希望能夠?qū)](méi)有接觸過(guò)相關(guān)內(nèi)容的讀者有所幫助。
![]() | ![]() |
更多建議: