go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架,其中rest是web框架模塊,基於Go語言原生的http包進行構建,是一個輕量的,高性能的,功能完整的,簡單易用的web框架
服務創建
go-zero中創建http服務非常簡單,官方推薦使用goctl工具來生成。為了方便演示,這里通過手動創建服務,代碼如下
package main
import (
"log"
"net/http"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/service"
"github.com/tal-tech/go-zero/rest"
"github.com/tal-tech/go-zero/rest/httpx"
)
func main() {
srv, err := rest.NewServer(rest.RestConf{
Port: 9090, // 偵聽端口
ServiceConf: service.ServiceConf{
Log: logx.LogConf{Path: "./logs"}, // 日志路徑
},
})
if err != nil {
log.Fatal(err)
}
defer srv.Stop()
// 注冊路由
srv.AddRoutes([]rest.Route{
{
Method: http.MethodGet,
Path: "/user/info",
Handler: userInfo,
},
})
srv.Start() // 啟動服務
}
type User struct {
Name string `json:"name"`
Addr string `json:"addr"`
Level int `json:"level"`
}
func userInfo(w http.ResponseWriter, r *http.Request) {
var req struct {
UserId int64 `form:"user_id"` // 定義參數
}
if err := httpx.Parse(r, &req); err != nil { // 解析參數
httpx.Error(w, err)
return
}
users := map[int64]*User{
1: &User{"go-zero", "shanghai", 1},
2: &User{"go-queue", "beijing", 2},
}
httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回結果
}
通過rest.NewServer創建服務,示例配置了端口號和日志路徑,服務啟動后偵聽在9090端口,並在當前目錄下創建logs目錄同時創建各等級日志文件
然后通過srv.AddRoutes注冊路由,每個路由需要定義該路由的方法、Path和Handler,其中Handler類型為http.HandlerFunc
最后通過srv.Start啟動服務,啟動服務后通過訪問http://localhost:9090/user/info?user_id=1可以看到返回結果
{
name: "go-zero",
addr: "shanghai",
level: 1
}
到此一個簡單的http服務就創建完成了,可見使用rest創建http服務非常簡單,主要分為三個步驟:創建Server、注冊路由、啟動服務
JWT鑒權
鑒權幾乎是每個應用必備的能力,鑒權的方式很多,而jwt是其中比較簡單和可靠的一種方式,在rest框架中內置了jwt鑒權功能,jwt的原理流程如下圖

