05 封装

文章讨论了如何在context中封装HTTP请求和响应,包括读取请求数据(Header和Body)以及封装返回数据。提出了定义IRequest和IResponse接口以提高易用性和可读性,详细介绍了接口中各种方法的设计,如Query、Param、Form、BindJson等,以及返回数据的Header和Body设置。此外,文章强调了链式调用在提高代码阅读性方面的作用。
摘要由CSDN通过智能技术生成

在对 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)

【小结】

  1. 对请求和响应进行封装,提供使用的便利性
  2. 请求封装有获取参数(url路径后缀、动态参数、表单参数)、获取header
  3. 返回封装有设置header、设备body
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值