rest框架概覽
我們先通過 go-zero
自帶的命令列工具 goctl
來生成一個 api service
,其 main
函式如下:
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
- 解析配置檔案
- 將配置檔案傳入,初始化
serviceContext
- 初始化
rest server
將
context
注入server
中:- 註冊路由
- 將
context
中的啟動的endpoint
同時注入到router
當中
- 啟動
server
接下來我們來一步步講解其設計原理!Let's Go!
web框架
從日常開發經驗來說,一個好的 web 框架大致需要滿足以下特性:
- 路由匹配/多路由支援
- 支援自定義中介軟體
- 框架和業務開發完全解耦,方便開發者快速開發
- 引數校驗/匹配
- 監控/日誌/指標等服務自查功能
- 服務自保護(熔斷/限流)
go-zero rest設計
https://github.com/zeromicro/go-zero/tree/master/rest
概覽
- 藉助 context (不同於 gin 的 context),將資源初始化好 → 儲存在
serviveCtx
中,在 handler 中共享(至於資源池化,交給資源自己處理,serviveCtx
只是入口和共享點) - 獨立 router 宣告檔案,同時加入 router group 的概念,方便開發者整理程式碼結構
- 內建若干中介軟體:監控/熔斷/鑑權等
- 利用 goctl codegen + option 設計模式,方便開發者自己控制部分中介軟體的接入
上圖描述了 rest 處理請求的模式和大部分處理路徑。
- 框架內建的中介軟體已經幫開發者解決了大部分服務自處理的邏輯
- 同時 go-zero 在
business logic
處也給予開發者開箱即用的元件(dq、fx 等) - 從開發模式上幫助開發者只需要關注自己的
business logic
以及所需資源準備
下面我們來細說一下整個 rest 是如何啟動的?
啟動流程
上圖描述了整體 server 啟動經過的模組和大致流程。準備按照如下流程分析 rest 實現:
- 基於 http.server 封裝以及改造:把 engine(web框架核心) 和 option 隔離開
- 多路由匹配採取 radix-tree 構造
- 中介軟體採用洋蔥模型 →
[]Middleware
- http parse 解析以及匹配校驗 →
httpx.Parse()
- 在請求過程會收集指標 (
createMetrics()
) 以及監控埋點 (prometheus)
server engine封裝
點開大圖觀看
engine 貫穿整個 server 生命週期中:
- router 會攜帶開發者定義的 path/handler,會在最後的 router.handle() 執行
- 註冊的自定義中介軟體 + 框架中介軟體,在 router handler logic 前執行
在這裡:go-zero 處理的粒度在 route 上,封裝和處理都在 route 一層層執行
路由匹配
那麼當 request 到來,首先是如何到路由這一層的?
首先在開發最原始的 http server ,都有這麼一段程式碼:
type helloHandler struct{}
func (h *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}
func main() {
http.Handle("/", &helloHandler{})
http.ListenAndServe(":12345", nil)
}
http.ListenAndServe()
內部會執行到:server.ListenAndServe()
我們看看在 rest 裡面是怎麼運用的:
而傳入的 handler 其實就是:router.NewRouter() 生成的 router。這個 router 承載了整個 server 的處理函式集合。
同時 http.Server 結構在初始化時,是把 handler 注入到裡面的:
type Server struct {
...
Handler Handler
}
func start(..., handler http.Handler, run func(srv *http.Server) error) (err error) {
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: handler,
}
...
return run(server)
}
在 http.Server 接收 req 後,最終執行的也是:handler.ServeHTTP(rw, req)
所以內建的 router 也需要實現 ServeHTTP
。至於 router 自己是怎麼實現 ServeHTTP
:無外乎就是尋找匹配路由,然後執行路由對應的 handle logic。
解析引數
解析引數是 http 框架需要提供的基本能力。在 goctl code gen 生成的程式碼中,handler 層已經整合了 req argument parse 函式:
// generate by goctl
func QueryAllTaskHandler(ctx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// custom request in .api file
var req types.QueryAllTaskRequest
// parse http request
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
l := logic.NewEventLogic(r.Context(), ctx)
resp, err := l.QueryAllTask(req)
baseresponse.FormatResponseWithRequest(resp, err, w, r)
}
}
進入到 httpx.Parse()
,主要解析以下幾塊:
https://github.com/zeromicro/go-zero/blob/master/rest/httpx/requests.go#L32:6
- 解析path
- 解析form表單
- 解析http header
- 解析json
Parse() 中的 引數校驗 的功能見:
https://go-zero.dev/cn/api-grammar.html 中的
tag修飾符
Tips
學習原始碼推薦 fork 出來邊看邊寫註釋和心得,可以加深理解,以後用到這塊功能的時候也可以回頭翻閱。
專案地址
https://github.com/zeromicro/go-zero
歡迎使用 go-zero
並 star 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。