前置原则

在实际真正需要的时候才对程序进行抽象

  1. 不要为了抽象而抽象
  2. 不要为了使用接口而使用接口

解耦或者抽象是有成本

  1. 造成运行效率的下降
  2. 影响代码的可读性

组合

  1. 如果 C++Java 是关于类型层次结构类型分类的语言,那么 Go 是关于组合的语言
  2. 组合是 Go 的重要设计哲学之一,而正交性则为组合哲学的落地提供了方便的条件

正交

  1. 在计算机技术中,正交性用于表示某种不相依赖性或者解耦性
  2. 编程语言中的语法元素语言特性也存在正交的情况,通过将这些正交的特性组合起来,可以实现更为高级的特性

Go 在语言设计层面提供的正交的语法元素

Key Value
类型定义是正交的 无类型体系,没有父子类的概念
方法类型是正交的 方法的本质只是将 receiver 参数作为第一个参数的函数而已
接口与其它语言元素是正交的 接口与它的实现者之间没有显式关联

方式

image-20231119224234141

垂直组合

用在将多个类型通过类型嵌入的方式实现新类型的定义

  1. 传统的 OOP 编程语言大多是通过继承的方式构建出类型体系
  2. Go 没有类型体系的概念,Go 通过类型的组合而不是继承让单一类型承载更多的功能 - 垂直组合
  3. 通过垂直组合定义的新类型与被嵌入的类型之间没有所谓的父子关系
    • 没有 Type Casting
    • 被嵌入的类型也不知道外部类型的存在
  4. 调用方法时,方法的匹配取决于方法名,而非类型
  5. 垂直组合更多应用于类型定义,本质是一种类型组合,属于类型之间的耦合方式

通过嵌入接口构建接口

io/io.go
1
2
3
4
5
// 实现接口行为聚合
type ReadWriter interface {
Reader
Writer
}

通过嵌入接口构建结构体

1
2
3
4
5
6
// 用于快速构建满足某一接口的结构体类型,常用于单元测试
type MyReader struct {
io.Reader

N int64
}

通过嵌入结构体构建结构体

1
2
3
4
5
6
7
8
type A struct {
}

type B struct {
A

N int64
}
  1. 结构体中嵌入接口或者其它结构体,都是委派模式(delegate)的一种应用
  2. 对新结构体类型的方法调用,可能会被委派给该结构体内部嵌入的结构体实例

水平组合

依赖接口,而非依赖实现

1
2
3
4
5
6
7
func Save(w io.Writer, data []byte) error {
_, err := w.Write(data)
if err != nil {
return err
}
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func TestSave(t *testing.T) {
b := make([]byte, 0, 128)
buf := bytes.NewBuffer(b)

data := []byte("hello world")
err := Save(buf, data)
if err != nil {
t.Errorf("want nil, actual %s", err.Error())
}

saved := buf.Bytes()
if !reflect.DeepEqual(saved, data) {
t.Errorf("want %s, actual %s", string(data), string(saved))
}
}

模式

基本模式

接受接口类型参数函数或者方法

1
2
3
4
type YourInterfaceType interface {
M1()
M2()
}

image-20231122234344323

创建模式

接受接口,返回结构体 - accept interfaces, return structs

NewXXX - 大多数包含接口类型字段的结构体的实例化,都可以使用创建模式实现

Case 1

sync/cond.go
1
2
3
4
5
6
7
8
9
10
11
12
13
type Cond struct {
noCopy noCopy

// L is held while observing or changing the condition
L Locker

notify notifyList
checker copyChecker
}

func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
sync/mutex.go
1
2
3
4
type Locker interface {
Lock()
Unlock()
}

Case 2

log/log.go
1
2
3
4
5
6
7
8
9
10
11
12
type Logger struct {
mu sync.Mutex // ensures atomic writes; protects the following fields
prefix string // prefix on each line to identify the logger (but see Lmsgprefix)
flag int // properties
out io.Writer // destination for output
buf []byte // for accumulating text to write
}

// 接受 io.Writer,返回 *Logger
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
io/io.go
1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

包装器模式

返回值类型参数类型相同

1
func YourWrapperFunc(param YourInterfaceType) YourInterfaceType

实现对输入参数的类型的包装
不改变被包装类型的定义的情况下,返回具备新功能特性、实现相同接口类型的新类型

io/io.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return
}
1
2
3
4
5
6
7
8
func main() {
r := strings.NewReader("hello, go!")
lr := io.LimitReader(r, 1<<2)
_, err := io.Copy(os.Stdout, lr)
if err != nil {
log.Fatal(err) // hell
}
}

