想必只要是熟悉 Python 的同学对装饰模式一定不会陌生,这类 Python 从语法上原生支持的装饰器,大大提高了装饰模式在 Python 中的应用。尽管 Go 语言中装饰模式没有 Python 中应用的那么广泛,但是它也有其独到的地方。接下来就一起看下装饰模式在 Go 语言中的应用。

简单装饰器

我们通过一个简单的例子来看一下装饰器的简单应用,首先编写一个 hello 函数:


package main import "fmt" func hello() {
fmt.Println("Hello World!")
} func main() {
hello()
}

完成上面代码后,执行会输出“Hello World!”。接下来通过以下方式,在打印“Hello World!”前后各加一行日志:

package main

import "fmt"

func hello() {
fmt.Println("before")
fmt.Println("Hello World!")
fmt.Println("after")
} func main() {
hello()
}

代码执行后输出:

before
Hello World!
after

当然我们可以选择一个更好的实现方式,即单独编写一个专门用来打印日志的 logger 函数,示例如下:

package main

import "fmt"

func logger(f func()) func() {
return func() {
fmt.Println("before")
f()
fmt.Println("after")
}
} func hello() {
fmt.Println("Hello World!")
} func main() {
hello := logger(hello)
hello()
}

可以看到 logger 函数接收并返回了一个函数,且参数和返回值的函数签名同 hello 一样。然后我们在原来调用 hello() 的位置进行如下修改:

hello := logger(hello)
hello()

这样我们通过 logger 函数对 hello 函数的包装,更加优雅的实现了给 hello 函数增加日志的功能。执行后的打印结果仍为:

before
Hello World!
after

其实 logger 函数也就是我们在 Python 中经常使用的装饰器,因为 logger 函数不仅可以用于 hello,还可以用于其他任何与 hello 函数有着同样签名的函数。

当然如果想使用 Python 中装饰器的写法,我们可以这样做:


package main import "fmt" func logger(f func()) func() {
return func() {
fmt.Println("before")
f()
fmt.Println("after")
}
} // 给 hello 函数打上 logger 装饰器
@logger
func hello() {
fmt.Println("Hello World!")
} func main() {
// hello 函数调用方式不变
hello()
}

但很遗憾,上面的程序无法通过编译。因为 Go 语言目前还没有像 Python 语言一样从语法层面提供对装饰器语法糖的支持。

装饰器实现中间件

尽管 Go 语言中装饰器的写法不如 Python 语言精简,但它被广泛运用于 Web 开发场景的中间件组件中。比如 Gin Web 框架的如下代码,只要使用过就肯定会觉得熟悉:

package main

import "github.com/gin-gonic/gin"

func main() {
r := gin.New() // 使用中间件
r.Use(gin.Logger(), gin.Recovery()) r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
_ = r.Run(":8888")
}

如示例中使用 gin.Logger() 增加日志,使用 gin.Recovery() 来处理 panic 异常一样,在 Gin 框架中可以通过 r.Use(middlewares...) 的方式给路由增加非常多的中间件,来方便我们拦截路由处理函数,并在其前后分别做一些处理逻辑。

而 Gin 框架的中间件正是使用装饰模式来实现的。下面我们借用 Go 语言自带的 http 库进行一个简单模拟。这是一个简单的 Web Server 程序,其监听 8888 端口,当访问 /hello 路由时会进入 handleHello 函数逻辑:

package main

import (
"fmt"
"net/http"
) func loggerMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("before")
f(w, r)
fmt.Println("after")
}
} func authMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if token := r.Header.Get("token"); token != "fake_token" {
_, _ = w.Write([]byte("unauthorized\n"))
return
}
f(w, r)
}
} func handleHello(w http.ResponseWriter, r *http.Request) {
fmt.Println("handle hello")
_, _ = w.Write([]byte("Hello World!\n"))
} func main() {
http.HandleFunc("/hello", authMiddleware(loggerMiddleware(handleHello)))
fmt.Println(http.ListenAndServe(":8888", nil))
}

我们分别使用 loggerMiddleware、authMiddleware 函数对 handleHello 进行了包装,使其支持打印访问日志和认证校验功能。如果我们还需要加入其他中间件拦截功能,可以通过这种方式进行无限包装。

