在对 context 的封装中,我们只是将 request、response 结构直接放入 context 结构体中,对应的方法并没有很好的封装。
函数封装并不是一件很简单、很随意的事情。相反,如何封装出易用、可读性高的函数是非常需要精心考量的,框架中每个函数的参数、返回值、命名,都代表着我们作为作者在某个事情上的思考。想要针对某个功能,封装出一系列比较完美的接口,更要我们从系统性的角度思考。
如何封装请求和返回
我们的目标是尽量在 context 这个数据结构中,封装“读取请求数据”和“封装返回数据”中的方法。
读取请求数据
头部:业务无关、传输相关的信息,如请求地址、编码格式、缓存时长
body:与业务相关的信息
Header 信息中,包含 HTTP 的一些基础信息,比如请求地址、请求方法、请求 IP、请求域名、Cookie 信息等,是经常读取使用的,为了方便,我们需要一一提供封装。
而另外一些更细节的内容编码格式、缓存时长等,由于涉及的 HTTP 协议细节内容比较多,我们很难将每个细节都封装出来,但是它们都是以 key=value 的形式传递到服务端的,所以这里也考虑封装一个通用的方法。
Body 信息中,HTTP 是已经以某种形式封装好的,可能是 JSON 格式、XML 格式,也有可能是 Form 表单格式。其中 Form 表单注意一下,它可能包含 File 文件,请求参数和返回值肯定和其他的 Form 表单字段是不一样的,需要我们对其单独封装一个函数。
封装返回数据
Header 头部,我们经常要设置的是返回状态码和 Cookie,所以单独为其封装。其他的 Header 同样是 key=value 形式设置的,设置一个通用的方法即可。
返回数据的 Body 体是有不同形式的,比如 JSON、JSONP、XML、HTML 或者其他文本格式,所以我们要针对不同的 Body 体形式,进行不同的封装。
定义接口让封装更明确
对于比较完整的功能模块,先定义接口,再具体实现,这样好处是实现解耦、开发清晰。
定义两个接口,IRequest 和 IResponse,分别对应“读取请求数据”和“封装返回数据” 这两个功能模块。
IRequest 接口定义
// 代表请求包含的方法
type IRequest interface {
// 请求地址 url 中带的参数
// 形如: foo.com?a=1&b=bar&c[]=bar
QueryInt(key string, def int) (int, bool)
QueryInt64(key string, def int64) (int64, bool)
QueryFloat64(key string, def float64) (float64, bool)
QueryFloat32(key string, def float32) (float32, bool)
QueryBool(key string, def bool) (bool, bool)
QueryString(key string, def string) (string, bool)
QueryStringSlice(key string, def []string) ([]string, bool)
Query(key string) interface{}
// 路由匹配中带的参数
// 形如 /book/:id
ParamInt(key string, def int) (int, bool)
ParamInt64(key string, def int64) (int64, bool)
ParamFloat64(key string, def float64) (float64, bool)
ParamFloat32(key string, def float32) (float32, bool)
ParamBool(key string, def bool) (bool, bool)
ParamString(key string, def string) (string, bool)
Param(key string) interface{}
// form 表单中带的参数
FormInt(key string, def int) (int, bool)
FormInt64(key string, def int64) (int64, bool)
FormFloat64(key string, def float64) (float64, bool)
FormFloat32(key string, def float32) (float32, bool)
FormBool(key string, def bool) (bool, bool)
FormString(key string, def string) (string, bool)
FormStringSlice(key string, def []string) ([]string, bool)
FormFile(key string) (*multipart.FileHeader, error)
Form(key string) interface{}
// json body
BindJson(obj interface{}) error
// xml body
BindXml(obj interface{}) error
// 其他格式
GetRawData() ([]byte, error)
// 基础信息
Uri() string
Method() string
Host() string
ClientIp() string
// header
Headers() map[string][]string
Header(key string) (string, bool)
// cookie
Cookies() map[string]string
Cookie(key string) (string, bool)
}
QueryXXX表示从URL后缀中获取参数,ParamXXX表示从路由匹配获取参数,FormXXX表示从Body的form表单获取参数。
这三类方法统一了参数与返回值:
- 参数: key和默认值
- 返回值:匹配值和bool
具体实现
- Query请求
// 获取请求地址中所有参数
func (ctx *Context) QueryAll() map[string][]string {
if ctx.request != nil {
return map[string][]string(ctx.request.URL.Query())
}
return map[string][]string{}
}
// 获取 Int 类型的请求参数
func (ctx *Context) QueryInt(key string, def int) (int, bool) {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
if len(vals) > 0 {
// 使用 cast 库将 string 转换为 Int
return cast.ToInt(vals[0]), true
}
}
return def, false
}
- Param 请求
找到路由节点后,根据uri的分段,网上寻找父节点,找到其中的参数,并存储到ctx中。
// 将 uri 解析为 params
func (n *node) parseParamsFromEndNode(uri string) map[string]string {
ret := map[string]string{}
segments := strings.Split(uri, "/")
cnt := len(segments)
cur := n
for i := cnt - 1; i >= 0; i-- {
if cur.segment == "" {
break
}
// 如果是通配符节点
if isWildSegment(cur.segment) {
// 设置 params
ret[cur.segment[1:]] = segments[i]
}
cur = cur.parent
}
return ret
}
// 所有请求都进入这个函数, 这个函数负责路由分发
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
// 封装自定义 context
ctx := NewContext(request, response)
// 寻找路由
node := c.FindRouteNodeByRequest(request)
...
// 设置路由参数
params := node.parseParamsFromEndNode(request.URL.Path)
ctx.SetParams(params)
...
}
// 获取路由参数
func (ctx *Context) Param(key string) interface{} {
if ctx.params != nil {
if val, ok := ctx.params[key]; ok {
return val
}
}
return nil
}
// 路由匹配中带的参数
// 形如 /book/:id
func (ctx *Context) ParamInt(key string, def int) (int, bool) {
if val := ctx.Param(key); val != nil {
// 通过 cast 进行类型转换
return cast.ToInt(val), true
}
return def, false
}
- Bind 请求
// 将 body 文本解析到 obj 结构体中
func (ctx *Context) BindJson(obj interface{}) error {
if ctx.request != nil {
// 读取文本
body, err := ioutil.ReadAll(ctx.request.Body)
if err != nil {
return err
}
// 重新填充 request.Body,为后续的逻辑二次读取做准备
ctx.request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
// 解析到 obj 结构体中
err = json.Unmarshal(body, obj)
if err != nil {
return err
}
} else {
return errors.New("ctx.request empty")
}
return nil
}
IResponse 接口定义
type IResponse interface {
// 将obj对象转成json并发送出去?
Json(obj interface{}) IResponse
Jsonp(obj interface{}) IResponse
Xml(obj interface{}) IResponse
Html(template string, obj interface{}) IResponse
Text(format string, values ...interface{}) IResponse
Redirect(path string) IResponse
SetHeader(key string, val string)
SetCookie(key string, val string, maxAge int, path, domin string, secure, httpOnly bool) IResponse
SetStatus(code int) IResponse
SetOkStatus() IResponse
}
对于 Header 部分,我们设计了状态码的设置函数 SetStatus/SetOkStatus/Redirect,还设计了 Cookie 的设置函数 SetCookie,同时,我们提供了通用的设置 Header 的函数 SetHeader。
对于 Body 部分,我们设计了 JSON、JSONP、XML、HTML、Text 等方法来输出不同格式的 Body。
这里注意下,很多方法的返回值使用 IResponse 接口本身, 这个设计能允许使用方进行链式调用。链式调用的好处是,能很大提升代码的阅读性,比如在业务逻辑代码 controller.go 里这个调用方法:
c.SetOkStatus().Json("ok, UserLoginController: " + foo)
【小结】
- 对请求和响应进行封装,提供使用的便利性
- 请求封装有获取参数(url路径后缀、动态参数、表单参数)、获取header
- 返回封装有设置header、设备body