支持链式调用

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
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
}

for i, v := range bytes.ToUpper(p) {
p[i] = v
}
return n, err
}

func CapReader(r io.Reader) io.Reader {
return &CapitalizedReader{r: r}
}

func main() {
r := strings.NewReader("hello, go!")
cr := CapReader(io.LimitReader(r, 1<<2))
_, err := io.Copy(os.Stdout, cr)
if err != nil {
log.Fatal(err) // HELL
}
}

适配器模式

核心为适配器函数类型(Adaptor Function Type),它是一个工具类型

将一个满足特定函数签名普通函数,显式转换成自身类型的实例,转换后的实例同时也是某个接口类型实现者

1
2
3
4
5
6
7
8
9
10
11
// greetings 的类型为 func(http.ResponseWriter, *http.Request)
func greetings(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello World!")
}

// HandlerFunc 是一个类型
// HandlerFunc 的底层类型与 greetings 的类型一致,因此可以进行强制转换
// 而且 HandlerFunc 实现了 Handler 接口
func main() {
_ = http.ListenAndServe(":8080", http.HandlerFunc(greetings))
}
net/http/server.go
1
2
3
4
5
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
net/http/server.go
1
2
3
4
5
6
7
8
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

适配器函数类型(http.HandlerFunc)将一个普通函数转型为实现了 http.Handler 接口的类型的实例

中间件模式

在 Go Web 中,中间件常指实现了 http.Handler 接口的 http.HandlerFunc 类型实例

中间件模式结合了包装器模式适配器模式

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
func validateAuth(s string) error {
if s != "12345" {
return fmt.Errorf("%s", "bad auth token")
}
return nil
}

func greetings(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Hello World!")
}

func logHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t)

h.ServeHTTP(w, r)
})
}

func authHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := validateAuth(r.Header.Get("auth"))
if err != nil {
http.Error(w, "bad auth param", http.StatusUnauthorized)
return
}

h.ServeHTTP(w, r)
})
}

func main() {
_ = http.ListenAndServe(":7777", logHandler(authHandler(http.HandlerFunc(greetings))))
}

所谓中间件,本质为一个包装函数,最里面利用了适配器函数类型(http.HandlerFunc)

1
2
3
4
5
$ curl 127.1:7777
bad auth param

$ curl -H 'auth:12345' 127.1:7777
Hello World!

空接口

尽量避免使用空接口作为函数参数类型

  1. 空接口不提供任何信息
  2. 虽然 Go 无需显式 implement 接口,但必须声明接口
    • 可以让种类繁多的类型与接口匹配,包括无法编辑的库代码
    • 兼顾安全性灵活性,而安全性由 Go 编译器来保证(需要接口类型的定义
io/io.go
1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}
  1. Go 编译器通过解析接口定义,得到接口的名字以及方法集合
    • 在为该接口类型参数赋值时,编译器会根据这些信息对实参进行检查
  2. 如果函数或者方法的参数类型为空接口(interface{}
    • 此时并没有为编译器提供关于传入实参数据的任何信息
    • 从而失去静态类型语言类型安全检查保护屏障 - 可能会到运行时才能发现错误
  3. 因此,尽量抽象出带有一定行为契约的接口,并将其作为函数或者方法的参数类型
  4. 使用空接口作为函数或者方法的参数类型的场景
    • 面对未知类型的数据,借用了具有『泛型能力』的 interface{}
    • Go 泛型落地后,interface{} 可以被慢慢替代,正如 Java 中很少直接使用 Object 一样