go-zero 之 rest 實戰與原理

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架,其中 rest 是 web 模塊,該模塊基於 Go 語言原生的 http 包進行構建,是一個輕量的,高性能的,功能完整的,簡單易用的 web 框架。使用 rest 能夠快速構建 restful 風格 api 服務,同時具備服務監控和彈性服務治理能力","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"快速開始","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們先來快速構建一個服務感受一下,使用 rest 創建 http 服務非常簡單,官方推薦使用","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/tal-tech/go-zero/tree/master/tools/goctl","title":""},"content":[{"type":"text","text":"goctl","attrs":{}}]},{"type":"text","text":"代碼自動生成工具來生成。這裏爲了演示構建的步驟細節我們手動來創建服務,代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package main\n\nimport (\n \"log\"\n \"net/http\"\n\n \"github.com/tal-tech/go-zero/core/logx\"\n \"github.com/tal-tech/go-zero/core/service\"\n \"github.com/tal-tech/go-zero/rest\"\n \"github.com/tal-tech/go-zero/rest/httpx\"\n)\n\nfunc main() {\n srv, err := rest.NewServer(rest.RestConf{\n Port: 9090, // 偵聽端口\n ServiceConf: service.ServiceConf{\n Log: logx.LogConf{Path: \"./logs\"}, // 日誌路徑\n },\n })\n if err != nil {\n log.Fatal(err)\n }\n defer srv.Stop()\n // 註冊路由\n srv.AddRoutes([]rest.Route{ \n {\n Method: http.MethodGet,\n Path: \"/user/info\",\n Handler: userInfo,\n },\n })\n\n srv.Start() // 啓動服務\n}\n\ntype User struct {\n Name string `json:\"name\"`\n Addr string `json:\"addr\"`\n Level int `json:\"level\"`\n}\n\nfunc userInfo(w http.ResponseWriter, r *http.Request) {\n var req struct {\n UserId int64 `form:\"user_id\"` // 定義參數\n }\n if err := httpx.Parse(r, &req); err != nil { // 解析參數\n httpx.Error(w, err)\n return\n }\n users := map[int64]*User{\n 1: &User{\"go-zero\", \"shanghai\", 1},\n 2: &User{\"go-queue\", \"beijing\", 2},\n }\n httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回結果\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 rest.NewServer 創建服務,示例配置了端口號和日誌路徑,服務啓動後偵聽在 9090 端口,並在當前目錄下創建 logs 目錄同時創建各等級日誌文件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後通過 srv.AddRoutes 註冊路由,每個路由需要定義該路由的方法、Path 和 Handler,其中 Handler 類型爲 http.HandlerFunc","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後通過 srv.Start 啓動服務,啓動服務後通過訪問","attrs":{}},{"type":"link","attrs":{"href":"http://localhost:9090/user/info?user_id=1","title":null},"content":[{"type":"text","text":"http://localhost:9090/user/info?user_id=1","attrs":{}}]},{"type":"text","text":"可以看到返回結果","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"{\n name: \"go-zero\",\n addr: \"shanghai\",\n level: 1\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到此一個簡單的 http 服務就創建完成了,可見使用 rest 創建 http 服務非常簡單,主要分爲三個步驟:創建 Server、註冊路由、啓動服務","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"服務監控","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務監控的重要性不言而喻,沒有監控我們就沒法清晰的瞭解到服務的運行情況,也就沒有辦法提前發現問題,當我們感知到問題存在時候往往爲時已晚。服務監控甚至和服務本身同等重要,通過監控我們可以瞭解到當前服務的運行狀況,比如當前的資源使用率、接口的 QPS,接口的耗時,錯誤率等等,以及在業務處理過程中我們也會記錄一些日誌幫助定位排查問題, 對於微服務的性能問題進行定位的時候我們往往還需要知道整條調用鏈路。在 rest 中內置了自動的服務的監控,主要分爲三個方面:日誌、指標和調用鏈","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"日誌","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"快速開始的示例中我們配置了日誌的路徑,服務啓動後會在該路徑下生成日誌文件,默認情況下所有日誌級別均開啓,可以通過","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"logx.SetLevel(1)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"來設置日誌級別,日誌級別的定義如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"const (\n // InfoLevel logs everything\n InfoLevel = iota\n // ErrorLevel includes errors, slows, stacks\n ErrorLevel\n // SevereLevel only log severe messages\n SevereLevel\n)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 logx 包進行日誌記錄,比如我們想要在參數解析出錯的時候記錄一個錯誤日誌如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"if err := httpx.Parse(r, &req); err != nil { // 解析參數\n logx.Errorf(\"parse req: %v error: %v\", req, err)\n httpx.Error(w, err)\n return\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訪問服務的時候我們故意把參數類型傳錯,因爲 user_id 爲 int64 類型,我們傳入字符串 aaa,參數解析就會出錯,查看 error 日誌","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"http://localhost:9090/user/info?user_id=aaa\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\"@timestamp\":\"2020-12-01T10:37:25.654+08\",\"level\":\"error\",\"content\":\"main.go:47 parse req: {0} error: the value \\\"aaa\\\" cannot parsed as int\"}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rest 框架中還會記錄慢日誌,當服務的響應時間大於 3s 的時候就會產生慢日誌,慢日誌自動記錄不需要手動配置,慢日誌如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\"@timestamp\":\"2020-12-01T10:45:47.679+08\",\"level\":\"slow\",\"content\":\"[HTTP] 200 - /user/info?user_id=123 - [::1]:60349 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 - slowcall(5004.3ms)\",\"trace\":\"401274358783b491\",\"span\":\"0\"}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"日誌監控對於我們排查問題非常有幫助,但是文件記錄的方式並不方便對日誌進行檢索,在生產環境一般會藉助 elk,把日誌同步到 elasticsearch 然後通過 kibana 界面實現快速的檢索","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"指標","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務指標監控的種類繁多,可以根據自身的服務特點進行監控,比較通用的指標有比如:接口 QPS、接口耗時、錯誤率等等,通過對這些指標的監控可以瞭解到服務的運行時的一些信息,rest 框架默認支持 prometheus 指標收集能力,通過添加 Prometheus 配置即可查看對應的指標信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/60/60265b54cbb1f13e8748730698dd935e.png","alt":"rest_metric","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了這些指標信息之後就可以配合 grafana 進行界面化的展示","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://static.gocn.vip/photo/2020/b0392391-7d2d-4acb-97bc-ec76b5c711cc.png?x-oss-process=image/resize,w_900","title":null}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4b/4b523eaca1f6f2184392b27211b04e68.png","alt":"grafana","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"調用鏈","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在微服務中服務依賴的關係往往比較複雜,那麼橫跨多個服務的慢請求要如何查詢呢?這時候我們需要一個能串聯整個調用鏈路的標識 (traceId) 和表示調用關係的標識 (spanId),也就是使用 traceId 串聯起單次請求,用 spanId 記錄每一次調用,原理如下圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2a/2aba200213032e297d1802c6bf1cb9c3.png","alt":"trace_list","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於 rest 的 api 服務往往是作爲入口,首先會先從 http header 中獲取 traceid,如果沒有獲取到則會生成一個新的 traceid,通過 context 上下文進行傳遞,我們知道 context 上下文的傳遞是進程內的,那麼跨服務跨進程是如何傳遞的呢?比如 api 服務調用 rpc 服務其實是利用了 rpc 提供的 metadata 功能,先把上下文中的調用鏈信息從 context 讀取出來存入 metadata,然後再從 metadata 中讀取調用鏈信息再存入 context 中","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e5/e57ecf7d1f2f98675d77bb6288e51610.png","alt":"context","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rest 默認會把調用鏈信息記錄在日誌中,通過在 elk 中搜索某一個 taceid 即可得到所有的調用鏈信息,日誌記錄如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\"@timestamp\":\"2020-12-01T10:03:01.280+08\",\"level\":\"info\",\"content\":\"200 - /user/info?user_id=1 - [::1]:58955 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36 - 0.4ms\",\"trace\":\"7a27076abd932c87\",\"span\":\"0\"}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"JWT 鑑權","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑權幾乎是每個應用必備的能力,鑑權的方式很多,而 jwt 是其中比較簡單和可靠的一種方式,在 rest 框架中內置了 jwt 鑑權功能,jwt 的原理流程如下圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://static.gocn.vip/photo/2020/6f21082d-c0d3-449a-b20a-33b71fe44867.png?x-oss-process=image/resize,w_900","title":null}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/41/4163586e85e3e433d944c648d2b1c9ab.png","alt":"jwt","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rest 框架中通過 rest.WithJwt(secret) 啓用 jwt 鑑權,其中 secret 爲服務器祕鑰是不能泄露的,因爲需要使用 secret 來算簽名驗證 payload 是否被篡改,如果 secret 泄露客戶端就可以自行簽發 token,黑客就能肆意篡改 token 了。我們基於上面的例子進行改造來驗證在 rest 中如何使用 jwt 鑑權","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"獲取 jwt","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步客戶端需要先獲取 jwt,在登錄接口中實現 jwt 生成邏輯","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"srv.AddRoute(rest.Route{\n Method: http.MethodPost,\n Path: \"/user/login\",\n Handler: userLogin,\n})\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了演示方便,userLogin 的邏輯非常簡單,主要是獲取信息然後生成 jwt,獲取到的信息存入 jwt payload 中,然後返回 jwt","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func userLogin(w http.ResponseWriter, r *http.Request) {\n var req struct {\n UserName string `json:\"user_name\"`\n UserId int `json:\"user_id\"`\n }\n if err := httpx.Parse(r, &req); err != nil {\n httpx.Error(w, err)\n return\n }\n token, _ := genToken(accessSecret, map[string]interface{}{\n \"user_id\": req.UserId,\n \"user_name\": req.UserName,\n }, accessExpire)\n\n httpx.WriteJson(w, http.StatusOK, struct {\n UserId int `json:\"user_id\"`\n UserName string `json:\"user_name\"`\n Token string `json:\"token\"`\n }{\n UserId: req.UserId,\n UserName: req.UserName,\n Token: token,\n })\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"生成 jwt 的方法如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {\n now := time.Now().Unix()\n claims := make(jwt.MapClaims)\n claims[\"exp\"] = now + expire\n claims[\"iat\"] = now\n for k, v := range payload {\n claims[k] = v\n }\n token := jwt.New(jwt.SigningMethodHS256)\n token.Claims = claims\n return token.SignedString([]byte(secret))\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"啓動服務後通過 cURL 訪問","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"curl -X \"POST\" \"http://localhost:9090/user/login\" \\\n -H 'Content-Type: application/json; charset=utf-8' \\\n -d $'{\n \"user_name\": \"gozero\",\n \"user_id\": 666\n}'\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"會得到如下返回結果","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"javascript"},"content":[{"type":"text","text":"{\n \"user_id\": 666,\n \"user_name\": \"gozero\",\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM\"\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"添加 Header","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 rest.WithJwt(accessSecret) 啓用 jwt 鑑權","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"srv.AddRoute(rest.Route{\n Method: http.MethodGet,\n Path: \"/user/data\",\n Handler: userData,\n}, rest.WithJwt(accessSecret))\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"訪問/user/data 接口返回 401 Unauthorized 鑑權不通過,添加 Authorization Header,即能正常訪問","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"curl \"http://localhost:9090/user/data?user_id=1\" \\\n -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"獲取信息","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般會將用戶的信息比如用戶 id 或者用戶名存入 jwt 的 payload 中,然後從 jwt 的 payload 中解析出我們預存的信息,即可知道本次請求時哪個用戶發起的","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func userData(w http.ResponseWriter, r *http.Request) {\n var jwt struct {\n UserId int `ctx:\"user_id\"`\n UserName string `ctx:\"user_name\"`\n }\n err := contextx.For(r.Context(), &jwt)\n if err != nil {\n httpx.Error(w, err)\n }\n httpx.WriteJson(w, http.StatusOK, struct {\n UserId int `json:\"user_id\"`\n UserName string `json:\"user_name\"`\n }{\n UserId: jwt.UserId,\n UserName: jwt.UserName,\n })\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"實現原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"jwt 鑑權的實現在 authhandler.go 中,實現原理也比較簡單,先根據 secret 解析 jwt token,驗證 token 是否有效,無效或者驗證出錯則返回 401 Unauthorized","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {\n writer := newGuardedResponseWriter(w)\n\n if err != nil {\n detailAuthLog(r, err.Error())\n } else {\n detailAuthLog(r, noDetailReason)\n }\n if callback != nil {\n callback(writer, r, err)\n }\n\n writer.WriteHeader(http.StatusUnauthorized)\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"驗證通過後把 payload 中的信息存入 http request 的 context 中","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"ctx := r.Context()\nfor k, v := range claims {\n switch k {\n case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:\n // ignore the standard claims\n default:\n ctx = context.WithValue(ctx, k, v)\n }\n}\n\nnext.ServeHTTP(w, r.WithContext(ctx))\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"中間件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"web 框架中的中間件是實現業務和非業務功能解耦的一種方式,在 web 框架中我們可以通過中間件來實現諸如鑑權、限流、熔斷等等功能,中間件的原理流程如下圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://static.gocn.vip/photo/2020/80d950fa-2712-4d95-b490-14ed72412607.png?x-oss-process=image/resize,w_900","title":null}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/14/14892a8b7d4b2d594b1387430acd1179.png","alt":"handler","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rest 框架中內置了非常豐富的中間件,在 rest/handler 路徑下,通過","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/justinas/alice","title":""},"content":[{"type":"text","text":"alice","attrs":{}}]},{"type":"text","text":"工具把所有中間件鏈接起來,當發起請求時會依次通過每一箇中間件,當滿足所有條件後最終請求才會到達真正的業務 Handler 執行業務邏輯,上面介紹的 jwt 鑑權就是通過 authHandler 來實現的,框架中內置的都是一些通用的中間件,比如業務上有一些特殊的處理我們也可以自定義中間件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func userMiddleware(next http.HandlerFunc) http.HandlerFunc {\n return func(w http.ResponseWriter, r *http.Request) {\n v := r.URL.Query().Get(\"user_id\")\n if v == \"1\" {\n w.WriteHeader(http.StatusForbidden)\n return\n }\n next.ServeHTTP(w, r)\n }\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 Use 方法註冊中間件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"srv.Use(userMiddleware)\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當發起請求 user_id 爲 1 的時候 http status 就回返回 403 Forbidden","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"路由原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"rest 框架中通過 AddRoutes 方法來註冊路由,每一個 Route 有 Method、Path 和 Handler 三個屬性,Handler 類型爲 http.HandlerFunc,添加的路由會被換成 featuredRoutes 定義如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"featuredRoutes struct {\n priority bool // 是否優先級\n jwt jwtSetting // jwt配置\n signature signatureSetting // 驗籤配置\n routes []Route // 通過AddRoutes添加的路由\n }\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"featuredRoutes 通過 engine 的 AddRoutes 添加到 engine 的 routes 屬性中","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (s *engine) AddRoutes(r featuredRoutes) {\n s.routes = append(s.routes, r)\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用 Start 方法啓動服務後會調用 engine 的 Start 方法,然後會調用 StartWithRouter 方法,該方法內通過 bindRoutes 綁定路由","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (s *engine) bindRoutes(router httpx.Router) error {\n metrics := s.createMetrics()\n\n for _, fr := range s.routes { \n if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 綁定路由\n return err\n }\n }\n\n return nil\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最終會調用 patRouter 的 Handle 方法進行綁定,patRouter 實現了 Router 接口","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"type Router interface {\n http.Handler\n Handle(method string, path string, handler http.Handler) error\n SetNotFoundHandler(handler http.Handler)\n SetNotAllowedHandler(handler http.Handler)\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"patRouter 中每一種請求方法都對應一個樹形結構,每個樹節點有兩個屬性 item 爲 path 對應的 handler,而 children 爲帶路徑參數和不帶路徑參數對應的樹節點, 定義如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"node struct {\n item interface{}\n children [2]map[string]*node\n}\n\nTree struct {\n root *node\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過 Tree 的 Add 方法把不同 path 與對應的 handler 註冊到該樹上我們通過一個圖來展示下該樹的存儲結構,比如我們定義路由如下","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"{\n Method: http.MethodGet,\n Path: \"/user\",\n Handler: userHander,\n},\n{\n Method: http.MethodGet,\n Path: \"/user/infos\",\n Handler: infosHandler,\n},\n{\n Method: http.MethodGet,\n Path: \"/user/info/:id\",\n Handler: infoHandler,\n},","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"路由存儲的樹形結構如下圖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://static.gocn.vip/photo/2020/3050b625-7a3b-414e-bf79-5d2a574049d5.png?x-oss-process=image/resize,w_900","title":null}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6d/6d663e2df56bc709742c8698236e6c85.png","alt":"tree","title":"","style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當請求來的時候會調用 patRouter 的 ServeHTTP 方法,在該方法中通過 tree.Search 方法找到對應的 handler 進行執行,否則會執行 notFound 或者 notAllow 的邏輯","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n reqPath := path.Clean(r.URL.Path)\n if tree, ok := pr.trees[r.Method]; ok {\n if result, ok := tree.Search(reqPath); ok { // 在樹中搜索對應的handler\n if len(result.Params) > 0 {\n r = context.WithPathVars(r, result.Params)\n }\n result.Item.(http.Handler).ServeHTTP(w, r)\n return\n }\n }\n\n allow, ok := pr.methodNotAllowed(r.Method, reqPath)\n if !ok {\n pr.handleNotFound(w, r)\n return\n }\n\n if pr.notAllowed != nil {\n pr.notAllowed.ServeHTTP(w, r)\n } else {\n w.Header().Set(allowHeader, allow)\n w.WriteHeader(http.StatusMethodNotAllowed)\n }\n}","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文從 rest 框架的基本使用、服務監控、jwt 鑑權、中間件和路由原理等方面進行了介紹,可見 rest 是一個功能強大的 web 框架,還有很多其他的功能由於篇幅有限後續再詳細介紹,希望本篇文章能給大家帶來幫助","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"項目地址","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/tal-tech/go-zero","title":null},"content":[{"type":"text","text":"https://github.com/tal-tech/go-zero","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"框架地址","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/tal-tech/go-zero/tree/master/rest","title":null},"content":[{"type":"text","text":"https://github.com/tal-tech/go-zero/tree/master/rest","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"文檔地址","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.yuque.com/tal-tech/go-zero/rhakzy","title":null},"content":[{"type":"text","text":"https://www.yuque.com/tal-tech/go-zero/rhakzy","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"go-zero在開源3個多月的star曲線如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ce/ce8edd21d94c0a29da17c61d358d2660.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"歡迎使用並 ","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"star","attrs":{}},{"type":"text","text":" ","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/tal-tech/go-zero","title":""},"content":[{"type":"text","text":"go-zero","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章