启动这个 Server 来验证下装饰器:

对结果进行简单分析可以看到,第一次请求 /hello 接口时,由于没有携带认证 token,收到了 unauthorized 响应。第二次请求时携带了 token,则得到响应“Hello World!”,并且后台程序打印如下日志:

before
handle hello
after

这说明中间件执行顺序是先由外向内进入,再由内向外返回。而这种一层一层包装处理逻辑的模型有一个非常形象且贴切的名字,洋葱模型。

但用洋葱模型实现的中间件有一个直观的问题。相比于 Gin 框架的中间件写法,这种一层层包裹函数的写法不如 Gin 框架提供的 r.Use(middlewares...) 写法直观。

Gin 框架源码的中间件和 handler 处理函数实际上被一起聚合到了路由节点的 handlers 属性中。其中 handlers 属性是 HandlerFunc 类型切片。对应到用 http 标准库实现的 Web Server 中,就是满足 func(ResponseWriter, *Request) 类型的 handler 切片。

当路由接口被调用时,Gin 框架就会像流水线一样依次调用执行 handlers 切片中的所有函数,再依次返回。这种思想也有一个形象的名字,就叫作流水线(Pipeline)。

接下来我们要做的就是将 handleHello 和两个中间件 loggerMiddleware、authMiddleware 聚合到一起,同样形成一个 Pipeline。

package main

import (
"fmt"
"net/http"
) func authMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if token := r.Header.Get("token"); token != "fake_token" {
_, _ = w.Write([]byte("unauthorized\n"))
return
}
f(w, r)
}
} func loggerMiddleware(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("before")
f(w, r)
fmt.Println("after")
}
} type handler func(http.HandlerFunc) http.HandlerFunc // 聚合 handler 和 middleware
func pipelineHandlers(h http.HandlerFunc, hs ...handler) http.HandlerFunc {
for i := range hs {
h = hs[i](h)
}
return h
} func handleHello(w http.ResponseWriter, r *http.Request) {
fmt.Println("handle hello")
_, _ = w.Write([]byte("Hello World!\n"))
} func main() {
http.HandleFunc("/hello", pipelineHandlers(handleHello, loggerMiddleware, authMiddleware))
fmt.Println(http.ListenAndServe(":8888", nil))
}

我们借用 pipelineHandlers 函数将 handler 和 middleware 聚合到一起,实现了让这个简单的 Web Server 中间件用法跟 Gin 框架用法相似的效果。

再次启动 Server 进行验证:

改造成功,跟之前使用洋葱模型写法的结果如出一辙。

总结

简单了解了 Go 语言中如何实现装饰模式后,我们通过一个 Web Server 程序中间件,学习了装饰模式在 Go 语言中的应用。

需要注意的是,尽管 Go 语言实现的装饰器有类型上的限制,不如 Python 装饰器那般通用。就像我们最终实现的 pipelineHandlers 不如 Gin 框架中间件强大,比如不能延迟调用,通过 c.Next() 控制中间件调用流等。但不能因为这样就放弃,因为 GO 语言装饰器依然有它的用武之地。

Go 语言是静态类型语言不像 Python 那般灵活,所以在实现上要多费一点力气。希望通过这个简单的示例,相信对大家深入学习 Gin 框架有所帮助。

推荐阅读

两招提升硬盘存储数据的写入效率

【程序员的实用工具推荐】 Mac 效率神器 Alfred

