接口的水平组合
如果说 C++ 和 Java 是关于类型层次结构和类型分类的语言,那么 Go 则是关于组合的语言。
在语言设计层面,Go 提供了诸多正交的语法元素提供后续组合的使用,包括:
- Go 语言无类型体系,类型定义正交独立;
- 方法和类型是正交的,每种类型都可以拥有自己的方法集合;
- 接口与其实现者之间无显式关联。
Go 语言中主要有两种组合方式:
- 垂直组合(类型组合): Go 语言主要通过类型嵌入机制实现垂直组合,进而实现方法的复用、接口定义重用等;
- 水平组合:通常 Go 程序以接口类型变量作为程序水平组合的连接点。接口是水平组合的关键,它就好比程序肌体上的关节,给予连接关节的两个部分或多个部分各自自由活动的能力,而整体又实现了了某种功能。
以接口为连接点的水平组合方式可以将各个垂直组合出的类型耦合在一起,从而编织出程序静态骨架。 而通过接口的进行水平组合的一种常见模式是使用接受接口类型参数的函数或方法。
以下是水平组合的一些管用形式:
基本形式
水平组合的基本形式是将接口类型作为函数或方法的参数,如:
func YourFuncName(param YourInterfaceType) {}
这种水平组合的基本形式在标准库中有着广泛的应用,如:
func readAll(r io.Reader) (b []byte, err error) {}
func Copy(dst Writer, src Reader) (written int64, err error) {}
接口与其实现者之间的隐式关系在不经意间满足了依赖抽象、里氏替换原则,接口隔离等代码设计原则,这在其他语言中是需要刻意设计的,而在 Go 接口来看,这一切却是那么自然而然。
包裹函数
包裹函数(wrapper function)的形式是这样的,它接受接口类型参数,并返回与其参数类型相同的返回值。 由于包裹函数的返回值类型与参数类型相同,因此我们可以将多个接受同一接口类型参数的包裹函数组合成一条链来调用。
func YourWrapperFuncName(param YourInterfaceType) YourInterfaceType {}
我们来看一个例子:
import (
"bytes"
"io"
"os"
"strings"
)
func CapReader(r io.Reader) io.Reader {
return &CapitalizedReader{r}
}
type CapitalizedReader struct {
r io.Reader
}
func (cr *CapitalizedReader) Read(p []byte) (int, error) {
n, err := cr.r.Read(p)
if err != nil {
return 0, err
}
q := bytes.ToUpper(p)
copy(p, q)
return n, nil
}
func main() {
r := strings.NewReader("hello, world\n")
cr := CapReader(io.LimitReader(r, 4))
_, err := io.Copy(os.Stdout, cr)
if err != nil {
println(err)
}
}
// 输出
// HELL
CapReader
和 io.LimitReader
串在一起形成了一条调用链,这个调用链的功能为:截取输入数据的前 4 个字符并将其转换为大写。
适配器函数类型
适配器函数类型(adapter function type)是一个辅助水平组合实现的工具类型,它是一个类型,可以将一个满足特定函数签名的普通函数显示转换成自身类型的实例。
我们来看一个例子:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc
是一个适配器函数类型,它将一个满足 ServeHTTP
函数签名的普通函数转换成 Handler
接口类型的实例。
func geeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}
func main() {
http.ListenAndServe(":8080", http.HandlerFunc(geeting))
}
我们可以看到 http.HandlerFunc
将 geeting
函数转换成了 http.Handler
接口类型的实例。转换后,我们便可以将其实例用作实参,实现基于接口的组合了。
中间件
中间件(middleware)含义可大可小,在 Go Web 编程中,它常常指的是一个实现了 http.Handler
接口的 http.HandlerFunc
类型实例。
实质上,这里的中间件就是包裹函数和适配器函数类型的结合产物。
我们来看一个例子:
func validateAuth(s, string) error {
if s != "auth" {
return errors.New("auth failed")
}
return nil
}
func geeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, world!")
}
func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
log.Println("[%s] %q %v\n", r.Method, r.URL.String(), time.Since(t))
h.ServeHTTP(w, r)
})
}
func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := validateAuth(r.Header.Get("auth")); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
func main() {
http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(geeting))))
}
// 输出
// curl http://localhost:8080
// auth failed
// curl -H "auth: auth" http://localhost:8080/
// Hello, world!
在这个例子中,logHandler
和 authHandler
就是中间件,它们都是包裹函数,将 http.HandlerFunc
类型的实例转换成了 http.Handler
类型的实例。