Go開源:Gear - 一個輕量級的、可組合擴展和高性能的Web服務框架

Gear 框架設計考量

Gear 是由 Teambition 開發的一個輕量級的、專注于可組合擴展和高性能的 Go 語言 Web 服務框架。

Gear 框架在設計與實現的過程中充分參考了 Go 語言下多款知名 Web 框架,也參考了 Node.js 下的知名 Web 框架,汲取各方優秀因素,結合我們的開發實踐,精心打磨而成。

1. Server 底層基于原生 net/http 而不是 fasthttp

我們在計劃使用并調研 Go 語言時,各種 Web 框架相關評測中 fasthttp 的優異表現讓我們對 Go 有了很大的信心。但隨著對 Go 的逐步深入學習和使用,當我們決定構建自己的 Web 框架時,還是選擇了原生的 net/http 作為框架底層。

一方面是 1.7,1.8 版 Go 的 net/http 性能已經很好了,在我的 MBP 電腦上 Gear 框架與基于 fasthttp 的 Iris 框架(據稱最快)評測比分約為 5:7 ,已經不再是當初號稱的10倍、20倍差距。如果算上應用的業務邏輯的消耗,這個差距會變得更小,甚至可以忽略。并且可以預見,隨著 Go 版本升級優化, net/http 的性能表現會越來越好。

另一方面從兼容性和生命力考量,隨著 Go 語言的版本升級,性能之外, net/http 的功能也會越來越強大、越來越完善(比如 HTTP/2 )。社區生態也在往這個方向聚集,之前基于 fasthttp 的很多框架都提供了 net/http 的選擇(如 Iris, Echo 等)。

2. 通過 gear.Middleware 中間件模式擴展功能模塊

中間件模式則是被各語言生態下 Web 框架驗證的可組合擴展的最佳模式,但仍然有 級聯單向順序 兩個截然不同的中間件運行流程模式,Gear 選擇是單向順序運行中間件的模式(后面講解原因)。

中間件的定義

一個 http.HandlerFunc 風格的 gear.Middleware 中間件定義如下:

type Middleware func(ctx *Context) error

我們用 App.Use 加載一個直接響應 Hello 的中間件到 app 應用:

app.Use(func(ctx *gear.Context) error {
  return ctx.HTML(200, "<h1>Hello, Gear!</h1>")
})

一個 http.Handler 風格的 gear.Handler 中間件定義如下:

type Handler interface {
  Serve(ctx *Context) error
}

我們用 App.UseHandler 加載一個 gear.Router 實例中間件到 app 應用,因為它實現了 Handler interface :

// https://github.com/teambition/gear/blob/master/example/http2/app.go
router := gear.NewRouter()
router.Get("/", func(ctx *gear.Context) error {
  ctx.Res.Push("/hello.css", &http.PushOptions{Method: "GET"})
  return ctx.HTML(200, htmlBody)
})
router.Get("/hello.css", func(ctx *gear.Context) error {
  ctx.Type("text/css")
  return ctx.End(200, []byte(pushBody))
})
app.UseHandler(router)

另外我們也可以這樣加載 gear.Handler 中間件:

app.Use(router.Serve)

兩種形式的中間件各有其用處,但本質上都是:

func(ctx *gear.Context) error

類型的函數。另外我們可以看到上面 Router 示例代碼中也使用了中間件:

router.Get(path, func(ctx *gear.Context) error {
  // ...
})

router 本身是個 gear.Handler 形式的中間件,而它的內含邏輯卻又由更多的 gear.Middleware 類型的中間件組成。Gear 內置了一些核心的中間件,包括 gear.Router 中間件, gear/logging 目錄下的 logging.Logger 中間件, gear/middleware 目錄下的 cors , favicon , secure , static 中間件等,都是相同的組合邏輯。

另外 https://github.com/teambition 也有我們維護的一些 gear-xxx 的中間件,也非常歡迎開發者們參與 gear-xxx 中間件生態開發中來。

因此, func(ctx *gear.Context) error 形態的中間件是 Gear 組合擴展的元語。它有兩個核心元素 gear.Context 和 error ,其中 gear.Context 集成了 Gear 框架的所有核心開發能力(后面講解),而返回值 error 則是框架提供的一個非常強大的錯誤處理機制。

中間件處理流程

一個完整 Gear 框架的 Request - Response 處理流程就是一系列中間件及其組合體的運行的流程,中間件按照引入的順序逐一、單向運行(而非 級聯 ),每個中間件解決一個特定的需求,與其它任何中間件沒有耦合。