Golang 常见设计模式之装饰模式的更多相关文章

  1. Golang 常见设计模式之单例模式

    之前我们已经看过了 Golang 常见设计模式中的装饰和选项模式,今天要看的是 Golang 设计模式里最简单的单例模式.单例模式的作用是确保无论对象被实例化多少次,全局都只有一个实例存在.根据这一特 ...

  2. Golang 常见设计模式之选项模式

    熟悉 Python 开发的同学都知道,Python 有默认参数的存在,使得我们在实例化一个对象的时候,可以根据需要来选择性的覆盖某些默认参数,以此来决定如何实例化对象.当一个对象有多个默认参数时,这个 ...

  3. JAVA设计模式简介及六种常见设计模式详解

    一.什么是设计模式                                                                                           ...

  4. 乐在其中设计模式(C#) - 装饰模式(Decorator Pattern)

    原文:乐在其中设计模式(C#) - 装饰模式(Decorator Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 装饰模式(Decorator Pattern) 作者:weba ...

  5. JavaScript 中常见设计模式整理

    开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式.本文意在梳理常见设计模式的特点,从而对它们有比较清晰的认知. JavaScript 中常见设计模 ...

  6. JS中常见设计模式总结

    github: https://github.com/14glwu/FEInterviewBox/tree/master/JS%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F ...

  7. java之 ------ DAO设计模式的【具体解释】及常见设计模式的【应用】

    DAO Data Access Object(数据訪问接口) 一.场景和问题 在Java程序中.常常须要把数据持久化,也须要获取持久化的数据.可是在进行数据持久化的过程中面临诸多问题(如:数据源 不同 ...

  8. golang 常见变成问题01

    Golang常见编程问题 --语言简单 func CopyFile (dst, src string) (w int64, err error) { srcFile, err := os.Open ( ...

  9. Golang 实现设计模式 —— 装饰模式

    概念 "用于代替继承的技术,无需通过继承增加子类就能扩展对象的新功能" "动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活" 何时 ...

随机推荐

  1. Python--变量和简单数据类型

    Python--变量和简单数据类型 目录 Python--变量和简单数据类型 一.Python脚本运行过程 二.变量 1. 变量的命名和使用 2. Python关键字和内置函数 2.1 Python关 ...

  2. python基础语法_9-2函数式编程

    https://www.imooc.com/learn/317 大纲 1-函数式编程简介 2-高阶函数 3-把函数作为参数 4-map()函数 5-reduce()函数 6-filter()函数 7- ...

  3. Solution -「CF 802C」Heidi and Library (hard)

    \(\mathcal{Descriptoin}\)   Link.   你有一个容量为 \(k\) 的空书架,现在共有 \(n\) 个请求,每个请求给定一本书 \(a_i\).如果你的书架里没有这本书 ...

  4. Grafana v8.3.3 & jmeter-influxdb2-backend

    1. 说明 接上篇文章,今天继续聊Grafana & influxdb2-backend. 2. Grafana v8.3.3安装 下载rpm包 wget https://dl.grafana ...

  5. 《深度探索C++对象模型》第一章 | 关于对象

    C++对象模式 非静态数据成员放置在每个类对象内,静态数据成员则被放置在所有类对象之外.静态和非静态的成员函数也被放置在所有类对象之外.每个类产生一堆指向虚函数的指针,放在虚表(vtbl)中.每个类对 ...

  6. zabbix主动上报mysql数据库内容

    zabbix_sender命令支持主动上报数据,web服务端添加对应机器和采集器即可. 2015年刚接触zabbix时候,用的上报sqlserver脚本是select数据后插入到临时表,bcp下载到本 ...

  7. SpringCloud微服务实战——搭建企业级开发框架(三十八):搭建ELK日志采集与分析系统

      一套好的日志分析系统可以详细记录系统的运行情况,方便我们定位分析系统性能瓶颈.查找定位系统问题.上一篇说明了日志的多种业务场景以及日志记录的实现方式,那么日志记录下来,相关人员就需要对日志数据进行 ...

  8. 超强可视化图表工具:Smartbi!!

    要制作出专业的可视化图表,还是需要一定的学习成本的,并且需要大量的时间.并且即使是制作出来,配色也是一大难题,对于一般人而言,通常会通过两种方式实现可视化大屏的制作: 1.写代码 大部分人可能会选择大 ...

  9. Keepalived非抢占模式配置

    一.前言 HA的实际运行过程中,当主机发生异常,且后期恢复正常后,存在抢占或非抢占两种情况. 结合实际需求,可能有很多用户需要非抢占的HA工作模式.keepalived能够很好的支持这一需求. 二.k ...

  10. NFA转化为DFA

    NFA(不确定的有穷自动机)转化为DFA(确定的有穷自动机) NFA转换DFA,通常是将带空串的NFA(即:ε-NFA)先转化为不带空串的NFA(即:NFA),然后再转化为DFA. 提示:ε是空串的意 ...