用 Go 开发终端接口服务--暴露 controller 控制层接口
控制层我们约定规则如下:避免写业务逻辑在控制层上,另外接口是直接对外的,所以注释接口的作用和各参数含义是非常有必要的,而且要求越详细越好。
终端请求内容格式是有若干种的,比如:
- application/x-www-form-urlencoded 纯粹表单键值对格式的。
- multipart/form-data 表单键值对加文件流格式的。
- application/json 纯粹 JSON 数据格式的。
我们项目传输内容格式统一采用 application/json,这样有很多好处,比如我们可以对整个 JSON 请求内容进行加密处理,很明显这样既干脆又利落;另外 JSON 格式是通用的,既简单又好维护;终端请求数据和服务端返回数据都是 JSON 格式的,格式上保持一致,有利于团队联调交流。
另外终端的请求方式一律采用 POST 请求方式,它相对 GET 请求方式会隐蔽安全些。这样统一约定好规则,可以省去很多工夫。
以下有两个封装好的函数比较关键,requestJSONString 主要功能是从终端的请求中,获取 JSON 字符串参数,然后 requestJSON 再把它转成 JSON 对象,直接取值传递给 service 服务层,JSON 字符串转成 JSON 对象,要依赖 gjson 库,它是一个非常好用的东西。
*代码清单 - 控制层封装好的公共函数和 Handler 函数*
// requestJSON 把请求参数转成 JSON 对象 func requestJSON(req *http.Request) gjson.Result { jsonString := requestJSONString(req) jsonResult := gjson.Result{} if jsonString != "" { jsonResult = gjson.Parse(jsonString) } return jsonResult } // requestJSONString 把请求参数转成 JSON 字符串 func requestJSONString(req *http.Request) string { return util.RequestJSON(req) } // ProductDetail 产品详情 func ProductDetail(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) respBody := service.ProductDetail(reqJSON.Get("productID").Int()) r.JSON(w, http.StatusOK, respBody) return }从代码中我们可以看出 ProductDetail 接口的 reqJSON 就是 JSON 对象了,根据 key 直接 reqJSON.Get("productID").Int() 取值,传递出来。
终端 POST 请求过来的参数,我们已经很容易获取到,我们还需要参数传递给服务层处理,处理完毕,service 服务层返回 model.ServiceResponse 对象,我们再把它转成 JSON 返回给终端。这个过程我们用到了关键的 render 库,它可以输出 html json xml text 格式的数据到 http.ResponseWriter 里,传递给终端。
*代码清单 - render 初始化代码*
r = render.New(render.Options{ Directory: "template", Layout: "layout", Extensions: []string{".html", ".tmpl"}, Funcs: []template.FuncMap{AppHelpers}, Delims: render.Delims{Left: "{{", Right: "}}"}, Charset: charsetDefault, IndentJSON: renderUtil.debug, IndentXML: renderUtil.debug, PrefixJSON: []byte(""), PrefixXML: []byte(""), HTMLContentType: "text/html", IsDevelopment: false, UnEscapeHTML: true, StreamingJSON: true, RequirePartials: true, DisableHTTPErrorRendering: true, })
render 初始化完成后,通过以下代码输入 JSON:
*代码清单 - 关键代码片段*
// RendJSON 响应渲染出 JSON 数据到 http.ResponseWriter func RendJSON(w http.ResponseWriter, req *http.Request, v interface{}) { r.JSON(w, http.StatusOK, v) } // respBody 是业务层返回的 model.ServiceResponse 对象 r.JSON(w, http.StatusOK, respBody) // 或者使用封装好的 RendJSON RendJSON(W, req, respBody)
终端输入数据到服务端,再由服务端处理,最后返回结果给终端,整个流程就这样完成了。controller 层主要接口函数如下,不全部列举:
*代码清单 - 部分控制层 Handler 代码*
// ProductList 产品列表 func ProductList(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) respBody := service.ProductList(reqJSON.Get("category").Int(), "", reqJSON.Get("start").Uint(), reqJSON.Get("end").Uint()) r.JSON(w, http.StatusOK, respBody) return } // ProductSearch 关键字搜索产品 func ProductSearch(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) respBody := service.ProductList(reqJSON.Get("category").Int(), reqJSON.Get("name").String(), reqJSON.Get("start").Uint(), reqJSON.Get("end").Uint()) r.JSON(w, http.StatusOK, respBody) return } // ProductDetail 产品详情 func ProductDetail(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) respBody := service.ProductDetail(reqJSON.Get("productID").Int()) r.JSON(w, http.StatusOK, respBody) return } // ProductAddNew 新增一个产品 func ProductAddNew(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) photoEditJSON := reqJSON.Get("photoEdit").String() var photoEdit []model.PhotoArgs if photoEditJSON != "" { json.Unmarshal([]byte(photoEditJSON), &photoEdit) } respBody := service.ProductAddNew( reqJSON.Get("category").Int(), reqJSON.Get("name").String(), reqJSON.Get("intro").String(), reqJSON.Get("price").Float(), photoEdit, ) r.JSON(w, http.StatusOK, respBody) return } // ProductModify 修改一个产品 func ProductModify(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) photoEditJSON := reqJSON.Get("photoEdit").String() var photoEdit []model.PhotoArgs if photoEditJSON != "" { json.Unmarshal([]byte(photoEditJSON), &photoEdit) } respBody := service.ProductModify( reqJSON.Get("productID").Int(), reqJSON.Get("category").Int(), reqJSON.Get("name").String(), reqJSON.Get("intro").String(), reqJSON.Get("price").Float(), photoEdit, ) r.JSON(w, http.StatusOK, respBody) return } // ProductDelete 删除一个产品,包括产品图片 func ProductDelete(w http.ResponseWriter, req *http.Request) { reqJSON := requestJSON(req) respBody := service.ProductDelete(reqJSON.Get("productID").Int()) r.JSON(w, http.StatusOK, respBody) return }
接口函数和路由关联起来,接口就相当于暴露出去了,比如我们以 ProductList 几个 Handler 为例,我们在 Web 服务器 server.go 文件上新建路由地址对应它们:
*代码清单 - 路由关键代码片段*
router := http.NewServeMux() router.HandleFunc("/api/v1/product/list", controller.ProductList) router.HandleFunc("/api/v1/product/search", controller.ProductSearch) router.HandleFunc("/api/v1/product/detail", controller.ProductDetail) router.HandleFunc("/api/v1/product/add", controller.ProductAddNew) router.HandleFunc("/api/v1/product/modify", controller.ProductModify) router.HandleFunc("/api/v1/product/delete", controller.ProductDelete) router.HandleFunc("/api/v1/product/photo/upload", controller.UploadProductPhoto)
这时候,一旦 Web 服务器启动,ProductList 等等几个接口就正式暴露出去了,终端可以通过:
http://localhost:3000/api/v1/product/list
http://localhost:3000/api/v1/product/search
http://localhost:3000/api/v1/product/detail
...
URL 地址把数据发送 POST 请求给服务器了。
得益于 negroni,控制层的函数和原生的 web handler 是一致的,都是传递一个 responserWriter 和 request 参数,这样和原生 net/http 完美切换,不需要修改什么东西,非常地道的做法。
*代码清单 - handler 示范代码片段*
func ProductDelete(w http.ResponseWriter, req *http.Request) { // here is your code return }因为 controller 控制层只接收 JSON 的参数,终端上传文件的时候,就涉及到文件以何种形态在 JSON 里存在的,好像没有太多的选择,文件我们以 []byte 形态在 JSON 一个属性里。比如我们对文件写了一个特定的结构体来承载我们需要的文件结构:
*代码清单 - 关键代码片段*
// UploadFileArgs 上传文件的请求结构体 type UploadFileArgs struct { File []byte `json:"file"` FileExt string `json:"fileExt"` Seq int `json:"seq"` }
终端传进来的 JSON 也是以此结构体为标本的,最终发出请求的 JSON 类似:
*代码清单 - 关键 JSON 代码片段*
{ "file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS", "fileExt": "png", "seq": 1 }
我们把它转化成 UploadFileReqCol 结构体,代码如下:
*代码清单 - 关键代码片段*
reqJSONString := requestJSONString(req) var uploadFileArgs model.UploadFileArgs err := json.Unmarshal([]byte(reqJSONString), &uploadFileArgs) if err != nil { common.ShowErr(err) }
以上代码通过 Go 原生 json 库,把 JSON 字符串转成了结构体实例,再取值传递给 service 服务层处理返回。
controller 控制层,只接收终端发送 POST 请求来获取数据,所以我们还需要写一个拦截器。本身 negroni 支持基于 URL 的拦截器。我们在 init.go 写一个公共拦截器
*代码清单 - 拦截器关键代码片段*
// CheckParamsMiddleware 检查公共参数 func CheckParamsMiddleware(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) { //白名单地址,一般都是 GET 请求的地址 if chkWhiteURI(req) { next(w, req) return } if strings.ToUpper(req.Method) != "POST" { serviceResp := service.SetServiceResponseCode(common.Code1005) RendJSON(w, req, serviceResp) return } next(w, req) return } // chkWhiteURI 检查给的后缀地址是否为白名单地址(一般都是 GET 请求,不需要传公共参数) func chkWhiteURI(req *http.Request) bool { requestURI := req.RequestURI if strings.ToUpper(req.Method) == "GET" && (strings.HasSuffix(requestURI, ".html") || strings.HasSuffix(requestURI, ".htm")) { return true } arr := []string{"/test"} for i, l := 0, len(arr); i < l; i++ { if strings.HasPrefix(requestURI, arr[i]) { return true } continue } return false }
拦截器和 Handler 函数很相似,都需要传入 responseWriter,request 参数,另外多了一个 HandlerFunc 方法,用于返回 controller 控制层的 Handler 函数。
拦截器进行一系列的验证,如果不通过直接 render 错误的 JSON 返回;如果通过了,直接 next(w,req) 返回 Handler 函数,继续处理未完成的工作。
上面的拦截器,首先验证请求是否是白名单,白名单我们将要写的案例测试地址,它们有以下特征:一定 Get 请求,并且地址路径末尾是 html 的;然后再验证请求是不是 POST 方式的,如果不是报错,如果是就返回 Handler 函数继续处理控制层的东西。
拦截器写好了,在 server.go 文件里,Web 服务器启用它:
//所有的地址都要检查公共参数是否合法 n.Use(negroni.HandlerFunc(controller.CheckParamsMiddleware))
小结
controller 控制层上 Handler 函数,就是一个完整的对外接口,要暴露出去,必须和 Web 服务器上的路由函数相关联。拦截器中间件可以起到全局的作用,它和控制层的 Handler 好比自家兄弟一样,结构方面都很相似,有时候好好利用它可以达到事半功倍的效果。另外 控制层的 Handler 不建议处理太多的逻辑,让它只负责获取参数和传递参数,从而达到每个 Handler 都大同小异,降低复杂度,便于可重用。
《用 Go 开发终端接口服务》 目录
- 小册介绍
- 前言
- 环境搭建与开发工具选择
- Go 语言基本语法
- Go 语言编码规范
- 快速编写一个 Web 服务器
- 项目整体结构介绍
- 准备项目所需的 Go 类包
- 公共类关键函数
- 定义 model 实体层结构体
- 灵活写 dao 数据层函数
- 按需写 service 服务层逻辑
- 暴露 controller 控制层接口
- 测试已写好的接口
- 把项目部署到服务器
- 保证高性能项目的法宝
- 写在后面
哇~~~ 竟然还没有评论!