單向順序處理流程模式的中間件最大的特點就是 cancelable ,隨時可以中斷,后續中間件不再運行。對于 Gear 框架來說有四種可能情況中斷(cancel)或結束中間件處理流程:

正常響應中斷

當某一個中間件調用了特定的方法(如 gear.Context 上的 ctx.End , ctx.JSON , ctx.Error 等,或者 Go 內置的 http.Redirect , http.ServeContent 等)直接往 http.ResponseWriter 寫入數據時,中間件處理流程中斷,后續的中間件(如果有)不再運行,請求處理流程正常結束。

一般這樣的正常結束都位于中間件流程的最末端,如 router 路由分支的最后一個中間件。但也有從中間甚至一開始就中斷的情況,比如 static 中間件:

func main() {
  app := gear.New()
  app.Use(static.New(static.Options{
    Root:        "./testdata",
    Prefix:      "/",
    StripPrefix: false,
  }))
  app.Use(func(ctx *gear.Context) error {
    return ctx.HTML(200, "<h1>Hello, Gear!</h1>")
  })
  app.Error(app.Listen(":3000"))
}

當請求是靜態文件資源請求時,第二個響應 "Hello, Gear!" 的中間件就不再運行。

error 中斷

當某一個中間件返回 error 時(比如 400 參數錯誤,401 身份驗證錯誤,數據庫請求錯誤等),中間件處理流程就被中斷,后續的中間件不再運行,Gear 應用會自動處理這個錯誤,并做出對應的 response 響應(也可以由開發者自定義錯誤響應結果,如響應一個包含錯誤信息的 JSON)。開發者不再疲于 error 的處理,可以盡情的 return error 。

另外通過 ctx.Error(err) 和 ctx.ErrorStatus(statusCode) 主動響應錯誤也算 error 中斷。

// https://github.com/seccom/kpass/blob/master/pkg/api/user.go
func (a User) Login(ctx gear.Context) (err error) {
  body := new(tplUserLogin)
  if err = ctx.ParseBody(body); err != nil {
    return
  }

var user *schema.User if user, err = a.user.CheckLogin(body.ID, body.Pass); err != nil { return }

token, err := auth.NewToken(user.ID) if err != nil { return ctx.Error(err) } ctx.Set(gear.HeaderPragma, "no-cache") ctx.Set(gear.HeaderCacheControl, "no-store") return ctx.JSON(200, map[string]interface{}{ "access_token": token, "token_type": "Bearer", "expires_in": auth.JWT().GetExpiresIn().Seconds(), }) }</code></pre>

上面這個示例代碼包含了兩種形式的 error 中斷。無論哪種,其 err 都會被 Gear 框架層自動識別處理(后面詳解),響應給客戶端。

與正常響應中斷不同, error 中斷及后面的異常中斷都會導致通過 ctx.After 注入的 after hooks 邏輯被清理,不會運行(后面再詳解),已設置的 response headers 也會被清理,只保留必要的 headers

context.Context cancel 中斷

當中間件處理流還在運行,請求卻因為某些原因被 context.Context 機制 cancel 時(如處理超時),中間件處理流程也會被中斷,cancel 的 error 會被提取,然后按照類似 error 中斷邏輯被框架自動處理。

panic 中斷

最后就是某些中間件運行時可能出現的 panic error,它們能被框架捕獲并按照類似 error 中斷邏輯自動處理,錯誤信息中還會包含錯誤堆棧(Error.Stack),方便開發者在運行日志中定位錯誤。

3. 中間件的單向順序流程控制和級聯流程控制

Node.js 生態中知名框架 koa 就是 級聯 流程控制,其文檔中的一個示例代碼如下:

const app = new Koa();

app.use(async (ctx, next) => { try { await next(); } catch (err) { ctx.body = { message: err.message }; ctx.status = err.status || 500; } });

app.use(async ctx => { const user = await User.getById(ctx.session.userid); ctx.body = user; });</code></pre>

Node.js 中最知名最經典的框架 Express 和類 koa 的 Toa 則選擇了 單向順序 流程控制模式。

Go 語言生態中, Iris , Gin 等采用了 級聯 流程控制模式。Gin 文檔中的一個示例代碼如下:

func Logger() gin.HandlerFunc {
  return func(c *gin.Context) {
    t := time.Now()
    // Set example variable
    c.Set("example", "12345")
    // before request
    c.Next()
    // after request
    latency := time.Since(t)
    log.Print(latency)

// access the status we are sending
status := c.Writer.Status()
log.Println(status)

} }</code></pre>

示例代碼中的 await next() 和 c.Next() 以及它們的上下文就是級聯邏輯,next 包含了當前中間件所有下游中間件的邏輯。

