关于 “Go 语言设计模式” 系列
这个系列首先是关于 Go 语言实践的。在项目中实际使用 Go 语言也有段时间了,一个体会就是不论是官方文档、图书还是网络资料,关于 Go 语言惯用法(idiom)的介绍都比较少,基本只能靠看标准库源代码自己琢磨,所以我特别想在这方面有一些收集和总结。
然后这个系列也是关于设计模式的。虽然 Go 语言不是一门面向对象编程语言,但是很多面向对象设计模式所要解决的问题是在程序设计中客观存在的。不管用什么语言,总是要面对和解决这些问题的,只是解决的思路和途径会有所不同。所以我想就以经典的设计模式作为切入点来展开这个系列,毕竟大家对设计模式都很熟悉了,可以避免无中生有想出一些蹩脚的应用场景。
本系列的具体主题会比较灵活,计划主要包括这些方面的话题:
- Go 语言惯用法。
- 设计模式的实现。特别是引入了闭包,协程,DuckType 等语言特性后带来的变化。
- 设计模式思想的探讨。会有一些吐槽。
不使用迭代器的方案
**首先要指出的是,绝大多数情况下 Go 程序是不需要用迭代器的。**因为内置的 slice 和 map 两种容器都可以通过 range 进行遍历,并且这两种容器在性能方面做了足够的优化。只要没有特殊的需求,通常是直接用这两种容器解决问题。即使不得不写了一个自定义容器,我们几乎总是可以实现一个函数,把所有元素(的引用)拷贝到一个 slice 之后返回,这样调用者又可以直接用 range 进行遍历了。
当然某些特殊场合迭代器还是有用武之地。比如迭代器的 Next() 是个耗时操作,不能一口气拷贝所有元素;再比如某些条件下需要中断遍历。
经典实现
经典实现完全采用面向对象的思路。为了简化问题,下面的例子中容器就是简单的[]int
,我们在 main
函数中使用迭代器进行遍历操作并打印取到的值,迭代器的接口设计参考 java。
1package main
2
3import "fmt"
4
5type Ints []int
6
7func (i Ints) Iterator() *Iterator {
8 return &Iterator{
9 data: i,
10 index: 0,
11 }
12}
13
14type Iterator struct {
15 data Ints
16 index int
17}
18
19func (i *Iterator) HasNext() bool {
20 return i.index < len(i.data)
21}
22
23func (i *Iterator) Next() (v int) {
24 v = i.data[i.index]
25 i.index++
26 return v
27}
28
29func main() {
30 ints := Ints{1, 2, 3}
31 for it := ints.Iterator(); it.HasNext(); {
32 fmt.Println(it.Next())
33 }
34}
闭包实现
Go 语言支持 first class functions、高阶函数、闭包、多返回值函数。用上这些特性可以换种方式实现迭代器。
初看之下闭包实现与经典实现完全不同,其实从本质上来看,二者区别不大。经典实现中把迭代器需要的数据存在 struct 中,HasNext()
Next()
两个函数定义为 Iterator
的方法从而和数据绑定了起来;闭包实现中迭代器是一个匿名函数,它所需要的数据 i Ints
和 index
以闭包 upvalue 的形式绑定了起来,匿名函数返回的两个值正好对应经典实现中的 Next()
和 HasNext()
。
1package main
2
3import "fmt"
4
5type Ints []int
6
7func (i Ints) Iterator() func() (int, bool) {
8 index := 0
9 return func() (val int, ok bool) {
10 if index >= len(i) {
11 return
12 }
13
14 val, ok = i[index], true
15 index++
16 return
17 }
18}
19
20func main() {
21 ints := Ints{1, 2, 3}
22 it := ints.Iterator()
23 for {
24 val, ok := it()
25 if !ok {
26 break
27 }
28 fmt.Println(val)
29 }
30}
channel 实现
这份实现是最 go way 的,使用了一个 channel 在新的 goroutine 中将容器内的元素依次输出。优点是 channel 是可以用 range 接收的,所以调用方代码很简洁;缺点是 goroutine 上下文切换会有开销,这份实现无疑是最低效的,另外调用方必须接收完所有数据,如果只接收一半就中断掉发送方将永远阻塞。
依稀记得在邮件列表里看到说标准库里有这个用法的例子,刚才去翻了下没找到原帖了:-)
顺便说一下,“在函数中创建一个 channel 返回,同时创建一个 goroutine 往 channel 中塞数据”这是一个重要的惯用法(Channel Factory pattern,见 the way to go 18.8 节),可以用来做序列发生器、fan-out、fan-in等。
1package main
2
3import "fmt"
4
5type Ints []int
6
7func (i Ints) Iterator() <-chan int {
8 c := make(chan int)
9 go func() {
10 for _, v := range i {
11 c <- v
12 }
13 close(c)
14 }()
15 return c
16}
17
18func main() {
19 ints := Ints{1, 2, 3}
20 for v := range ints.Iterator() {
21 fmt.Println(v)
22 }
23}
Do 实现
这份迭代器实现是最简洁的,代码也很直白,无须多言。如果想加上中断迭代的功能,可以将func(int)
改为 func(int)bool
,Do 中根据返回值决定是否退出迭代。
标准库中的 container/ring
中有 Do() 用法的例子。
1package main
2
3import "fmt"
4
5type Ints []int
6
7func (i Ints) Do(fn func(int)) {
8 for _, v := range i {
9 fn(v)
10 }
11}
12
13func main() {
14 ints := Ints{1, 2, 3}
15 ints.Do(func(v int) {
16 fmt.Println(v)
17 })
18}
总结
- Go 语言中没有 class 和继承,不具备完整表达面向对象的能力,不是一门通常意义上的面向对象语言。但是这不妨碍 Go 语言实现面向对象的思想,利用其语言特性,实现封装、组合、多态都没有问题。
- 设计模式的精髓在于思想而不在于类图。编程语言是在不断进步的,类图却一直用几十年前那一张,抛开类图重新审视问题,合理利用语言新特性可以得到更简洁的设计模式实现。