古老的榕树

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

发表 2019-05-14 17:04 阅读(604)
控制层主要负责接收外部的请求参数,然后把参数传递给 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 函数*

// 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 开发终端接口服务》 目录


Donate

如果文章对您有帮助,请使用手机支付宝扫描二维码,捐赠X元,作者离不开读者的支持!