相對于 單向順序級聯 唯一的優勢就是在當前上下文中實現了 after 邏輯:在當前運行棧中,處理完所有后續中間件后再回來繼續處理,正如上面 Logger。 Gear 框架使用 after hooks 來滿足這個需求,另外也有 end hooks 來精確處理 級聯 中無法實現的需求(比如上面 Logger 中間件中 c.Next() panic 了,這個日志就沒了)。

那么 級聯 流程控制有什么問題呢?這里提出兩點:

  1. next 中的邏輯是個有狀態的黑盒,當前中間件可能會與這個黑盒發生狀態耦合,或者說這個黑盒導致當前中間件充滿不確定性的狀態,比如黑盒中是否出了錯誤(如果出了錯要另外處理的話)?是否寫入了響應數據?是否會 panic?這都是無法預知的。
  2. 無法被 context.Context 的 cancel 終止,正如上所述,這個巨大的級聯黑盒無法知道運行到哪一層時 cancel 了,只能默默的往下運行。

4. 功能強大,完美集成 context.Context 的 gear.Context

gear.Context 是中間件 func(ctx *gear.Context) error 的一個核心。它完全集成了 context.Context 、 http.Request 、 http.ResponseWriter 的能力,并且提供了很多核心、便捷的方法。開發者通過調用 gear.Context 即可快速實現各種 Web 業務邏輯。

context.Context 是 Go 語言原生的用于解決異步流程控制的方案,它主要為異步控制流程提供了完全 cancel 的能力和域內傳值(request-scoped value)的能力, net/http 底層就使用了它。

gear.Context 充分利用了 context.Context ,并實現了它的 interface,可以直接當成 context.Context 使用。也提供了 ctx.WithCancel , ctx.WithDeadline , ctx.WithTimeout , ctx.WithValue 等快速創建子級 context.Context 的便捷方法。還提供了 ctx.Cancel 主動完全退出中間件處理流程的方法。還有 App 級設置 的中間件處理流程 timeout cancel 能力,甚至是 ctx.Timing 針對某個異步處理邏輯的 timeout cancel 能力等。

5. 錯誤和異常處理

error 是中間件 func(ctx *gear.Context) error 的另一個核心。這個由 Golang 語言層定義的、最簡單的 error interface 在 Gear 框架下,其靈活度和強大的潛力超出你的想象。

對于 Web 服務而言, error 中必須要包含兩個信息:error message 和 error code。比如一個 400 Bad request 的 error,框架能提取 status code 和 message 的話,就能自動響應給客戶端了。對于實際業務需求,這個 400 錯誤還需要包含更具體的錯誤信息,甚至包含 i18n 信息。

gear.HTTPError , gear.Error , gear.ErrorWithStack

所以 Gear 框架定義了一個核心的 gear.HTTPError interface:

type HTTPError interface {
  Error() string
  Status() int
}

gear.HTTPError interface 實現了 error interface。另外又定義了一個基礎的通用的 gear.Error 類型:

type Error struct {
  Code  int         `json:"code"`
  Msg   string      `json:"error"`
  Meta  interface{} `json:"meta,omitempty"`
  Stack string      `json:"-"`
}

它實現了 gear.HTTPError interface,并額外提供了 Meta 和 Stack 分別用于保存更具體的錯誤信息和錯誤堆棧,另外還有一個 String 方法:

func (err *Error) String() string {
  switch v := err.Meta.(type) {
  case []byte:
    err.Meta = string(v)
  }
  return fmt.Sprintf(`Error{Code:%3d, Msg:"%s", Meta:%#v, Stack:"%s"}`,
    err.Code, err.Msg, err.Meta, err.Stack)
}

gear.Error 類型既可以像傳統錯誤一樣直接響應給客戶端:

ctx.End(err.Status(), []byte(err.Error()))

也可以用 JSON 的形式響應:

ctx.JSON(err.Status(), err.Error)

對于必要的(如 5xx 系列)錯誤會進入 App.Error 處理,這樣也保留了錯誤堆棧。

func (app *App) Error(err error) {
  if err := ErrorWithStack(err, 4); err != nil {
    app.logger.Println(err.String())
  }
}

其中 gear.ErrorWithStack 就是創建一個包含錯誤堆棧的 gear.Error :

func ErrorWithStack(val interface{}, skip ...int) Error {
  var err Error
  if IsNil(val) {
    return err
  }

switch v := val.(type) { case *Error: err = v case error: e := ParseError(v) err = &Error{e.Status(), e.Error(), nil, ""} case string: err = &Error{500, v, nil, ""} default: err = &Error{500, fmt.Sprintf("%#v", v), nil, ""} }

if err.Stack == "" { buf := make([]byte, 2048) buf = buf[:runtime.Stack(buf, false)] s := 1 if len(skip) != 0 { s = skip[0] } err.Stack = pruneStack(buf, s) } return err }</code></pre>

