欢迎访问昆山宝鼎软件有限公司网站! 设为首页 | 网站地图 | XML | RSS订阅 | 宝鼎邮箱 | 后台管理


新闻资讯

MENU

软件开发知识

Go 语言 Web 服务框架 Gear 设计考量

点击: 次  来源: 时间:2017-03-13

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(),
  })
}

上面这个示例代码包含了两种形式的 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;
});


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)
  }
}


示例代码中的 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 能力等。

    更多的方法请参考 https://godoc.org/github.com/teambition/gear#Context

    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
    }

    从其逻辑我们可以看出,如果 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)
    }

    当然这只是一个相当简单的自定义的实现了 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
      }
    }

    从上面的处理逻辑我们可以看出,gear.HTTPError 会被直接返回,所以保留了原始错误的所有信息,如自定义的 json tag。其它错误会被加工处理,无法取得 status code 的错误则默认取 500。

    这里再次强调,框架内捕捉的所有错误,包括 ctx.Error(error) 和 ctx.ErrorStatus(statusCode) 主动发起的,包括中间件 return error 返回的,包括 panic 的,也包括 context.Context cancel 引发的错误等,都是经过上面叙述的错误处理流程处理,响应给客户端,有必要的则输出到日志。