Go开源:Gear - 一个轻量级的、可组合扩展和高性能的Web服务框架
ah05q7w1
8年前
<h2>Gear 框架设计考量</h2> <p>Gear 是由 Teambition 开发的一个轻量级的、专注于可组合扩展和高性能的 Go 语言 Web 服务框架。</p> <p>Gear 框架在设计与实现的过程中充分参考了 Go 语言下多款知名 Web 框架,也参考了 Node.js 下的知名 Web 框架,汲取各方优秀因素,结合我们的开发实践,精心打磨而成。</p> <h2>1. Server 底层基于原生 net/http 而不是 fasthttp</h2> <p>我们在计划使用并调研 Go 语言时,各种 Web 框架相关评测中 <a href="/misc/goto?guid=4959649787265840401" rel="nofollow,noindex">fasthttp</a> 的优异表现让我们对 Go 有了很大的信心。但随着对 Go 的逐步深入学习和使用,当我们决定构建自己的 Web 框架时,还是选择了原生的 net/http 作为框架底层。</p> <p>一方面是 1.7,1.8 版 Go 的 net/http 性能已经很好了,在我的 MBP 电脑上 Gear 框架与基于 fasthttp 的 <a href="/misc/goto?guid=4959739441789492182" rel="nofollow,noindex">Iris</a> 框架(据称最快)评测比分约为 5:7 ,已经不再是当初号称的10倍、20倍差距。如果算上应用的业务逻辑的消耗,这个差距会变得更小,甚至可以忽略。并且可以预见,随着 Go 版本升级优化, net/http 的性能表现会越来越好。</p> <p>另一方面从兼容性和生命力考量,随着 Go 语言的版本升级,性能之外, net/http 的功能也会越来越强大、越来越完善(比如 HTTP/2 )。社区生态也在往这个方向聚集,之前基于 fasthttp 的很多框架都提供了 net/http 的选择(如 Iris, Echo 等)。</p> <h2>2. 通过 gear.Middleware 中间件模式扩展功能模块</h2> <p>中间件模式则是被各语言生态下 Web 框架验证的可组合扩展的最佳模式,但仍然有 <strong>级联</strong> 和 <strong>单向顺序</strong> 两个截然不同的中间件运行流程模式,Gear 选择是单向顺序运行中间件的模式(后面讲解原因)。</p> <h3>中间件的定义</h3> <p>一个 http.HandlerFunc 风格的 gear.Middleware 中间件定义如下:</p> <pre> <code class="language-go">type Middleware func(ctx *Context) error</code></pre> <p>我们用 App.Use 加载一个直接响应 Hello 的中间件到 app 应用:</p> <pre> <code class="language-go">app.Use(func(ctx *gear.Context) error { return ctx.HTML(200, "<h1>Hello, Gear!</h1>") })</code></pre> <p>一个 http.Handler 风格的 gear.Handler 中间件定义如下:</p> <pre> <code class="language-go">type Handler interface { Serve(ctx *Context) error }</code></pre> <p>我们用 App.UseHandler 加载一个 gear.Router 实例中间件到 app 应用,因为它实现了 Handler interface :</p> <pre> <code class="language-go">// 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)</code></pre> <p>另外我们也可以这样加载 gear.Handler 中间件:</p> <pre> <code class="language-go">app.Use(router.Serve)</code></pre> <p>两种形式的中间件各有其用处,但本质上都是:</p> <pre> <code class="language-go">func(ctx *gear.Context) error</code></pre> <p>类型的函数。另外我们可以看到上面 Router 示例代码中也使用了中间件:</p> <pre> <code class="language-go">router.Get(path, func(ctx *gear.Context) error { // ... })</code></pre> <p>router 本身是个 gear.Handler 形式的中间件,而它的内含逻辑却又由更多的 gear.Middleware 类型的中间件组成。Gear 内置了一些核心的中间件,包括 gear.Router 中间件, gear/logging 目录下的 logging.Logger 中间件, gear/middleware 目录下的 cors , favicon , secure , static 中间件等,都是相同的组合逻辑。</p> <p>另外 https://github.com/teambition 也有我们维护的一些 gear-xxx 的中间件,也非常欢迎开发者们参与 gear-xxx 中间件生态开发中来。</p> <p>因此, func(ctx *gear.Context) error 形态的中间件是 Gear 组合扩展的元语。它有两个核心元素 gear.Context 和 error ,其中 gear.Context 集成了 Gear 框架的所有核心开发能力(后面讲解),而返回值 error 则是框架提供的一个非常强大的错误处理机制。</p> <h3>中间件处理流程</h3> <p>一个完整 Gear 框架的 Request - Response 处理流程就是一系列中间件及其组合体的运行的流程,中间件按照引入的顺序逐一、单向运行(而非 <strong>级联</strong> ),每个中间件解决一个特定的需求,与其它任何中间件没有耦合。</p> <p>单向顺序处理流程模式的中间件最大的特点就是 cancelable ,随时可以中断,后续中间件不再运行。对于 Gear 框架来说有四种可能情况中断(cancel)或结束中间件处理流程:</p> <p>正常响应中断</p> <p>当某一个中间件调用了特定的方法(如 gear.Context 上的 ctx.End , ctx.JSON , ctx.Error 等,或者 Go 内置的 http.Redirect , http.ServeContent 等)直接往 http.ResponseWriter 写入数据时,中间件处理流程中断,后续的中间件(如果有)不再运行,请求处理流程正常结束。</p> <p>一般这样的正常结束都位于中间件流程的最末端,如 router 路由分支的最后一个中间件。但也有从中间甚至一开始就中断的情况,比如 static 中间件:</p> <pre> <code class="language-go">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")) }</code></pre> <p>当请求是静态文件资源请求时,第二个响应 "Hello, Gear!" 的中间件就不再运行。</p> <p>error 中断</p> <p>当某一个中间件返回 error 时(比如 400 参数错误,401 身份验证错误,数据库请求错误等),中间件处理流程就被中断,后续的中间件不再运行,Gear 应用会自动处理这个错误,并做出对应的 response 响应(也可以由开发者自定义错误响应结果,如响应一个包含错误信息的 JSON)。开发者不再疲于 error 的处理,可以尽情的 return error 。</p> <p>另外通过 ctx.Error(err) 和 ctx.ErrorStatus(statusCode) 主动响应错误也算 error 中断。</p> <pre> <code class="language-go">// 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> <p>上面这个示例代码包含了两种形式的 error 中断。无论哪种,其 err 都会被 Gear 框架层自动识别处理(后面详解),响应给客户端。</p> <p>与正常响应中断不同, error 中断及后面的异常中断都会导致通过 ctx.After 注入的 <strong>after hooks</strong> 逻辑被清理,不会运行(后面再详解),已设置的 response headers 也会被清理,只保留必要的 headers</p> <p>context.Context cancel 中断</p> <p>当中间件处理流还在运行,请求却因为某些原因被 context.Context 机制 cancel 时(如处理超时),中间件处理流程也会被中断,cancel 的 error 会被提取,然后按照类似 error 中断逻辑被框架自动处理。</p> <p>panic 中断</p> <p>最后就是某些中间件运行时可能出现的 panic error,它们能被框架捕获并按照类似 error 中断逻辑自动处理,错误信息中还会包含错误堆栈(Error.Stack),方便开发者在运行日志中定位错误。</p> <h2>3. 中间件的单向顺序流程控制和级联流程控制</h2> <p>Node.js 生态中知名框架 <a href="/misc/goto?guid=4958854326152945436" rel="nofollow,noindex">koa</a> 就是 <strong>级联</strong> 流程控制,其文档中的一个示例代码如下:</p> <pre> <code class="language-go">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> <p>Node.js 中最知名最经典的框架 Express 和类 koa 的 Toa 则选择了 <strong>单向顺序</strong> 流程控制模式。</p> <p>Go 语言生态中, Iris , Gin 等采用了 <strong>级联</strong> 流程控制模式。Gin 文档中的一个示例代码如下:</p> <pre> <code class="language-go">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> <p>示例代码中的 await next() 和 c.Next() 以及它们的上下文就是级联逻辑,next 包含了当前中间件所有下游中间件的逻辑。</p> <p>相对于 <strong>单向顺序</strong> , <strong>级联</strong> 唯一的优势就是在当前上下文中实现了 <strong>after</strong> 逻辑:在当前运行栈中,处理完所有后续中间件后再回来继续处理,正如上面 Logger。 Gear 框架使用 <strong>after hooks</strong> 来满足这个需求,另外也有 <strong>end hooks</strong> 来精确处理 <strong>级联</strong> 中无法实现的需求(比如上面 Logger 中间件中 c.Next() panic 了,这个日志就没了)。</p> <p>那么 <strong>级联</strong> 流程控制有什么问题呢?这里提出两点:</p> <ol> <li>next 中的逻辑是个有状态的黑盒,当前中间件可能会与这个黑盒发生状态耦合,或者说这个黑盒导致当前中间件充满不确定性的状态,比如黑盒中是否出了错误(如果出了错要另外处理的话)?是否写入了响应数据?是否会 panic?这都是无法预知的。</li> <li>无法被 context.Context 的 cancel 终止,正如上所述,这个巨大的级联黑盒无法知道运行到哪一层时 cancel 了,只能默默的往下运行。</li> </ol> <h2>4. 功能强大,完美集成 context.Context 的 gear.Context</h2> <p>gear.Context 是中间件 func(ctx *gear.Context) error 的一个核心。它完全集成了 context.Context 、 http.Request 、 http.ResponseWriter 的能力,并且提供了很多核心、便捷的方法。开发者通过调用 gear.Context 即可快速实现各种 Web 业务逻辑。</p> <p>context.Context 是 Go 语言原生的用于解决异步流程控制的方案,它主要为异步控制流程提供了完全 cancel 的能力和域内传值(request-scoped value)的能力, net/http 底层就使用了它。</p> <p>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 能力等。</p> <h2>5. 错误和异常处理</h2> <p>error 是中间件 func(ctx *gear.Context) error 的另一个核心。这个由 Golang 语言层定义的、最简单的 error interface 在 Gear 框架下,其灵活度和强大的潜力超出你的想象。</p> <p>对于 Web 服务而言, error 中必须要包含两个信息:error message 和 error code。比如一个 400 Bad request 的 error,框架能提取 status code 和 message 的话,就能自动响应给客户端了。对于实际业务需求,这个 400 错误还需要包含更具体的错误信息,甚至包含 i18n 信息。</p> <h3>gear.HTTPError , gear.Error , gear.ErrorWithStack</h3> <p>所以 Gear 框架定义了一个核心的 gear.HTTPError interface:</p> <pre> <code class="language-go">type HTTPError interface { Error() string Status() int }</code></pre> <p>gear.HTTPError interface 实现了 error interface。另外又定义了一个基础的通用的 gear.Error 类型:</p> <pre> <code class="language-go">type Error struct { Code int `json:"code"` Msg string `json:"error"` Meta interface{} `json:"meta,omitempty"` Stack string `json:"-"` }</code></pre> <p>它实现了 gear.HTTPError interface,并额外提供了 Meta 和 Stack 分别用于保存更具体的错误信息和错误堆栈,另外还有一个 String 方法:</p> <pre> <code class="language-go">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) }</code></pre> <p>gear.Error 类型既可以像传统错误一样直接响应给客户端:</p> <pre> <code class="language-go">ctx.End(err.Status(), []byte(err.Error()))</code></pre> <p>也可以用 JSON 的形式响应:</p> <pre> <code class="language-go">ctx.JSON(err.Status(), err.Error)</code></pre> <p>对于必要的(如 5xx 系列)错误会进入 App.Error 处理,这样也保留了错误堆栈。</p> <pre> <code class="language-go">func (app *App) Error(err error) { if err := ErrorWithStack(err, 4); err != nil { app.logger.Println(err.String()) } }</code></pre> <p>其中 gear.ErrorWithStack 就是创建一个包含错误堆栈的 gear.Error :</p> <pre> <code class="language-go">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> <p>从其逻辑我们可以看出,如果 val 已经是 gear.Error ,则直接使用,如果 err 没有包含 Stack ,则追加。</p> <p>一般来说, gear.Error 即可满足常规需求,Gear 的其它中间件就使用了它,比如 gear.Router 中,当路由未定义时会:</p> <pre> <code class="language-go">return ctx.Error(&Error{Code: http.StatusNotImplemented, Msg: fmt.Sprintf(`"%s" is not implemented`, ctx.Path)})</code></pre> <p>又比如 cors 中间件中,当跨域域名不允许时:</p> <pre> <code class="language-go">return ctx.Error(&gear.Error{Code: http.StatusForbidden, Msg: fmt.Sprintf("Origin: %v is not allowed", origin)})</code></pre> <h3>gear.ParseError , gear.SetOnError</h3> <p>那么, func(ctx *gear.Context) error 中的 error 是怎么变成我们期望的携带具体信息的 error 的呢?它又是怎样被自动化处理或输出自定义 JSON 错误的呢?</p> <p>我们先来个自定义的 error 类型:</p> <pre> <code class="language-go">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", } )</code></pre> <p>其中还包含了两个预定义的错误 InvalidParams 和 NotFound 。然后我们定义一个自己的 error 处理逻辑:</p> <pre> <code class="language-go">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) } })</code></pre> <p>这里我们通过 switch type 判断如果 httpError 是我们自定义的 errors.Error 类型(也就是我们预期的在业务逻辑中使用的)则用 ctx.JSON 主动处理,否则不处理,而是由框架自动处理。这个自定义 Error 在实际业务逻辑中用起来大概是:</p> <pre> <code class="language-go">// 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> <p>当然这只是一个相当简单的自定义的实现了 gear.HTTPError interface 的 error 类型。Gear 框架下完全可以自定义更复杂的,充满想象力的错误处理机制。</p> <p>框架内的任何 error interface 的错误,都会经过 gear.ParseError 处理成 gear.HTTPError interface,然后再交给 gear.SetOnError 做进一步自定义处理:</p> <pre> <code class="language-go">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> <p>从上面的处理逻辑我们可以看出, gear.HTTPError 会被直接返回,所以保留了原始错误的所有信息,如自定义的 json tag。其它错误会被加工处理,无法取得 status code 的错误则默认取 500 。</p> <p>这里再次强调,框架内捕捉的所有错误,包括 ctx.Error(error) 和 ctx.ErrorStatus(statusCode) 主动发起的,包括中间件 return error 返回的,包括 panic 的,也包括 context.Context cancel 引发的错误等,都是经过上面叙述的错误处理流程处理,响应给客户端,有必要的则输出到日志。</p> <h2> </h2> <p>来自:https://github.com/teambition/gear/blob/master/doc/design.md</p> <p> </p>