從其邏輯我們可以看出,如果 val 已經是 gear.Error ,則直接使用,如果 err 沒有包含 Stack ,則追加。

一般來說, gear.Error 即可滿足常規需求,Gear 的其它中間件就使用了它,比如 gear.Router 中,當路由未定義時會:

return ctx.Error(&Error{Code: http.StatusNotImplemented,
  Msg: fmt.Sprintf(`"%s" is not implemented`, ctx.Path)})

又比如 cors 中間件中,當跨域域名不允許時:

return ctx.Error(&gear.Error{Code: http.StatusForbidden,
  Msg: fmt.Sprintf("Origin: %v is not allowed", origin)})

gear.ParseError , gear.SetOnError

那么, func(ctx *gear.Context) error 中的 error 是怎么變成我們期望的攜帶具體信息的 error 的呢?它又是怎樣被自動化處理或輸出自定義 JSON 錯誤的呢?

我們先來個自定義的 error 類型:

package errors
// Error represents an error used by application.
type Error struct {
  Code int    `json:"code"`
  Err  string `json:"error"`
  Msg  string `json:"message"`
}
// Status is to implement gear.HTTPError interface.
func (e Error) Status() int {
  return e.Code
}
// Error is to implement gear.HTTPError interface.
func (e Error) Error() string {
  return e.Err
}
// SetMsg returns a new error with given new message.
func (e Error) SetMsg(params ...string) Error {
  if len(params) != 0 {
    e.Msg = strings.Join(params, ", ")
  }
  return e
}
// Predefined errors.
var (
  InvalidParams = Error{
    Code: http.StatusBadRequest,
    Err:  "Invalid Parameters",
  }
  NotFound = Error{
    Code: http.StatusNotFound,
    Err:  "Resource Not Found",
  }
)

其中還包含了兩個預定義的錯誤 InvalidParams 和 NotFound 。然后我們定義一個自己的 error 處理邏輯:

app.Set(gear.SetOnError, func(ctx *gear.Context, httpError gear.HTTPError) {
  switch err := httpError.(type) {
  case errors.Error, *errors.Error:
    ctx.JSON(err.Code, err)
  }
})

這里我們通過 switch type 判斷如果 httpError 是我們自定義的 errors.Error 類型(也就是我們預期的在業務邏輯中使用的)則用 ctx.JSON 主動處理,否則不處理,而是由框架自動處理。這個自定義 Error 在實際業務邏輯中用起來大概是:

// GET /workspaces/:_workspaceId
func (w Workspace) GetByID(ctx gear.Context) error {
  workspaceID := ctx.Param("_workspaceId")
  if !valid.IsMongoID(workspaceID) {
    return errors.InvalidParams.SetMsg("_workspaceId")
  }

workspace, err := w.Model.FindByID(workspaceID) if err != nil { return err } if workspace == nil { return errors.NotFound.SetMsg("workspace") } return ctx.JSON(http.StatusOK, workspace) }</code></pre>

當然這只是一個相當簡單的自定義的實現了 gear.HTTPError interface 的 error 類型。Gear 框架下完全可以自定義更復雜的,充滿想象力的錯誤處理機制。

框架內的任何 error interface 的錯誤,都會經過 gear.ParseError 處理成 gear.HTTPError interface,然后再交給 gear.SetOnError 做進一步自定義處理:

func ParseError(e error, code ...int) HTTPError {
  if IsNil(e) {
    return nil
  }

switch v := e.(type) { case HTTPError: return v case *textproto.Error: return &Error{v.Code, v.Msg, nil, ""} default: err := &Error{500, e.Error(), nil, ""} if len(code) > 0 && code[0] > 0 { err.Code = code[0] } return err } }</code></pre>

從上面的處理邏輯我們可以看出, gear.HTTPError 會被直接返回,所以保留了原始錯誤的所有信息,如自定義的 json tag。其它錯誤會被加工處理,無法取得 status code 的錯誤則默認取 500 。

這里再次強調,框架內捕捉的所有錯誤,包括 ctx.Error(error) 和 ctx.ErrorStatus(statusCode) 主動發起的,包括中間件 return error 返回的,包括 panic 的,也包括 context.Context cancel 引發的錯誤等,都是經過上面敘述的錯誤處理流程處理,響應給客戶端,有必要的則輸出到日志。

 

來自:https://github.com/teambition/gear/blob/master/doc/design.md

 

 本文由用戶 ah05q7w1 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!