用 Go 开发接口服务--暴露 controller 控制层接口

栏目: Go · 发布时间: 6年前

内容简介:控制层主要负责接收外部的请求参数,然后把参数传递给 service 服务层,等服务层处理返回数据,再把数据序列化输出给外部。所以实际上 controller 控制层,是负责把终端提交的  JSON 数据转成对象,传递给 service 服务层函数,然后再把服务层函数返回的对象转成 JSON 返回给终端,这些流程都是可以封装在一些通用的函数里,可以节省很多重复的代码。在我们看来,这些接口不同点集中在传递的参数名、参数类别、参数顺序、上传文件时,需要做一些特别处理上面。控制层我们约定规则如下:避免写业务逻辑在控

控制层主要负责接收外部的请求参数,然后把参数传递给 service 服务层,等服务层处理返回数据,再把数据序列化输出给外部。所以实际上 controller 控制层,是负责把终端提交的  JSON 数据转成对象,传递给 service 服务层函数,然后再把服务层函数返回的对象转成 JSON 返回给终端,这些流程都是可以封装在一些通用的函数里,可以节省很多重复的代码。在我们看来,这些接口不同点集中在传递的参数名、参数类别、参数顺序、上传文件时,需要做一些特别处理上面。

控制层我们约定规则如下:避免写业务逻辑在控制层上,另外接口是直接对外的,所以注释接口的作用和各参数含义是非常有必要的,而且要求越详细越好。

终端请求内容格式是有若干种的,比如:

- 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 函数*

<em>// 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
}</em>

从代码中我们可以看出 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:

*代码清单 - 关键代码片段*

<em>// 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)</em>

终端输入数据到服务端,再由服务端处理,最后返回结果给终端,整个流程就这样完成了。controller 层主要接口函数如下,不全部列举:
*代码清单 - 部分控制层 Handler 代码*

<em>// 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
}</em>

接口函数和路由关联起来,接口就相当于暴露出去了,比如我们以 ProductList 几个 Handler 为例,我们在 Web 服务器 server.go  文件上新建路由地址对应它们:
*代码清单 - 路由关键代码片段*

<em>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)</em>

这时候,一旦 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 示范代码片段*

<em>func ProductDelete(w http.ResponseWriter, req *http.Request) {
	// here is your code
	return
}</em>

因为 controller 控制层只接收 JSON 的参数,终端上传文件的时候,就涉及到文件以何种形态在 JSON 里存在的,好像没有太多的选择,文件我们以 []byte 形态在 JSON 一个属性里。比如我们对文件写了一个特定的结构体来承载我们需要的文件结构:

*代码清单 - 关键代码片段*

// UploadFileArgs 上传文件的请求结构体
type UploadFileArgs struct {
	File    []byte `json:"file"`
	FileExt string `json:"fileExt"`
	Seq     int    `json:"seq"`
}

终端传进来的 JSON 也是以此结构体为标本的,最终发出请求的 JSON 类似:
*代码清单 - 关键  JSON 代码片段*

<em>{
	"file": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS",
	"fileExt": "png",
	"seq": 1
}</em>

我们把它转化成 UploadFileReqCol 结构体,代码如下:
*代码清单 - 关键代码片段*

<em>reqJSONString := requestJSONString(req)
var uploadFileArgs model.UploadFileArgs
err := json.Unmarshal([]byte(reqJSONString), &uploadFileArgs)
if err != nil {
	common.ShowErr(err)
}</em>

以上代码通过 Go 原生 json 库,把 JSON 字符串转成了结构体实例,再取值传递给 service 服务层处理返回。
controller 控制层,只接收终端发送 POST 请求来获取数据,所以我们还需要写一个拦截器。本身 negroni 支持基于 URL 的拦截器。我们在 init.go 写一个公共拦截器
*代码清单 - 拦截器关键代码片段*

<em>// 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
}</em>

拦截器和 Handler 函数很相似,都需要传入 responseWriter,request 参数,另外多了一个 HandlerFunc 方法,用于返回 controller 控制层的 Handler 函数。
拦截器进行一系列的验证,如果不通过直接 render 错误的 JSON 返回;如果通过了,直接 next(w,req) 返回 Handler 函数,继续处理未完成的工作。
上面的拦截器,首先验证请求是否是白名单,白名单我们将要写的案例测试地址,它们有以下特征:一定 Get 请求,并且地址路径末尾是 html 的;然后再验证请求是不是 POST 方式的,如果不是报错,如果是就返回 Handler 函数继续处理控制层的东西。
拦截器写好了,在 server.go 文件里,Web 服务器启用它:

<em>//所有的地址都要检查公共参数是否合法
n.Use(negroni.HandlerFunc(controller.CheckParamsMiddleware))</em>

小结

controller 控制层上 Handler 函数,就是一个完整的对外接口,要暴露出去,必须和 Web 服务器上的路由函数相关联。拦截器中间件可以起到全局的作用,它和控制层的 Handler 好比自家兄弟一样,结构方面都很相似,有时候好好利用它可以达到事半功倍的效果。另外 控制层的 Handler 不建议处理太多的逻辑,让它只负责获取参数和传递参数,从而达到每个 Handler 都大同小异,降低复杂度,便于可重用。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Designing for Emotion

Designing for Emotion

Aarron Walter / Happy Cog / 2011-10-18 / USD 18.00

Make your users fall in love with your site via the precepts packed into this brief, charming book by MailChimp user experience design lead Aarron Walter. From classic psychology to case studies, high......一起来看看 《Designing for Emotion》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换