rest框架中通過rest.WithJwt(secret)啟用jwt鑒權,其中secret為服務器秘鑰是不能泄露的,因為需要使用secret來算簽名驗證payload是否被篡改,如果secret泄露客戶端就可以自行簽發token,黑客就能肆意篡改token了。我們基於上面的例子進行改造來驗證在rest中如何使用jwt鑒權
獲取jwt
第一步客戶端需要先獲取jwt,在登錄接口中實現jwt生成邏輯
srv.AddRoute(rest.Route{
Method: http.MethodPost,
Path: "/user/login",
Handler: userLogin,
})
為了演示方便,userLogin的邏輯非常簡單,主要是獲取信息然后生成jwt,獲取到的信息存入jwt payload中,然后返回jwt
func userLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
UserName string `json:"user_name"`
UserId int `json:"user_id"`
}
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
token, _ := genToken(accessSecret, map[string]interface{}{
"user_id": req.UserId,
"user_name": req.UserName,
}, accessExpire)
httpx.WriteJson(w, http.StatusOK, struct {
UserId int `json:"user_id"`
UserName string `json:"user_name"`
Token string `json:"token"`
}{
UserId: req.UserId,
UserName: req.UserName,
Token: token,
})
}
生成jwt的方法如下
func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {
now := time.Now().Unix()
claims := make(jwt.MapClaims)
claims["exp"] = now + expire
claims["iat"] = now
for k, v := range payload {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secret))
}
啟動服務后通過cURL訪問
curl -X "POST" "http://localhost:9090/user/login" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"user_name": "gozero",
"user_id": 666
}'
會得到如下返回結果
{
"user_id": 666,
"user_name": "gozero",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM"
}
添加Header
通過rest.WithJwt(accessSecret)啟用jwt鑒權
srv.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/user/data",
Handler: userData,
}, rest.WithJwt(accessSecret))
訪問/user/data接口返回 401 Unauthorized 鑒權不通過,添加Authorization Header,即能正常訪問
curl "http://localhost:9090/user/data?user_id=1" \
-H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'
獲取信息
一般會將用戶的信息比如用戶id或者用戶名存入jwt的payload中,然后從jwt的payload中解析出我們預存的信息,即可知道本次請求時哪個用戶發起的
func userData(w http.ResponseWriter, r *http.Request) {
var jwt struct {
UserId int `ctx:"user_id"`
UserName string `ctx:"user_name"`
}
err := contextx.For(r.Context(), &jwt)
if err != nil {
httpx.Error(w, err)
}
httpx.WriteJson(w, http.StatusOK, struct {
UserId int `json:"user_id"`
UserName string `json:"user_name"`
}{
UserId: jwt.UserId,
UserName: jwt.UserName,
})
}
實現原理
jwt鑒權的實現在authhandler.go中,實現原理也比較簡單,先根據secret解析jwt token,驗證token是否有效,無效或者驗證出錯則返回401 Unauthorized
func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {
writer := newGuardedResponseWriter(w)
if err != nil {
detailAuthLog(r, err.Error())
} else {
detailAuthLog(r, noDetailReason)
}
if callback != nil {
callback(writer, r, err)
}
writer.WriteHeader(http.StatusUnauthorized)
}
驗證通過后把payload中的信息存入http request的context中
ctx := r.Context()
for k, v := range claims {
switch k {
case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
// ignore the standard claims
default:
ctx = context.WithValue(ctx, k, v)
}
}
next.ServeHTTP(w, r.WithContext(ctx))
中間件
web框架中的中間件是實現業務和非業務功能解耦的一種方式,在web框架中我們可以通過中間件來實現諸如鑒權、限流、熔斷等等功能,中間件的原理流程如下圖
rest框架中內置了非常豐富的中間件,在rest/handler路徑下,通過alice工具把所有中間件鏈接起來,當發起請求時會依次通過每一個中間件,當滿足所有條件后最終請求才會到達真正的業務Handler執行業務邏輯,上面介紹的jwt鑒權就是通過authHandler來實現的。由於內置中間件比較多篇幅有限不能一一介紹,感興趣的伙伴可以自行學習,這里我們介紹一下prometheus指標收集的中間件PromethousHandler,代碼如下
func PromethousHandler(path string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := timex.Now() // 起始時間
cw := &security.WithCodeResponseWriter{Writer: w}
defer func() {
// 耗時
metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), path)
// code碼
metricServerReqCodeTotal.Inc(path, strconv.Itoa(cw.Code))
}()
next.ServeHTTP(cw, r)
})
}
}
在該中間件中,在請求開始時記錄了起始時間,在請求結束后在defer中通過prometheus的Histogram和Counter數據類型分別記錄了當前請求path的耗時和返回的code碼,此時我們通過訪問http://127.0.0.1:9101/metrics即可查看相關的指標信息
路由原理
rest框架中通過AddRoutes方法來注冊路由,每一個Route有Method、Path和Handler三個屬性,Handler類型為http.HandlerFunc,添加的路由會被換成featuredRoutes定義如下
featuredRoutes struct {
priority bool // 是否優先級
jwt jwtSetting // jwt配置
signature signatureSetting // 驗簽配置
routes []Route // 通過AddRoutes添加的路由
}
featuredRoutes通過engine的AddRoutes添加到engine的routes屬性中
func (s *engine) AddRoutes(r featuredRoutes) {
s.routes = append(s.routes, r)
}
調用Start方法啟動服務后會調用engine的Start方法,然后會調用StartWithRouter方法,該方法內通過bindRoutes綁定路由
func (s *engine) bindRoutes(router httpx.Router) error {
metrics := s.createMetrics()
for _, fr := range s.routes {
if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 綁定路由
return err
}
}
return nil
}
最終會調用patRouter的Handle方法進行綁定,patRouter實現了Router接口
type Router interface {
http.Handler
Handle(method string, path string, handler http.Handler) error
SetNotFoundHandler(handler http.Handler)
SetNotAllowedHandler(handler http.Handler)
}
patRouter中每一種請求方法都對應一個樹形結構,每個樹節點有兩個屬性item為path對應的handler,而children為帶路徑參數和不帶路徑參數對應的樹節點, 定義如下:
node struct {
item interface{}
children [2]map[string]*node
}
Tree struct {
root *node
}
通過Tree的Add方法把不同path與對應的handler注冊到該樹上我們通過一個圖來展示下該樹的存儲結構,比如我們定義路由如下
{
Method: http.MethodGet,
Path: "/user",
Handler: userHander,
},
{
Method: http.MethodGet,
Path: "/user/infos",
Handler: infosHandler,
},
{
Method: http.MethodGet,
Path: "/user/info/:id",
Handler: infoHandler,
},
路由存儲的樹形結構如下圖

當請求來的時候會調用patRouter的ServeHTTP方法,在該方法中通過tree.Search方法找到對應的handler進行執行,否則會執行notFound或者notAllow的邏輯
func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reqPath := path.Clean(r.URL.Path)
if tree, ok := pr.trees[r.Method]; ok {
if result, ok := tree.Search(reqPath); ok { // 在樹中搜索對應的handler
if len(result.Params) > 0 {
r = context.WithPathVars(r, result.Params)
}
result.Item.(http.Handler).ServeHTTP(w, r)
return
}
}
allow, ok := pr.methodNotAllowed(r.Method, reqPath)
if !ok {
pr.handleNotFound(w, r)
return
}
if pr.notAllowed != nil {
pr.notAllowed.ServeHTTP(w, r)
} else {
w.Header().Set(allowHeader, allow)
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
總結
本文從整體上介紹了rest,通過該篇文章能夠基本了解rest的設計和主要功能,其中中間件部分是重點,里面集成了各種服務治理相關的功能,並且是自動集成的不需要我們做任何配置,其他功能比如參數自動效驗等功能由於篇幅有限在這里就不做介紹了,感興趣的朋友可以自行查看官方文檔進行學習。go-zero中不光有http協議還提供了rpc協議和各種提高性能和開發效率的工具,是一款值得我們深入學習和研究的框架。
項目地址
https://github.com/tal-tech/go-zero
如果覺得文章不錯,歡迎 github 點個 star 🤝