Go 装饰器模式在 API 服务程序中的使用
Go 语言是由谷歌主导并开源的编程语言,和 C 语言有不少相似之处,都强调执行效率,语言结构尽量简单,也都主要用来解决相对偏底层的问题。因为 Go 简洁的语法、较高的开发效率和 goroutine,有一段时间也在 Web 开发上颇为流行。由于工作的关系,我最近也在用 Go 开发 API 服务。但对于 Golang 这种奉行极简主义的语言,如何提高代码复用率就会成为一个很大的挑战,API server 中的大量接口很可能有完全一致的逻辑,如果不解决这个问题,代码会变得非常冗余和难看。
Python 中的装饰器
在 Python 中,装饰器功能非常好的解决了这个问题,下面的伪代码中展示了一个例子,检查 token 的逻辑放在了装饰器函数 check_token 里,在接口函数上加一个 @check_token
就可以在进入接口函数逻辑前,先检查 token 是否有效。虽然说不用装饰器一样可以将公共逻辑抽取出来,但是调用还是要写在每个接口函数的函数体里,侵入性明显大于使用装饰器的方式。
# 装饰器函数,用来检查客户端的 token 是否有效。 def check_token(): ... @check_token # 接口函数,用来让用户登陆。 def login(): ... @check_token # 接口函数,查询用户信息。 def get_user(): ...
Go 中装饰器的应用
Go 语言也是可以使用相同的思路来解决这个问题的,但因为 Go 没有提供象 Python 一样便利的语法支持,所以很难做到像 Python 那样漂亮,不过我觉得解决问题才是更重要的,让我们一起来看看是如何做到的吧。
以下的 API 服务代码示例是基于 Gin-Gonic 框架,对 Gin 不太熟悉的朋友,可以参考我之前翻译的一篇文章:如何使用 Gin 和 Gorm 搭建一个简单的 API 服务器 (一)
本文中的代码为了方便展示,我做了些简化,完整版见于 https://github.com/blackpiglet/go-api-example
简单示例
Go 语言实现装饰器的道理并不复杂,CheckParamAndHeader 实现了一个高阶函数,入参 h 是 gin 的基本函数类型 gin.HandlerFunc。返回值是一个匿名函数,类型也是 gin.HandlerFunc。CheckParamAndHeader 中除了运行自己的代码,也调用了作为入参传递进来的 h 函数。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing token", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", CheckParamAndHeader(Login)) r.Run(":8080") }
装饰器的 pipeline
装饰器的功能已经实现了,但如果接口函数需要调用多个装饰,那么函数套函数,还是比较乱,可以写一个装饰器处理函数来简化代码,将装饰器及联起来,这样代码变得简洁了不少。
package main import ( "fmt" "github.com/gin-gonic/gin" ) func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc { for i := range decors { d := decors[len(decors)-1-i] // iterate in reverse h = d(h) } return h } func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing token", }) return } } } func CheckParamAndHeader_1(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("auth") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": ". Missing auth", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckParamAndHeader_1, Login)) r.Run(":8080") }
根据接口名称判断用户是否有权限访问
API 服务程序可能会需要判断用户是否有权限访问接口,如果使用了 MVC 模式,就需要根据接口所在的 module 和接口自己的名称来判断用户能否访问,这就要求在装饰器函数中知道被调用的接口函数名称是什么,这点可以通过 Go 自带的 runtime 库来实现。
package main import ( "fmt" "runtime" "strings" "github.com/gin-gonic/gin" ) func Decorator(h gin.HandlerFunc, decors ...HandlerDecoratored) gin.HandlerFunc { for i := range decors { d := decors[len(decors)-1-i] // iterate in reverse h = d(h) } return h } func CheckParamAndHeader(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { header := c.Request.Header.Get("token") if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": "Missing token", }) return } } } func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name() function_name_array := strings.Split(function_name_str, "/") module_method := strings.Split(function_name_array[len(function_name_array)-1], ".") module := module_method[0] method := module_method[1] if module != "Login" { c.JSON(200, gin.H{ "code": 2, "result": "failed", "msg": "No permission", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", Decorator(CheckParamAndHeader, CheckPermission, Login)) r.Run(":8080") }
向装饰器函数传参
接口可能会有要求客户端必须传某些特定的参数或者消息头,而且很可能每个接口的必传参数都不一样,这就要求装饰器函数可以接收参数,不过我目前还没有找到在 pipeline 的方式下传参的方法,只能使用最基本的方式。
package main import ( "fmt" "runtime" "strconv" "strings" "github.com/gin-gonic/gin" ) func CheckParamAndHeader(input gin.HandlerFunc, http_params ...string) gin.HandlerFunc { return func(c *gin.Context) { http_params_local := append([]string{"param:user_id", "header:token"}, http_params...) required_params_str := strings.Join(http_params_local, ", ") required_params_str = "Required parameters include: " + required_params_str fmt.Println(http_params_local, required_params_str, len(http_params_local)) for _, v := range http_params_local { ret := strings.Split(v, ":") switch ret[0] { case "header": header := c.Request.Header.Get(ret[1]) if header == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } case "param": _, err := c.GetQuery(ret[1]) if err == false { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } case "body": body_param := c.PostForm(ret[1]) if body_param == "" { c.JSON(200, gin.H{ "code": 3, "result": "failed", "msg": required_params_str + ". Missing " + v, }) return } default: fmt.Println("Unsupported checking type: %s", ret[0]) } } input(c) } } func CheckPermission(h gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { function_name_str := runtime.FuncForPC(reflect.ValueOf(input).Pointer()).Name() function_name_array := strings.Split(function_name_str, "/") module_method := strings.Split(function_name_array[len(function_name_array)-1], ".") module := module_method[0] method := module_method[1] if module != "Login" { c.JSON(200, gin.H{ "code": 2, "result": "failed", "msg": "No permission", }) return } } } func Login(c *gin.Context) { c.JSON(200, gin.H{ "code": 0, "result": "success", "msg": "验证成功", }) } func main() { r := gin.Default() r.POST("/v1/login", CheckParamAndHeader(CheckPermission(Login), "body:password", "body:name")) r.Run(":8080") }
到目前为止,已经实现了我对 API 服务器的基本需求,如果大家有更好的实现方式,烦请赐教,有什么我没想到的需求,也欢迎留言讨论。
本文主要参考以下两篇文章:
GO语言的修饰器编程
Decorated functions in Go
尤其推荐左耳朵耗子的 GO语言的修饰器编程,里面还谈到了装饰器的范型,让装饰器更加通用。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
消息队列深入解析
消息队列和消息 “消息队列”(Message queue)是在消息的传输过程中保存消息的容器。“消息” 是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。 常见的消息队列有那些? 当前使用较多的消息队列有RabbitMQ、ActiveMQ、RocketMQ、Kafka等等,我们之前提高的redis数据库也可以实现消息队列,不过不推荐,redis本身设计就不是用来做消息队列的。 使用消息队列的场景和好处 《大型网站技术架构》第四章和第七章均有提到消息队列对应用性能及扩展性的提升。 1.通过异步处理提高系统性能 如上图,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。 通过以上分析我们可以得出消息队列具有很好的削峰作用的功能——即通过异步处理,将短时...
- 下一篇
Java书籍推荐
基础 《Head First Java.第二版》: 可以说是我的Java启蒙书籍了,特别适合新手读当然也适合我们用来温故Java知识点。 《Head First设计模式(高清版)》: 非常推荐。 《Java多线程编程核心技术》: Java多线程入门级书籍还不错。 《JAVA网络编程 第4版》: 可以系统的学习一下网络的一些概念以及网络编程在Java中的使用。 《Spring MVC+MYBatis企业应用实战》: 学习SSM比较新的一本书了,书中Spring版本是4.0以上,所以当做工具书来读也很不错。 进阶 《Java核心技术卷1+卷2》 很棒的两本书,建议有点Java基础之后再读,介绍的还是比较深入的,非常推荐。 《Java编程思想(第4版)》 这本书要常读,初学者可以快速概览,中等程序员可以深入看看java,老鸟还可以用之回顾java的体系。这本书之所以厉害,因为它在无形中整合了设计模式,这本书之所以难读,也恰恰在于他对设计模式的整合是无形的。 《Java并发编程的艺术》 这本书不是很适合作为Java并发入门书籍,需要具备一定的JVM基础。前面三章写得很深入,而后面几章特别是介绍...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- 2048小游戏-低调大师作品
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题