-
Notifications
You must be signed in to change notification settings - Fork 5
Home
开始接触后端开发是从nodejs开始,最开始使用的框架是express,后来陆续接触了其它的框架,觉得最熟悉的还是koa。使用golang做后端开发时,对比使用过gin,echo以及iris三个框架,它们的用法都比较类似(都支持中间件,中间件的处理也类似),但是在开发过程中还是钟情于koa的处理方式,失败则throw error,成功则将响应数据赋值至ctx.body,简单易懂。
造一个新的轮子的时候,首先考虑的是满足自己的需求,弱水三千只取一瓢饮,对我来说,新轮子的满足我所需要的一瓢则是重中之重。
- 请求经过中间件的处理方式为由外至内,响应时再由内至外
- 所有的处理函数都一致(参数、类型等),每个处理函数都有可能是别的前置中间件
- 请求处理成功时,直接赋值至Body(interface{}),由中间件将interface{}序列化(如json,xml等)
- 请求处理失败时,返回error,由中间件将error转换为相应的HTTP响应(golang中的error为interface,可自定义相应的Error实例)
cod参考koa的实现,能够简单的添加各类中间件,中间件的执行也和koa一样,如下图所示的洋葱图,从外层往内层递进,再从内层返回外层(也可以未至最内层则直接往上返回)。
下面我们先看一下简单的处理成功与出错的例子:
package main
import (
"bytes"
"errors"
"github.com/vicanso/cod"
)
func main() {
d := cod.New()
d.GET("/", func(c *cod.Context) (err error) {
c.BodyBuffer = bytes.NewBufferString("hello world")
return
})
d.GET("/error", func(c *cod.Context) (err error) {
err = errors.New("my error")
return
})
d.ListenAndServe(":7001")
}
如代码所示,处理过程非常简单,响应数据由BodyBuffer中获取,如果流程出错,直接返回Error则可。不过例子中的与koa
的处理还是有所差距,koa中返回数据时可以随意的指定各种Object(转换为json),而在golang中因为是强类型,所以需要使用interface{}的形式返回,再通过中间件将各类不同的struct转换至BodyBuffer,以及设置Content-Type
。
在cod中有两类中间件是必须了解的,一是将Context.Body(interface{})转换至对应的响应数据的Responder中间件,另外一个就是将Error转换至对应的响应数据的Error Handler中间件。在详解这两类中间件之前,我们首先来了解一下cod的中间件的一些基本概念。
cod中间件的处理函数是Handler func(*Context) error
,可以通过Use方法添加至全局的中间件,也可单独添加至单一组或单一的路由处理。中间件处理也非常简单,如果出错,返回Error(后续的处理函数不再执行)。在当前函数中已完成处理,则无需要调用Context.Next()
,需要转至下一处理函数,则调用Context.Next()
则可。
package main
import (
"bytes"
"log"
"time"
"github.com/vicanso/cod"
)
func main() {
d := cod.New()
// logger
d.Use(func(c *cod.Context) (err error) {
err = c.Next()
rt := c.GetHeader("X-Response-Time")
log.Printf("%s %s - %s\n", c.Request.Method, c.Request.RequestURI, rt)
return
})
// x-response-time
d.Use(func(c *cod.Context) (err error) {
start := time.Now()
err = c.Next()
c.SetHeader("X-Response-Time", time.Since(start).String())
return
})
d.GET("/", func(c *cod.Context) (err error) {
c.BodyBuffer = bytes.NewBufferString("hello world")
return
})
d.ListenAndServe(":7001")
}
HTTP的响应主要分三部分,HTTP响应状态码,HTTP响应头以及HTTP响应体。前两部分比较简单,格式统一,但是HTTP响应体对于不同的应用有所不同。在cod的处理中,会将BodyBuffer的相应数据在响应时作为HTTP响应体输出。在实际应用中,有些会使用json,有些是xml或者自定义的响应格式。因此在cod是提供了Body(interface{})属性,允许将响应数据赋值至此字段,再由相应的中间件转换为对应的BodyBuffer以及设置Content-Type
。
在实际使用中,HTTP接口的响应主要还是以json
为主,因此cod-responder提供了将Body转换为对应的BodyBuffer(json)的处理,主要的处理如下:
// New create a responder
func New(config Config) cod.Handler {
skipper := config.Skipper
if skipper == nil {
skipper = cod.DefaultSkipper
}
marshal := standardJSON.Marshal
if config.Fastest {
marshal = fastJSON.Marshal
}
return func(c *cod.Context) (err error) {
if skipper(c) {
return c.Next()
}
err = c.Next()
if err != nil {
return
}
// 如果已设置了BodyBuffer,则已生成好响应数据,跳过
if c.BodyBuffer != nil {
return
}
if c.StatusCode == 0 && c.Body == nil {
// 如果status code 与 body 都为空,则为非法响应
err = errInvalidResponse
return
}
// 如果body是reader,则跳过
if c.IsReaderBody() {
return
}
ct := cod.HeaderContentType
hadContentType := false
// 判断是否已设置响应头的Content-Type
if c.GetHeader(ct) != "" {
hadContentType = true
}
statusCode := c.StatusCode
if statusCode == 0 {
statusCode = http.StatusOK
}
var body []byte
if c.Body != nil {
switch c.Body.(type) {
case string:
if !hadContentType {
c.SetHeader(ct, cod.MIMETextPlain)
}
body = []byte(c.Body.(string))
case []byte:
if !hadContentType {
c.SetHeader(ct, cod.MIMEBinary)
}
body = c.Body.([]byte)
default:
// 转换为json
buf, err := marshal(c.Body)
if err != nil {
c.Cod().EmitError(c, err)
statusCode = http.StatusInternalServerError
he := hes.NewWithErrorStatusCode(err, statusCode)
he.Exception = true
c.SetHeader(ct, cod.MIMEApplicationJSON)
body = he.ToJSON()
err = nil
} else {
if !hadContentType {
c.SetHeader(ct, cod.MIMEApplicationJSON)
}
body = buf
}
}
}
c.BodyBuffer = bytes.NewBuffer(body)
c.StatusCode = statusCode
return nil
}
}
代码的处理步骤如下:
1、前置判断是否跳过中间件,主要判断条件为:是否出错,或者已设置BodyBuffer
(表示已完成响应数据的处理)或者Body为Reader(以流的形式输出响应数据)。
2、如果Body的类型为string,则将string转换为bytes,如果未设置数据类型,则设置为text/plain; charset=UTF-8
3、如果Body的类型为[]byte,如果未设置数据类型,则设置为application/octet-stream
4、对于其它类型,则使用json.Marshal
转换为对应的[]byte,如果未设置数据类型,则设置为application/json; charset=UTF-8
通过此中间件,在开发时可以简单的将各种struct对象,map对象以json
的形式返回,无需要过多关心数据转换流程,方便快捷。如果应用需要以xml等其它形式返回,则可自己编写自定义中间件来做转换处理。
cod中默认的Error处理只是简单的输出err.Error()
,而且状态码也只是简单的使用StatusInternalServerError
,无法满足应用中的各类定制的出错方式。因此一般建议编写自定义的出错处理中间件,根据自定义的Error对象生成相应的出错响应数据。如cod-error-handler则针对返回的hes对象对应生成相应的状态码,响应类型以及响应数据(json):
// New create a error handler
func New(config Config) cod.Handler {
skipper := config.Skipper
if skipper == nil {
skipper = cod.DefaultSkipper
}
return func(c *cod.Context) error {
if skipper(c) {
return c.Next()
}
err := c.Next()
// 如果没有出错,直接返回
if err == nil {
return nil
}
he, ok := err.(*hes.Error)
if !ok {
he = hes.Wrap(err)
he.StatusCode = http.StatusInternalServerError
he.Exception = true
he.Category = errErrorHandlerCategory
}
c.StatusCode = he.StatusCode
if config.ResponseType == "json" {
buf := he.ToJSON()
c.BodyBuffer = bytes.NewBuffer(buf)
c.SetHeader(cod.HeaderContentType, cod.MIMEApplicationJSON)
} else {
c.BodyBuffer = bytes.NewBufferString(he.Error())
c.SetHeader(cod.HeaderContentType, cod.MIMETextPlain)
}
return nil
}
}
cod提供更简单方便的WEB开发体验,主要实现的代码非常简单,更多的功能都依赖于各类中间件。更多的中间件以及更多的文档说明请查阅github项目中的说明。