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 了,這個日志就沒了)。
那么 級聯 流程控制有什么問題呢?這里提出兩點:
- next 中的邏輯是個有狀態的黑盒,當前中間件可能會與這個黑盒發生狀態耦合,或者說這個黑盒導致當前中間件充滿不確定性的狀態,比如黑盒中是否出了錯誤(如果出了錯要另外處理的話)?是否寫入了響應數據?是否會 panic?這都是無法預知的。
- 無法被 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