Go 编程模式 05 修饰器(Decoration)

Go 是一种静态类型的编译语言,它的设计目标是简洁、高效。虽然 Go 不是一种完全的面向对象语言,但是我们仍然可以使用一些设计模式来提高代码的可读性和可维护性。今天,我将介绍一种常见的设计模式:修饰器模式。

01 什么是修饰器模式?

修饰器模式是一种设计模式,它允许我们在运行时动态地添加行为到对象上,而不改变其实现。这是通过创建一个包装对象或修饰器来实现的,这个修饰器包含了原始对象,并提供了一个增强的接口来添加新的行为。

在 Go 中,我们可以使用函数作为修饰器,这是因为 Go 支持高阶函数,即函数可以作为参数传递,也可以作为返回值。

02 一个例子

为了更好地理解修饰器模式,让我们通过一个例子来看看如何在 Go 中实现它。

首先,我们定义一个函数类型 Foo 和一个修饰器类型 FooDecorator

1
2
3
type Foo func(string) string

type FooDecorator func(Foo) Foo

然后,我们可以创建一个修饰器,它接受一个 Foo 类型的函数,并返回一个新的 Foo 函数,这个新的函数在调用原始函数之前和之后添加了一些行为:

1
2
3
4
5
6
7
8
func WithLog(decorated Foo) Foo {
return func(s string) string {
fmt.Println("Before calling decorated function")
result := decorated(s)
fmt.Println("After calling decorated function")
return result
}
}

现在,我们可以创建一个 Foo 函数,并使用修饰器来增强它:

1
2
3
4
5
6
7
8
func main() {
foo := func(s string) string {
fmt.Println("Foo function called")
return s
}
foo = WithLog(foo)
foo("Hello, world!")
}

在这个例子中,我们创建了一个 Foo 函数,然后使用 WithLog 修饰器来增强它。当我们调用增强后的函数时,它会首先打印一条消息,然后调用原始的 Foo 函数,最后再打印一条消息。

这就是 Go 中的修饰器模式。通过使用修饰器,我们可以在不修改原始函数的情况下,动态地添加新的行为。

HTTP 相关的一个示例

接下来,我们再看一个处理 HTTP 请求的相关例子。先看一个简单的 HTTP Server 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"log"
"net/http"
"strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("--->WithServerHeader()")
w.Header().Set("Server", "HelloServer v0.0.1")
h(w, r)
}
}

func hello(w http.ResponseWriter, r *http.Request) {
log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
http.HandleFunc("/v1/hello", WithServerHeader(hello))
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

这段代码中使用到了修饰器模式,WithServerHeader() 函数就是一个 Decorator,它会传入一个 http.HandlerFunc,然后返回一个改写的版本。这个例子还是比较简单的,用 WithServerHeader() 就可以加入一个 Response 的 Header。所以,这样的函数我们可以写出好多。如下所示,有写 HTTP 响应头的,有写认证 Cookie 的,有检查认证 Cookie 的,有打日志的……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package main

import (
"fmt"
"log"
"net/http"
"strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("--->WithServerHeader()")
w.Header().Set("Server", "HelloServer v0.0.1")
h(w, r)
}
}

func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("--->WithAuthCookie()")
cookie := &http.Cookie{Name: "Auth", Value: "Pass", Path: "/"}
http.SetCookie(w, cookie)
h(w, r)
}
}

func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("--->WithBasicAuth()")
cookie, err := r.Cookie("Auth")
if err != nil || cookie.Value != "Pass" {
w.WriteHeader(http.StatusForbidden)
return
}
h(w, r)
}
}

func WithDebugLog(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println("--->WithDebugLog")
r.ParseForm()
log.Println(r.Form)
log.Println("path", r.URL.Path)
log.Println("scheme", r.URL.Scheme)
log.Println(r.Form["url_long"])
for k, v := range r.Form {
log.Println("key:", k)
log.Println("val:", strings.Join(v, ""))
}
h(w, r)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc("/v3/hello", WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

多个修饰器的 Pipeline

在使用上,需要对函数一层层地套起来,看上去好像不是很好看,如果需要修饰器比较多的话,代码就会比较难看了。不过,我们可以重构一下。重构时,我们需要先写一个工具函数,用来遍历并调用各个修饰器:

1
2
3
4
5
6
7
8
9
type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc

func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
for i := range decors {
d := decors[len(decors)-1-i] // iterate in reverse
h = d(h)
}
return h
}

然后,我们就可以像下面这样使用了:

1
2
http.HandleFunc("/v4/hello", Handler(hello,
WithServerHeader, WithBasicAuth, WithDebugLog))

总结

再这篇文章中,我用两个例子演示了修饰器模式,但是因为Go语言中不支持注解这个语法糖,所以使用装饰器还是有点丑陋的,不过这个思想还是挺重要的,我们日常开发中可以参考这种思想,写出更优质的代码来。