This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,348 @@
<audio id="audio" title="107 | Go编程模式切片、接口、时间和性能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/55/9c/55f560098399dc84977e3d6b814d129c.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
今天是我们的第一节课我先带你学习下Go语言编程模式的一些基本技术和要点。了解了这些内容你就可以更轻松地掌握Go语言编程了其中主要包括数组切片的一些小坑、接口编程以及时间和程序运行性能相关的内容。
话不多说,我们直接开始。
## Slice
首先我来介绍下Slice中文翻译叫“切片”这个东西在Go语言中不是数组而是一个结构体其定义如下
```
type slice struct {
array unsafe.Pointer //指向存放数据的数组指针
len int //长度有多大
cap int //容量有多大
}
```
一个空的Slice的表现如下图所示
<img src="https://static001.geekbang.org/resource/image/ea/80/eac9bc362064f5cba58d663d3dde8780.png" alt="">
熟悉C/C++的同学一定会知道在结构体里用数组指针的问题——**数据会发生共享**下面我们来看看Slice的一些操作
```
foo = make([]int, 5)
foo[3] = 42
foo[4] = 100
bar := foo[1:4]
bar[1] = 99
```
我来解释下这段代码:
- 首先创建一个foo的Slice其中的长度和容量都是5
- 然后开始对foo所指向的数组中的索引为3和4的元素进行赋值
- 最后对foo做切片后赋值给bar再修改bar[1]。
为了方便你理解,我画了一张图:
<img src="https://static001.geekbang.org/resource/image/fb/c6/fb0574yye57002dfc435efe9db3c88c6.png" alt="">
从这张图片中我们可以看到因为foo和bar的内存是共享的所以foo和bar对数组内容的修改都会影响到对方。
接下来,我们再来看一个数据操作 `append()` 的示例:
```
a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42
```
在这段代码中,把 `a[1:16]` 的切片赋给 `b` ,此时,`a``b` 的内存空间是共享的,然后,对 `a` 做了一个 `append()`的操作,这个操作会让 `a` 重新分配内存,这就会导致 `a``b` 不再共享,如下图所示:
<img src="https://static001.geekbang.org/resource/image/9a/13/9a29d71d309616f6092f6bea23f30013.png" alt="">
从图中,我们可以看到,`append()`操作让 `a` 的容量变成了64而长度是33。这里你需要重点注意一下**`append()`这个函数在 `cap` 不够用的时候,就会重新分配内存以扩大容量,如果够用,就不会重新分配内存了**
我们再来看一个例子:
```
func main() {
path := []byte(&quot;AAAA/BBBBBBBBB&quot;)
sepIndex := bytes.IndexByte(path,'/')
dir1 := path[:sepIndex]
dir2 := path[sepIndex+1:]
fmt.Println(&quot;dir1 =&gt;&quot;,string(dir1)) //prints: dir1 =&gt; AAAA
fmt.Println(&quot;dir2 =&gt;&quot;,string(dir2)) //prints: dir2 =&gt; BBBBBBBBB
dir1 = append(dir1,&quot;suffix&quot;...)
fmt.Println(&quot;dir1 =&gt;&quot;,string(dir1)) //prints: dir1 =&gt; AAAAsuffix
fmt.Println(&quot;dir2 =&gt;&quot;,string(dir2)) //prints: dir2 =&gt; uffixBBBB
}
```
在这个例子中,`dir1``dir2` 共享内存,虽然 `dir1` 有一个 `append()` 操作,但是因为 cap 足够,于是数据扩展到了`dir2` 的空间。下面是相关的图示(注意上图中 `dir1``dir2` 结构体中的 `cap``len` 的变化):
<img src="https://static001.geekbang.org/resource/image/17/aa/1727ca49dfe2e6a73627a52a899535aa.png" alt="">
如果要解决这个问题,我们只需要修改一行代码。我们要把代码
```
dir1 := path[:sepIndex]
```
修改为:
```
dir1 := path[:sepIndex:sepIndex]
```
新的代码使用了 Full Slice Expression最后一个参数叫“Limited Capacity”于是后续的 `append()` 操作会导致重新分配内存。
## 深度比较
当我们复制一个对象时这个对象可以是内建数据类型、数组、结构体、Map……在复制结构体的时候如果我们需要比较两个结构体中的数据是否相同就要使用深度比较而不只是简单地做浅度比较。这里需要使用到反射 `reflect.DeepEqual()` ,下面是几个示例:
```
import (
&quot;fmt&quot;
&quot;reflect&quot;
)
func main() {
v1 := data{}
v2 := data{}
fmt.Println(&quot;v1 == v2:&quot;,reflect.DeepEqual(v1,v2))
//prints: v1 == v2: true
m1 := map[string]string{&quot;one&quot;: &quot;a&quot;,&quot;two&quot;: &quot;b&quot;}
m2 := map[string]string{&quot;two&quot;: &quot;b&quot;, &quot;one&quot;: &quot;a&quot;}
fmt.Println(&quot;m1 == m2:&quot;,reflect.DeepEqual(m1, m2))
//prints: m1 == m2: true
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println(&quot;s1 == s2:&quot;,reflect.DeepEqual(s1, s2))
//prints: s1 == s2: true
}
```
## 接口编程
下面,我们来看段代码,其中是两个方法,它们都是要输出一个结构体,其中一个使用一个函数,另一个使用一个“成员函数”。
```
func PrintPerson(p *Person) {
fmt.Printf(&quot;Name=%s, Sexual=%s, Age=%d\n&quot;,
p.Name, p.Sexual, p.Age)
}
func (p *Person) Print() {
fmt.Printf(&quot;Name=%s, Sexual=%s, Age=%d\n&quot;,
p.Name, p.Sexual, p.Age)
}
func main() {
var p = Person{
Name: &quot;Hao Chen&quot;,
Sexual: &quot;Male&quot;,
Age: 44,
}
PrintPerson(&amp;p)
p.Print()
}
```
你更喜欢哪种方式呢?在 Go 语言中使用“成员函数”的方式叫“Receiver”这种方式是一种封装因为 `PrintPerson()`本来就是和 `Person`强耦合的,所以理应放在一起。更重要的是,这种方式可以进行接口编程,对于接口编程来说,也就是一种抽象,主要是用在“多态”,这个技术,我在《[Go语言简介接口与多态](https://coolshell.cn/articles/8460.html#%E6%8E%A5%E5%8F%A3%E5%92%8C%E5%A4%9A%E6%80%81)》中讲过,你可以点击链接查看。
在这里我想讲另一个Go语言接口的编程模式。
首先,我们来看一段代码:
```
type Country struct {
Name string
}
type City struct {
Name string
}
type Printable interface {
PrintStr()
}
func (c Country) PrintStr() {
fmt.Println(c.Name)
}
func (c City) PrintStr() {
fmt.Println(c.Name)
}
c1 := Country {&quot;China&quot;}
c2 := City {&quot;Beijing&quot;}
c1.PrintStr()
c2.PrintStr()
```
可以看到,这段代码中使用了一个 `Printable` 的接口,而 `Country``City` 都实现了接口方法 `PrintStr()` 把自己输出。然而,这些代码都是一样的,能不能省掉呢?
其实,我们可以使用“结构体嵌入”的方式来完成这个事,如下所示:
```
type WithName struct {
Name string
}
type Country struct {
WithName
}
type City struct {
WithName
}
type Printable interface {
PrintStr()
}
func (w WithName) PrintStr() {
fmt.Println(w.Name)
}
c1 := Country {WithName{ &quot;China&quot;}}
c2 := City { WithName{&quot;Beijing&quot;}}
c1.PrintStr()
c2.PrintStr()
```
引入一个叫 `WithName`的结构体,但是这会带来一个问题:在初始化的时候变得有点乱。那么,有没有更好的方法呢?再来看另外一个解。
```
type Country struct {
Name string
}
type City struct {
Name string
}
type Stringable interface {
ToString() string
}
func (c Country) ToString() string {
return &quot;Country = &quot; + c.Name
}
func (c City) ToString() string{
return &quot;City = &quot; + c.Name
}
func PrintStr(p Stringable) {
fmt.Println(p.ToString())
}
d1 := Country {&quot;USA&quot;}
d2 := City{&quot;Los Angeles&quot;}
PrintStr(d1)
PrintStr(d2)
```
在这段代码中,我们可以看到,**我们使用了一个叫**`Stringable` 的接口,我们用这个接口把“业务类型” `Country``City` 和“控制逻辑” `Print()` 给解耦了。于是,只要实现了`Stringable` 接口,都可以传给 `PrintStr()` 来使用。
这种编程模式在Go 的标准库有很多的示例,最著名的就是 `io.Read``ioutil.ReadAll` 的玩法,其中 `io.Read` 是一个接口,你需要实现它的一个 `Read(p []byte) (n int, err error)` 接口方法,只要满足这个规则,就可以被 `ioutil.ReadAll`这个方法所使用。**这就是面向对象编程方法的黄金法则——“Program to an interface not an implementation”。**
## 接口完整性检查
另外我们可以看到Go语言的编译器并没有严格检查一个对象是否实现了某接口所有的接口方法如下面这个示例
```
type Shape interface {
Sides() int
Area() int
}
type Square struct {
len int
}
func (s* Square) Sides() int {
return 4
}
func main() {
s := Square{len: 5}
fmt.Printf(&quot;%d\n&quot;,s.Sides())
}
```
可以看到,`Square` 并没有实现 `Shape` 接口的所有方法,程序虽然可以跑通,但是这样的编程方式并不严谨,如果我们需要强制实现接口的所有方法,那该怎么办呢?
在Go语言编程圈里有一个比较标准的做法
```
var _ Shape = (*Square)(nil)
```
声明一个 `_` 变量(没人用)会把一个 `nil` 的空指针从 `Square` 转成 `Shape`,这样,如果没有实现完相关的接口方法,编译器就会报错:
>
**cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)**
这样就做到了强验证的方法。
## 时间
对于时间来说,这应该是编程中比较复杂的问题了,相信我,时间是一种非常复杂的事(比如《[你确信你了解时间吗?](https://coolshell.cn/articles/5075.html)》《[关于闰秒](https://coolshell.cn/articles/7804.html)》等文章)。而且,时间有时区、格式、精度等问题,其复杂度不是一般人能处理的。所以,一定要重用已有的时间处理,而不是自己干。
在 Go 语言中,你一定要使用 `time.Time``time.Duration` 这两个类型。
- 在命令行上,`flag` 通过 `time.ParseDuration` 支持了 `time.Duration`
- JSON 中的 `encoding/json` 中也可以把`time.Time` 编码成 [RFC 3339](https://tools.ietf.org/html/rfc3339) 的格式。
- 数据库使用的 `database/sql` 也支持把 `DATATIME``TIMESTAMP` 类型转成 `time.Time`
- YAML也可以使用 `gopkg.in/yaml.v2` 支持 `time.Time``time.Duration` 和 [RFC 3339](https://tools.ietf.org/html/rfc3339) 格式。
如果你要和第三方交互,实在没有办法,也请使用 [RFC 3339](https://tools.ietf.org/html/rfc3339) 的格式。
最后如果你要做全球化跨时区的应用一定要把所有服务器和时间全部使用UTC时间。
## 性能提示
Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面我给你提供一份在编程方面和性能相关的提示。
- 如果需要把数字转换成字符串,使用 `strconv.Itoa()``fmt.Sprintf()` 要快一倍左右。
- 尽可能避免把`String`转成`[]Byte` ,这个转换会导致性能下降。
- 如果在for-loop里对某个Slice 使用 `append()`,请先把 Slice的容量扩充到位这样可以避免内存重新分配以及系统自动按2的N次方幂进行扩展但又用不到的情况从而避免浪费内存。
- 使用`StringBuffer` 或是`StringBuild` 来拼接字符串,性能会比使用 `+``+=`高三到四个数量级。
- 尽可能使用并发的 goroutine然后使用 `sync.WaitGroup` 来同步分片操作。
- 避免在热代码中进行内存分配这样会导致gc很忙。尽可能使用 `sync.Pool` 来重用对象。
- 使用 lock-free的操作避免使用 mutex尽可能使用 `sync/Atomic`包(关于无锁编程的相关话题,可参看《[无锁队列实现](https://coolshell.cn/articles/8239.html)》或《[无锁Hashmap实现](https://coolshell.cn/articles/9703.html)》)。
- 使用 I/O缓冲I/O是个非常非常慢的操作使用 `bufio.NewWrite()``bufio.NewReader()` 可以带来更高的性能。
- 对于在for-loop里的固定的正则表达式一定要使用 `regexp.Compile()` 编译正则表达式。性能会提升两个数量级。
- 如果你需要更高性能的协议,就要考虑使用 [protobuf](https://github.com/golang/protobuf) 或 [msgp](https://github.com/tinylib/msgp) 而不是JSON因为JSON的序列化和反序列化里使用了反射。
- 你在使用Map的时候使用整型的key会比字符串的要快因为整型比较比字符串比较要快。
## 参考文档
其实还有很多不错的技巧我给你推荐一些参考文档它们可以帮助你写出更好的Go的代码必读
- [**Effective Go**](https://golang.org/doc/effective_go.html)
- [**Uber Go Style**](https://github.com/uber-go/guide/blob/master/style.md)
- [**50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs**](http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/)
- [**Go Advice**](https://github.com/cristaloleg/go-advice)
- [**Practical Go Benchmarks**](https://www.instana.com/blog/practical-golang-benchmarks/)
- [**Benchmarks of Go serialization methods**](https://github.com/alecthomas/go_serialization_benchmarks)
- [**Debugging performance issues in Go programs**](https://github.com/golang/go/wiki/Performance)
- [**Go code refactoring: the 23x performance hunt**](https://medium.com/@val_deleplace/go-code-refactoring-the-23x-performance-hunt-156746b522f7)
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,380 @@
<audio id="audio" title="108 | Go 编程模式:错误处理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/02/ae/02516ec122190d0e5aa7210ed2761dae.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
错误处理一直是编程必须要面对的问题。错误处理如果做得好的话代码的稳定性会很好。不同的语言有不同的错误处理的方式。Go语言也一样这节课我们来讨论一下Go语言的错误出处尤其是那令人抓狂的 `if err != nil`
在正式讨论“Go代码里满屏的 `if err != nil` 怎么办”这件事儿之前,我想先说一说编程中的错误处理。
## C语言的错误检查
首先,我们知道,处理错误最直接的方式是通过错误码,这也是传统的方式,在过程式语言中通常都是用这样的方式处理错误的。比如 C 语言,基本上来说,其通过函数的返回值标识是否有错,然后通过全局的 `errno` 变量加一个 `errstr` 的数组来告诉你为什么出错。
为什么是这样的设计呢?道理很简单,除了可以共用一些错误,更重要的是这其实是一种妥协,比如:`read()``write()``open()` 这些函数的返回值其实是返回有业务逻辑的值,也就是说,这些函数的返回值有两种语义:
1. 一种是成功的值,比如 `open()` 返回的文件句柄指针 `FILE*`
1. 另一种是错误 `NULL`。这会导致调用者并不知道是什么原因出错了,需要去检查 `errno` 以获得出错的原因,从而正确地处理错误。
一般而言,这样的错误处理方式在大多数情况下是没什么问题的,不过也有例外的情况,我们来看一下下面这个 C 语言的函数:
```
int atoi(const char *str)
```
这个函数是把一个字符串转成整型。但是问题来了,如果一个要转的字符串是非法的(不是数字的格式),如 “ABC” 或者整型溢出了,那么这个函数应该返回什么呢?出错返回,返回什么数都不合理,因为这会和正常的结果混淆在一起。比如,如果返回 `0`,就会和正常的对 “0” 字符的返回值完全混淆在一起,这样就无法判断出错的情况了。你可能会说,是不是要检查一下 `errno`呢?按道理说应该是要去检查的,但是,我们在 C99 的规格说明书中可以看到这样的描述:
>
**7.20.1The functions atof, atoi, atol, and atoll need not affect the value of the integer expression errno on an error. If the value of the result cannot be represented, the behavior is undefined.**
`atoi()``atof()``atol()``atoll()` 这样的函数,是不会设置 `errno`而且如果结果无法计算的话行为是undefined。所以后来libc 又给出了一个新的函数`strtol()`,这个函数在出错的时候会设置全局变量 `errno`
```
long val = strtol(in_str, &amp;endptr, 10); //10的意思是10进制
//如果无法转换
if (endptr == str) {
fprintf(stderr, &quot;No digits were found\n&quot;);
exit(EXIT_FAILURE);
}
//如果整型溢出了
if ((errno == ERANGE &amp;&amp; (val == LONG_MAX || val == LONG_MIN)) {
fprintf(stderr, &quot;ERROR: number out of range for LONG\n&quot;);
exit(EXIT_FAILURE);
}
//如果是其它错误
if (errno != 0 &amp;&amp; val == 0) {
perror(&quot;strtol&quot;);
exit(EXIT_FAILURE);
}
```
虽然,`strtol()` 函数解决了 `atoi()` 函数的问题,但是我们还是能感觉到不是很舒服,也不是很自然。
因为这种用返回值 + errno 的错误检查方式会有一些问题:
- 程序员一不小心就会忘记检查返回值,从而造成代码的 Bug
- 函数接口非常不纯洁,正常值和错误值混淆在一起,导致语义有问题。
所以后来有一些类库就开始区分这样的事情。比如Windows 的系统调用开始使用 `HRESULT` 的返回来统一错误的返回值,这样可以明确函数调用时的返回值是成功还是错误。但这样一来,函数的 input 和 output 只能通过函数的参数来完成,于是就出现了所谓的“入参”和“出参”这样的区别。
然而,这又使得函数接入中参数的语义变得很复杂,一些参数是入参,一些参数是出参,函数接口变得复杂了一些。而且,依然没有解决函数的成功或失败可以被人为忽略的问题。
## Java的错误处理
Java语言使用 `try-catch-finally` 通过使用异常的方式来处理错误其实这比起C语言的错误处理进了一大步使用抛异常和抓异常的方式可以让我们的代码有这样一些好处。
- 函数接口在 input参数和 output返回值以及错误处理的语义是比较清楚的。
- 正常逻辑的代码可以跟错误处理和资源清理的代码分开,提高了代码的可读性。
- 异常不能被忽略(如果要忽略也需要 catch 住,这是显式忽略)。
- 在面向对象的语言中(如 Java异常是个对象所以可以实现多态式的 catch。
- 与状态返回码相比,异常捕捉有一个显著的好处,那就是函数可以嵌套调用,或是链式调用,比如:
```
int x = add(a, div(b,c));
Pizza p = PizzaBuilder().SetSize(sz).SetPrice(p)...;
```
## Go语言的错误处理
Go 语言的函数支持多返回值所以可以在返回接口把业务语义业务返回值和控制语义出错返回值区分开。Go 语言的很多函数都会返回 result、err 两个值,于是就有这样几点:
- 参数上基本上就是入参,而返回接口把结果和错误分离,这样使得函数的接口语义清晰;
- 而且Go 语言中的错误参数如果要忽略,需要显式地忽略,用 _ 这样的变量来忽略;
- 另外,因为返回的 `error` 是个接口(其中只有一个方法 `Error()`,返回一个 `string` ),所以你可以扩展自定义的错误处理。
另外,如果一个函数返回了多个不同类型的 `error`,你也可以使用下面这样的方式:
```
if err != nil {
switch err.(type) {
case *json.SyntaxError:
...
case *ZeroDivisionError:
...
case *NullPointerError:
...
default:
...
}
}
```
我们可以看到,**Go语言的错误处理的方式本质上是返回值检查但是它也兼顾了异常的一些好处——对错误的扩展**。
## 资源清理
出错后是需要做资源清理的,不同的编程语言有不同的资源清理的编程模式。
- C语言使用的是 `goto fail;` 的方式到一个集中的地方进行清理(给你推荐一篇有意思的文章《[由苹果的低级BUG想到的](https://coolshell.cn/articles/11112.html)》,你可以点击链接看一下)。
- C++语言:一般来说使用 [RAII模式](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization),通过面向对象的代理模式,把需要清理的资源交给一个代理类,然后再析构函数来解决。
- Java语言可以在finally 语句块里进行清理。
- Go语言使用 `defer` 关键词进行清理。
下面是一个Go语言的资源清理的示例
```
func Close(c io.Closer) {
err := c.Close()
if err != nil {
log.Fatal(err)
}
}
func main() {
r, err := Open(&quot;a&quot;)
if err != nil {
log.Fatalf(&quot;error opening 'a'\n&quot;)
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
r, err = Open(&quot;b&quot;)
if err != nil {
log.Fatalf(&quot;error opening 'b'\n&quot;)
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
}
```
## Error Check Hell
好了,说到 Go 语言的 `if err !=nil` 的代码了,这样的代码的确是能让人写到吐。那么有没有什么好的方式呢?有的。我们先看一个令人崩溃的代码。
```
func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &amp;p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &amp;p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &amp;p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &amp;p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &amp;p.ElevationLoss); err != nil {
return nil, err
}
}
```
要解决这个事,我们可以用函数式编程的方式,如下代码示例:
```
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&amp;p.Longitude)
read(&amp;p.Latitude)
read(&amp;p.Distance)
read(&amp;p.ElevationGain)
read(&amp;p.ElevationLoss)
if err != nil {
return &amp;p, err
}
return &amp;p, nil
}
```
从这段代码中我们可以看到我们通过使用Closure 的方式把相同的代码给抽出来重新定义一个函数,这样大量的 `if err!=nil` 处理得很干净了,但是会带来一个问题,那就是有一个 `err` 变量和一个内部的函数,感觉不是很干净。
那么我们还能不能搞得更干净一点呢我们从Go 语言的 `bufio.Scanner()`中似乎可以学习到一些东西:
```
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
```
可以看到,`scanner`在操作底层的I/O的时候那个for-loop中没有任何的 `if err !=nil` 的情况,退出循环后有一个 `scanner.Err()` 的检查,看来使用了结构体的方式。模仿它,就可以对我们的代码进行重构了。
首先,定义一个结构体和一个成员函数:
```
type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
```
然后,我们的代码就可以变成下面这样:
```
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&amp;p.Longitude)
r.read(&amp;p.Latitude)
r.read(&amp;p.Distance)
r.read(&amp;p.ElevationGain)
r.read(&amp;p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &amp;p, nil
}
```
有了刚刚的这个技术,我们的“[流式接口 Fluent Interface](https://martinfowler.com/bliki/FluentInterface.html)”也就很容易处理了。如下所示:
```
package main
import (
&quot;bytes&quot;
&quot;encoding/binary&quot;
&quot;fmt&quot;
)
// 长度不够少一个Weight
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c}
var r = bytes.NewReader(b)
type Person struct {
Name [10]byte
Age uint8
Weight uint8
err error
}
func (p *Person) read(data interface{}) {
if p.err == nil {
p.err = binary.Read(r, binary.BigEndian, data)
}
}
func (p *Person) ReadName() *Person {
p.read(&amp;p.Name)
return p
}
func (p *Person) ReadAge() *Person {
p.read(&amp;p.Age)
return p
}
func (p *Person) ReadWeight() *Person {
p.read(&amp;p.Weight)
return p
}
func (p *Person) Print() *Person {
if p.err == nil {
fmt.Printf(&quot;Name=%s, Age=%d, Weight=%d\n&quot;,p.Name, p.Age, p.Weight)
}
return p
}
func main() {
p := Person{}
p.ReadName().ReadAge().ReadWeight().Print()
fmt.Println(p.err) // EOF 错误
}
```
相信你应该看懂这个技巧了,不过,需要注意的是,它的使用场景是有局限的,也就只能在对于同一个业务对象的不断操作下可以简化错误处理,如果是多个业务对象,还是得需要各种 `if err != nil`的方式。
## 包装错误
最后,多说一句,我们需要包装一下错误,而不是干巴巴地把`err`返回到上层,我们需要把一些执行的上下文加入。
通常来说,我们会使用 `fmt.Errorf()`来完成这个事,比如:
```
if err != nil {
return fmt.Errorf(&quot;something failed: %v&quot;, err)
}
```
另外在Go语言的开发者中更为普遍的做法是将错误包装在另一个错误中同时保留原始内容
```
type authorizationError struct {
operation string
err error // original error
}
func (e *authorizationError) Error() string {
return fmt.Sprintf(&quot;authorization failed during %s: %v&quot;, e.operation, e.err)
}
```
当然,更好的方式是通过一种标准的访问方法,这样,我们最好使用一个接口,比如 `causer`接口中实现 `Cause()` 方法来暴露原始错误,以供进一步检查:
```
type causer interface {
Cause() error
}
func (e *authorizationError) Cause() error {
return e.err
}
```
这里有个好消息是,这样的代码不必再写了,有一个第三方的[错误库](http://github.com/pkg/errors),对于这个库,我无论到哪儿都能看到它的存在,所以,这个基本上来说就是事实上的标准了。代码示例如下:
```
import &quot;github.com/pkg/errors&quot;
//错误包装
if err != nil {
return errors.Wrap(err, &quot;read failed&quot;)
}
// Cause接口
switch err := errors.Cause(err).(type) {
case *MyError:
// handle specifically
default:
// unknown error
}
```
## 参考文章
- [Golang Error Handling lesson by Rob Pike](http://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike)
- [Errors are values](https://blog.golang.org/errors-are-values)
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,251 @@
<audio id="audio" title="109 | Go 编程模式Functional Options" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/03/df6a839e657d22b7bfa944439a242003.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
这节课我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例编程技巧也很好是目前Go语言中最流行的一种编程模式。
但是,在正式讨论这个模式之前,我们先来看看要解决什么样的问题。
## 配置选项问题
在编程中,我们经常需要对一个对象(或是业务实体)进行相关的配置。比如下面这个业务实体(注意,这只是一个示例):
```
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
```
在这个 `Server` 对象中,我们可以看到:
- 要有侦听的IP地址 `Addr` 和端口号 `Port` 这两个配置选项是必填的当然IP地址和端口号都可以有默认值不过这里我们用于举例所以是没有默认值而且不能为空需要是必填的
- 然后,还有协议 `Protocol``Timeout``MaxConns` 字段这几个字段是不能为空的但是有默认值的比如协议是TCP超时`30`秒 和 最大链接数`1024`个。
- 还有一个 `TLS` ,这个是安全链接,需要配置相关的证书和私钥。这个是可以为空的。
所以,针对这样的配置,我们需要有多种不同的创建不同配置 `Server` 的函数签名,如下所示:
```
func NewDefaultServer(addr string, port int) (*Server, error) {
return &amp;Server{addr, port, &quot;tcp&quot;, 30 * time.Second, 100, nil}, nil
}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
return &amp;Server{addr, port, &quot;tcp&quot;, 30 * time.Second, 100, tls}, nil
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
return &amp;Server{addr, port, &quot;tcp&quot;, timeout, 100, nil}, nil
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
return &amp;Server{addr, port, &quot;tcp&quot;, 30 * time.Second, maxconns, tls}, nil
}
```
因为Go语言不支持重载函数所以你得用不同的函数名来应对不同的配置选项。
## 配置对象方案
要解决这个问题,最常见的方式是使用一个配置对象,如下所示:
```
type Config struct {
Protocol string
Timeout time.Duration
Maxconns int
TLS *tls.Config
}
```
我们把那些非必输的选项都移到一个结构体里,这样一来, `Server` 对象就会变成:
```
type Server struct {
Addr string
Port int
Conf *Config
}
```
于是,我们就只需要一个 `NewServer()` 的函数了,在使用前需要构造 `Config` 对象。
```
func NewServer(addr string, port int, conf *Config) (*Server, error) {
//...
}
//Using the default configuratrion
srv1, _ := NewServer(&quot;localhost&quot;, 9000, nil)
conf := ServerConfig{Protocol:&quot;tcp&quot;, Timeout: 60*time.Duration}
srv2, _ := NewServer(&quot;locahost&quot;, 9000, &amp;conf)
```
这段代码算是不错了,大多数情况下,我们可能就止步于此了。但是,对于有洁癖的、有追求的程序员来说,他们会看到其中不太好的一点,那就是`Config` 并不是必需的,所以,你需要判断是否是 `nil` 或是 Empty—— `Config{}`会让我们的代码感觉不太干净。
## Builder模式
如果你是一个Java程序员熟悉设计模式的一定会很自然地使用Builder模式。比如下面的代码
```
User user = new User.Builder()
.name(&quot;Hao Chen&quot;)
.email(&quot;haoel@hotmail.com&quot;)
.nickname(&quot;左耳朵&quot;)
.build();
```
仿照这个模式,我们可以把刚刚的代码改写成下面的样子(注:下面的代码没有考虑出错处理,其中关于出错处理的更多内容,你可以再回顾下[上节课](https://time.geekbang.org/column/article/332602)
```
//使用一个builder类来做包装
type ServerBuilder struct {
Server
}
func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {
sb.Server.Addr = addr
sb.Server.Port = port
//其它代码设置其它成员的默认值
return sb
}
func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
sb.Server.Protocol = protocol
return sb
}
func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {
sb.Server.MaxConns = maxconn
return sb
}
func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {
sb.Server.Timeout = timeout
return sb
}
func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {
sb.Server.TLS = tls
return sb
}
func (sb *ServerBuilder) Build() (Server) {
return sb.Server
}
```
这样一来,就可以使用这样的方式了:
```
sb := ServerBuilder{}
server, err := sb.Create(&quot;127.0.0.1&quot;, 8080).
WithProtocol(&quot;udp&quot;).
WithMaxConn(1024).
WithTimeOut(30*time.Second).
Build()
```
这种方式也很清楚不需要额外的Config类使用链式的函数调用的方式来构造一个对象只需要多加一个Builder类。你可能会觉得这个Builder类似乎有点多余我们似乎可以直接在`Server` 上进行这样的 Builder 构造,的确是这样的。但是,在处理错误的时候可能就有点麻烦,不如一个包装类更好一些。
如果我们想省掉这个包装的结构体就要请出Functional Options上场了函数式编程。
## Functional Options
首先,我们定义一个函数类型:
```
type Option func(*Server)
```
然后,我们可以使用函数式的方式定义一组如下的函数:
```
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
```
这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的 `Server` 参数。例如,当我们调用其中的一个函数 `MaxConns(30)` 时,其返回值是一个 `func(s* Server) { s.MaxConns = 30 }` 的函数。
这个叫高阶函数。在数学上,这有点像是计算长方形面积的公式为: `rect(width, height) = width * height;` 这个函数需要两个参数,我们包装一下,就可以变成计算正方形面积的公式:`square(width) = rect(width, width)` 。也就是说,`squre(width)`返回了另外一个函数,这个函数就是`rect(w,h)` ,只不过它的两个参数是一样的,即:`f(x) = g(x, x)`
好了,现在我们再定一个 `NewServer()`的函数,其中,有一个可变参数 `options` 它可以传出多个上面的函数然后使用一个for-loop来设置我们的 `Server` 对象。
```
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: &quot;tcp&quot;,
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&amp;srv)
}
//...
return &amp;srv, nil
}
```
于是,我们在创建 `Server` 对象的时候,就可以像下面这样:
```
s1, _ := NewServer(&quot;localhost&quot;, 1024)
s2, _ := NewServer(&quot;localhost&quot;, 2048, Protocol(&quot;udp&quot;))
s3, _ := NewServer(&quot;0.0.0.0&quot;, 8080, Timeout(300*time.Second), MaxConns(1000))
```
怎么样,是不是高度整洁和优雅?这不但解决了“使用 `Config` 对象方式的需要有一个config参数但在不需要的时候是放 `nil` 还是放 `Config{}`”的选择困难问题也不需要引用一个Builder的控制对象直接使用函数式编程在代码阅读上也很优雅。
所以以后你要玩类似的代码时我强烈推荐你使用Functional Options这种方式这种方式至少带来了6个好处
- 直觉式的编程;
- 高度的可配置化;
- 很容易维护和扩展;
- 自文档;
- 新来的人很容易上手;
- 没有什么令人困惑的事是nil 还是空)。
## 参考文档
- [Self referential functions and design](http://commandcenter.blogspot.com.au/2014/01/self-referential-functions-and-design.html) by Rob Pike
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,290 @@
<audio id="audio" title="110 | Go编程模式委托和反转控制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c5/d8/c574db2d68fefc0b649be52e5ddf76d8.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
控制反转([Inversion of Control](https://en.wikipedia.org/wiki/Inversion_of_control)[loC](https://en.wikipedia.org/wiki/Inversion_of_control) )是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。
我之前在《[IoC/DIP其实是一种管理思想](https://coolshell.cn/articles/9949.html)》这篇文章中,举过一个开关和电灯的例子。其实,这里的开关就是控制逻辑,电器是业务逻辑。我们不要在电器中实现开关,而是要把开关抽象成一种协议,让电器都依赖它。这样的编程方式可以有效降低程序复杂度,并提升代码重用度。
面向对象的设计模式我就不提了我们来看看Go语言使用Embed结构的一个示例。
## 嵌入和委托
### 结构体嵌入
在Go语言中我们可以很轻松地把一个结构体嵌到另一个结构体中如下所示
```
type Widget struct {
X, Y int
}
type Label struct {
Widget // Embedding (delegation)
Text string // Aggregation
}
```
在这个示例中,我们把 `Widget`嵌入到了 `Label` 中,于是,我们可以这样使用:
```
label := Label{Widget{10, 10}, &quot;State:&quot;}
label.X = 11
label.Y = 12
```
如果在`Label` 结构体里出现了重名,就需要解决重名问题,例如,如果成员 `X` 重名,我们就要用 `label.X`表明是自己的`X` ,用 `label.Wedget.X` 表明是嵌入过来的。
有了这样的嵌入我们就可以像UI组件一样在结构的设计上进行层层分解了。比如我可以新写出两个结构体 `Button``ListBox`
```
type Button struct {
Label // Embedding (delegation)
}
type ListBox struct {
Widget // Embedding (delegation)
Texts []string // Aggregation
Index int // Aggregation
}
```
### 方法重写
然后我们需要两个接口用Painter把组件画出来Clicker 用于表明点击事件。
```
type Painter interface {
Paint()
}
type Clicker interface {
Click()
}
```
当然,对于 `Lable` 来说,只有 `Painter` ,没有`Clicker`;对于 `Button``ListBox`来说,`Painter``Clicker`都有。
我们来看一些实现:
```
func (label Label) Paint() {
fmt.Printf(&quot;%p:Label.Paint(%q)\n&quot;, &amp;label, label.Text)
}
//因为这个接口可以通过 Label 的嵌入带到新的结构体,
//所以,可以在 Button 中重载这个接口方法
func (button Button) Paint() { // Override
fmt.Printf(&quot;Button.Paint(%s)\n&quot;, button.Text)
}
func (button Button) Click() {
fmt.Printf(&quot;Button.Click(%s)\n&quot;, button.Text)
}
func (listBox ListBox) Paint() {
fmt.Printf(&quot;ListBox.Paint(%q)\n&quot;, listBox.Texts)
}
func (listBox ListBox) Click() {
fmt.Printf(&quot;ListBox.Click(%q)\n&quot;, listBox.Texts)
}
```
说到这儿,我要重点提醒你一下,`Button.Paint()` 接口可以通过 Label 的嵌入带到新的结构体,如果 `Button.Paint()` 不实现的话,会调用 `Label.Paint()` ,所以,在 `Button` 中声明 `Paint()` 方法相当于Override。
### 嵌入结构多态
从下面的程序中,我们可以看到整个多态是怎么执行的。
```
button1 := Button{Label{Widget{10, 70}, &quot;OK&quot;}}
button2 := NewButton(50, 70, &quot;Cancel&quot;)
listBox := ListBox{Widget{10, 40},
[]string{&quot;AL&quot;, &quot;AK&quot;, &quot;AZ&quot;, &quot;AR&quot;}, 0}
for _, painter := range []Painter{label, listBox, button1, button2} {
painter.Paint()
}
for _, widget := range []interface{}{label, listBox, button1, button2} {
widget.(Painter).Paint()
if clicker, ok := widget.(Clicker); ok {
clicker.Click()
}
fmt.Println() // print a empty line
}
```
我们可以使用接口来多态,也可以使用泛型的 `interface{}` 来多态,但是需要有一个类型转换。
## 反转控制
我们再来看一个示例。
我们有一个存放整数的数据结构,如下所示:
```
type IntSet struct {
data map[int]bool
}
func NewIntSet() IntSet {
return IntSet{make(map[int]bool)}
}
func (set *IntSet) Add(x int) {
set.data[x] = true
}
func (set *IntSet) Delete(x int) {
delete(set.data, x)
}
func (set *IntSet) Contains(x int) bool {
return set.data[x]
}
```
其中实现了 `Add()``Delete()``Contains()` 三个操作,前两个是写操作,后一个是读操作。
### 实现Undo功能
现在,我们想实现一个 Undo 的功能。我们可以再包装一下 `IntSet` ,变成 `UndoableIntSet` ,代码如下所示:
```
type UndoableIntSet struct { // Poor style
IntSet // Embedding (delegation)
functions []func()
}
func NewUndoableIntSet() UndoableIntSet {
return UndoableIntSet{NewIntSet(), nil}
}
func (set *UndoableIntSet) Add(x int) { // Override
if !set.Contains(x) {
set.data[x] = true
set.functions = append(set.functions, func() { set.Delete(x) })
} else {
set.functions = append(set.functions, nil)
}
}
func (set *UndoableIntSet) Delete(x int) { // Override
if set.Contains(x) {
delete(set.data, x)
set.functions = append(set.functions, func() { set.Add(x) })
} else {
set.functions = append(set.functions, nil)
}
}
func (set *UndoableIntSet) Undo() error {
if len(set.functions) == 0 {
return errors.New(&quot;No functions to undo&quot;)
}
index := len(set.functions) - 1
if function := set.functions[index]; function != nil {
function()
set.functions[index] = nil // For garbage collection
}
set.functions = set.functions[:index]
return nil
}
```
我来解释下这段代码。
- 我们在 `UndoableIntSet` 中嵌入了`IntSet` 然后Override了 它的 `Add()``Delete()` 方法;
- `Contains()` 方法没有Override所以就被带到 `UndoableInSet` 中来了。
- 在Override的 `Add()`中,记录 `Delete` 操作;
- 在Override的 `Delete()` 中,记录 `Add` 操作;
- 在新加入的 `Undo()` 中进行Undo操作。
用这样的方式为已有的代码扩展新的功能是一个很好的选择。这样就可以在重用原有代码功能和新的功能中达到一个平衡。但是这种方式最大的问题是Undo操作其实是一种控制逻辑并不是业务逻辑所以在复用 Undo这个功能时是有问题的因为其中加入了大量跟 `IntSet` 相关的业务逻辑。
### 反转依赖
现在我们来看另一种方法。
我们先声明一种函数接口表示我们的Undo控制可以接受的函数签名是什么样的
```
type Undo []func()
```
有了这个协议之后我们的Undo控制逻辑就可以写成下面这样
```
func (undo *Undo) Add(function func()) {
*undo = append(*undo, function)
}
func (undo *Undo) Undo() error {
functions := *undo
if len(functions) == 0 {
return errors.New(&quot;No functions to undo&quot;)
}
index := len(functions) - 1
if function := functions[index]; function != nil {
function()
functions[index] = nil // For garbage collection
}
*undo = functions[:index]
return nil
}
```
看到这里,你不必觉得奇怪, `Undo` 本来就是一个类型,不必是一个结构体,是一个函数数组也没有什么问题。
然后我们在IntSet里嵌入 Undo接着在 `Add()``Delete()` 里使用刚刚的方法,就可以完成功能了。
```
type IntSet struct {
data map[int]bool
undo Undo
}
func NewIntSet() IntSet {
return IntSet{data: make(map[int]bool)}
}
func (set *IntSet) Undo() error {
return set.undo.Undo()
}
func (set *IntSet) Contains(x int) bool {
return set.data[x]
}
func (set *IntSet) Add(x int) {
if !set.Contains(x) {
set.data[x] = true
set.undo.Add(func() { set.Delete(x) })
} else {
set.undo.Add(nil)
}
}
func (set *IntSet) Delete(x int) {
if set.Contains(x) {
delete(set.data, x)
set.undo.Add(func() { set.Add(x) })
} else {
set.undo.Add(nil)
}
}
```
这个就是控制反转,不是由控制逻辑 `Undo` 来依赖业务逻辑 `IntSet`,而是由业务逻辑 `IntSet` 依赖 `Undo` 。这里依赖的是其实是一个协议,**这个协议是一个没有参数的函数数组。**可以看到,这样一来,我们 Undo 的代码就可以复用了。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,552 @@
<audio id="audio" title="111 | Go 编程模式Map-Reduce" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/78/d5402a5c228f0810697005dd686c9178.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
这节课我们来学习一下函数式编程中非常重要的Map、Reduce、Filter这三种操作。这三种操作可以让我们轻松灵活地进行一些数据处理毕竟我们的程序大多数情况下都在倒腾数据。尤其是对于一些需要统计的业务场景来说Map、Reduce、Filter是非常通用的玩法。
话不多说,我们先来看几个例子。
## 基本示例
### Map示例
在下面的程序代码中我写了两个Map函数这两个函数需要两个参数
- 一个是字符串数组 `[]` `string`,说明需要处理的数据是一个字符串;
- 另一个是一个函数func(s string) string 或 func(s string) int。
```
func MapStrToStr(arr []string, fn func(s string) string) []string {
var newArray = []string{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}
func MapStrToInt(arr []string, fn func(s string) int) []int {
var newArray = []int{}
for _, it := range arr {
newArray = append(newArray, fn(it))
}
return newArray
}
```
整个Map函数的运行逻辑都很相似函数体都是在遍历第一个参数的数组然后调用第二个参数的函数把它的值组合成另一个数组返回。
因此,我们就可以这样使用这两个函数:
```
var list = []string{&quot;Hao&quot;, &quot;Chen&quot;, &quot;MegaEase&quot;}
x := MapStrToStr(list, func(s string) string {
return strings.ToUpper(s)
})
fmt.Printf(&quot;%v\n&quot;, x)
//[&quot;HAO&quot;, &quot;CHEN&quot;, &quot;MEGAEASE&quot;]
y := MapStrToInt(list, func(s string) int {
return len(s)
})
fmt.Printf(&quot;%v\n&quot;, y)
//[3, 4, 8]
```
可以看到,我们给第一个 `MapStrToStr()` 传了功能为“转大写”的函数,于是出来的数组就成了全大写的,给`MapStrToInt()` 传的是计算长度,所以出来的数组是每个字符串的长度。
我们再来看一下Reduce和Filter的函数是什么样的。
### Reduce 示例
```
func Reduce(arr []string, fn func(s string) int) int {
sum := 0
for _, it := range arr {
sum += fn(it)
}
return sum
}
var list = []string{&quot;Hao&quot;, &quot;Chen&quot;, &quot;MegaEase&quot;}
x := Reduce(list, func(s string) int {
return len(s)
})
fmt.Printf(&quot;%v\n&quot;, x)
// 15
```
### Filter示例
```
func Filter(arr []int, fn func(n int) bool) []int {
var newArray = []int{}
for _, it := range arr {
if fn(it) {
newArray = append(newArray, it)
}
}
return newArray
}
var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {
return n%2 == 1
})
fmt.Printf(&quot;%v\n&quot;, out)
out = Filter(intset, func(n int) bool {
return n &gt; 5
})
fmt.Printf(&quot;%v\n&quot;, out)
```
为了方便你理解呢我给你展示一张图它形象地说明了Map-Reduce的业务语义在数据处理中非常有用。
<img src="https://static001.geekbang.org/resource/image/15/56/1506b63044071bfa5c214a725a9caf56.png" alt="">
## 业务示例
通过刚刚的一些示例你现在应该有点明白了Map、Reduce、Filter只是一种控制逻辑真正的业务逻辑是以传给它们的数据和函数来定义的。
是的,这是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。
接下来,我们来看一个有业务意义的代码,来进一步帮助你理解什么叫“控制逻辑”与“业务逻辑”分离。
### 员工信息
首先,我们有一个员工对象和一些数据:
```
type Employee struct {
Name string
Age int
Vacation int
Salary int
}
var list = []Employee{
{&quot;Hao&quot;, 44, 0, 8000},
{&quot;Bob&quot;, 34, 10, 5000},
{&quot;Alice&quot;, 23, 5, 9000},
{&quot;Jack&quot;, 26, 0, 4000},
{&quot;Tom&quot;, 48, 9, 7500},
{&quot;Marry&quot;, 29, 0, 6000},
{&quot;Mike&quot;, 32, 8, 4000},
}
```
### 相关的Reduce、Fitler函数
然后,我们有下面的几个函数:
```
func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {
count := 0
for i, _ := range list {
if fn(&amp;list[i]) {
count += 1
}
}
return count
}
func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {
var newList []Employee
for i, _ := range list {
if fn(&amp;list[i]) {
newList = append(newList, list[i])
}
}
return newList
}
func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {
var sum = 0
for i, _ := range list {
sum += fn(&amp;list[i])
}
return sum
}
```
简单说明一下:
- `EmployeeConutIf``EmployeeSumIf` 分别用于统计满足某个条件的个数或总数。它们都是Filter + Reduce的语义。
- `EmployeeFilterIn` 就是按某种条件过滤就是Fitler的语义。
### 各种自定义的统计示例
于是,我们就可以有接下来的代码了。
**1.统计有多少员工大于40岁**
```
old := EmployeeCountIf(list, func(e *Employee) bool {
return e.Age &gt; 40
})
fmt.Printf(&quot;old people: %d\n&quot;, old)
//old people: 2
```
**2.统计有多少员工的薪水大于6000**
```
high_pay := EmployeeCountIf(list, func(e *Employee) bool {
return e.Salary &gt; 6000
})
fmt.Printf(&quot;High Salary people: %d\n&quot;, high_pay)
//High Salary people: 4
```
**3.列出有没有休假的员工**
```
no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {
return e.Vacation == 0
})
fmt.Printf(&quot;People no vacation: %v\n&quot;, no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]
```
**4.统计所有员工的薪资总和**
```
total_pay := EmployeeSumIf(list, func(e *Employee) int {
return e.Salary
})
fmt.Printf(&quot;Total Salary: %d\n&quot;, total_pay)
//Total Salary: 43500
```
**5.统计30岁以下员工的薪资总和**
```
younger_pay := EmployeeSumIf(list, func(e *Employee) int {
if e.Age &lt; 30 {
return e.Salary
}
return 0
})
```
## 泛型Map-Reduce
刚刚的Map-Reduce都因为要处理数据的类型不同而需要写出不同版本的Map-Reduce虽然它们的代码看上去是很类似的。所以这里就要提到泛型编程了。
### 简单版 Generic Map
我在写这节课的时候Go语言还不支持泛型Go开发团队技术负责人Russ Cox在2012年11月21golang-dev上的mail确认了Go泛型将在Go 1.18版本落地时间是2022年2月。所以目前的Go语言的泛型只能用 `interface{}` + `reflect`来完成。`interface{}` 可以理解为C中的 `void*`、Java中的 `Object` `reflect`是Go的反射机制包作用是在运行时检查类型。
下面我们来看一下一个非常简单的、不做任何类型检查的泛型的Map函数怎么写。
```
func Map(data interface{}, fn interface{}) []interface{} {
vfn := reflect.ValueOf(fn)
vdata := reflect.ValueOf(data)
result := make([]interface{}, vdata.Len())
for i := 0; i &lt; vdata.Len(); i++ {
result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()
}
return result
}
```
我来简单解释下这段代码。
- 首先,我们通过 `reflect.ValueOf()` 获得 `interface{}` 的值,其中一个是数据 `vdata`,另一个是函数 `vfn`
- 然后,通过 `vfn.Call()` 方法调用函数,通过 `[]refelct.Value{vdata.Index(i)}`获得数据。
Go语言中的反射的语法有点令人费解不过简单看一下手册还是能够读懂的。反射不是这节课的重点我就不讲了。如果你还不太懂这些基础知识课下可以学习下相关的教程。
于是,我们就可以有下面的代码——不同类型的数据可以使用相同逻辑的`Map()`代码。
```
square := func(x int) int {
return x * x
}
nums := []int{1, 2, 3, 4}
squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]
upcase := func(s string) string {
return strings.ToUpper(s)
}
strs := []string{&quot;Hao&quot;, &quot;Chen&quot;, &quot;MegaEase&quot;}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]
```
但是,因为反射是运行时的事,所以,如果类型出问题的话,就会有运行时的错误。比如:
```
x := Map(5, 5)
fmt.Println(x)
```
代码可以很轻松地编译通过但是在运行时却出问题了而且还是panic错误……
```
panic: reflect: call of reflect.Value.Len on int Value
goroutine 1 [running]:
reflect.Value.Len(0x10b5240, 0x10eeb58, 0x82, 0x10716bc)
/usr/local/Cellar/go/1.15.3/libexec/src/reflect/value.go:1162 +0x185
main.Map(0x10b5240, 0x10eeb58, 0x10b5240, 0x10eeb60, 0x1, 0x14, 0x0)
/Users/chenhao/.../map.go:12 +0x16b
main.main()
/Users/chenhao/.../map.go:42 +0x465
exit status 2
```
### 健壮版的Generic Map
所以,如果要写一个健壮的程序,对于这种用`interface{}` 的“过度泛型”就需要我们自己来做类型检查。来看一个有类型检查的Map代码
```
func Transform(slice, function interface{}) interface{} {
return transform(slice, function, false)
}
func TransformInPlace(slice, function interface{}) interface{} {
return transform(slice, function, true)
}
func transform(slice, function interface{}, inPlace bool) interface{} {
//check the `slice` type is Slice
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic(&quot;transform: not slice&quot;)
}
//check the function signature
fn := reflect.ValueOf(function)
elemType := sliceInType.Type().Elem()
if !verifyFuncSignature(fn, elemType, nil) {
panic(&quot;trasform: function must be of type func(&quot; + sliceInType.Type().Elem().String() + &quot;) outputElemType&quot;)
}
sliceOutType := sliceInType
if !inPlace {
sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())
}
for i := 0; i &lt; sliceInType.Len(); i++ {
sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])
}
return sliceOutType.Interface()
}
func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {
//Check it is a funciton
if fn.Kind() != reflect.Func {
return false
}
// NumIn() - returns a function type's input parameter count.
// NumOut() - returns a function type's output parameter count.
if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {
return false
}
// In() - returns the type of a function type's i'th input parameter.
for i := 0; i &lt; len(types)-1; i++ {
if fn.Type().In(i) != types[i] {
return false
}
}
// Out() - returns the type of a function type's i'th output parameter.
outType := types[len(types)-1]
if outType != nil &amp;&amp; fn.Type().Out(0) != outType {
return false
}
return true
}
```
代码一下子就复杂起来了可见复杂的代码都是在处理异常的地方。我不打算Walk through 所有的代码,别看代码多,还是可以读懂的。
我来列一下代码中的几个要点。
1. 代码中没有使用Map函数因为和数据结构有含义冲突的问题所以使用`Transform`,这个来源于 C++ STL库中的命名。
1. 有两个版本的函数,一个是返回一个全新的数组 `Transform()`,一个是“就地完成” `TransformInPlace()`
1. 在主函数中,用 `Kind()` 方法检查了数据类型是不是 Slice函数类型是不是Func。
1. 检查函数的参数和返回类型是通过 `verifyFuncSignature()` 来完成的:`NumIn()`用来检查函数的“入参”;`NumOut()` :用来检查函数的“返回值”。
1. 如果需要新生成一个Slice会使用 `reflect.MakeSlice()` 来完成。
好了,有了这段代码,我们的代码就很可以很开心地使用了:
1.可以用于字符串数组:
```
list := []string{&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;, &quot;5&quot;, &quot;6&quot;}
result := Transform(list, func(a string) string{
return a +a +a
})
//{&quot;111&quot;,&quot;222&quot;,&quot;333&quot;,&quot;444&quot;,&quot;555&quot;,&quot;666&quot;}
```
2.可以用于整形数组:
```
list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {
return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}
```
3.可以用于结构体:
```
var list = []Employee{
{&quot;Hao&quot;, 44, 0, 8000},
{&quot;Bob&quot;, 34, 10, 5000},
{&quot;Alice&quot;, 23, 5, 9000},
{&quot;Jack&quot;, 26, 0, 4000},
{&quot;Tom&quot;, 48, 9, 7500},
}
result := TransformInPlace(list, func(e Employee) Employee {
e.Salary += 1000
e.Age += 1
return e
})
```
### 健壮版的 Generic Reduce
同样,泛型版的 Reduce 代码如下:
```
func Reduce(slice, pairFunc, zero interface{}) interface{} {
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic(&quot;reduce: wrong type, not slice&quot;)
}
len := sliceInType.Len()
if len == 0 {
return zero
} else if len == 1 {
return sliceInType.Index(0)
}
elemType := sliceInType.Type().Elem()
fn := reflect.ValueOf(pairFunc)
if !verifyFuncSignature(fn, elemType, elemType, elemType) {
t := elemType.String()
panic(&quot;reduce: function must be of type func(&quot; + t + &quot;, &quot; + t + &quot;) &quot; + t)
}
var ins [2]reflect.Value
ins[0] = sliceInType.Index(0)
ins[1] = sliceInType.Index(1)
out := fn.Call(ins[:])[0]
for i := 2; i &lt; len; i++ {
ins[0] = out
ins[1] = sliceInType.Index(i)
out = fn.Call(ins[:])[0]
}
return out.Interface()
}
```
### 健壮版的 Generic Filter
同样,泛型版的 Filter 代码如下(同样分是否“就地计算”的两个版本):
```
func Filter(slice, function interface{}) interface{} {
result, _ := filter(slice, function, false)
return result
}
func FilterInPlace(slicePtr, function interface{}) {
in := reflect.ValueOf(slicePtr)
if in.Kind() != reflect.Ptr {
panic(&quot;FilterInPlace: wrong type, &quot; +
&quot;not a pointer to slice&quot;)
}
_, n := filter(in.Elem().Interface(), function, true)
in.Elem().SetLen(n)
}
var boolType = reflect.ValueOf(true).Type()
func filter(slice, function interface{}, inPlace bool) (interface{}, int) {
sliceInType := reflect.ValueOf(slice)
if sliceInType.Kind() != reflect.Slice {
panic(&quot;filter: wrong type, not a slice&quot;)
}
fn := reflect.ValueOf(function)
elemType := sliceInType.Type().Elem()
if !verifyFuncSignature(fn, elemType, boolType) {
panic(&quot;filter: function must be of type func(&quot; + elemType.String() + &quot;) bool&quot;)
}
var which []int
for i := 0; i &lt; sliceInType.Len(); i++ {
if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {
which = append(which, i)
}
}
out := sliceInType
if !inPlace {
out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))
}
for i := range which {
out.Index(i).Set(sliceInType.Index(which[i]))
}
return out.Interface(), len(which)
}
```
## 后记
最后,还有几个未尽事宜:
1. 使用反射来做这些东西会有一个问题,**那就是代码的性能会很差。所以,上面的代码不能用在需要高性能的地方**。怎么解决这个问题,我会在下节课给你介绍下。
1. 这节课中的代码大量地参考了 Rob Pike的版本你可以点击这个链接查看 [https://github.com/robpike/filter](https://github.com/robpike/filter)。
1. 其实在全世界范围内有大量的程序员都在问Go语言官方什么时候在标准库中支持 Map、Reduce。Rob Pike说这种东西难写吗还要我们官方来帮你们写吗这种代码我多少年前就写过了但是我一次都没有用过我还是喜欢用“For循环”我觉得你最好也跟我一起用 “For循环”。
我个人觉得Map、Reduce在数据处理的时候还是很有用的Rob Pike可能平时也不怎么写“业务逻辑”的代码所以他可能也不太了解业务的变化有多么频繁……
当然,好还是不好,由你来判断,但多学一些编程模式,一定是对自己很有帮助的。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,399 @@
<audio id="audio" title="112 | Go 编程模式Go Generation" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a2/5a/a2b30aace72fd7499aa319a63ec4825a.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
这节课我们来学习一下Go语言的代码生成的玩法。
Go语言的代码生成主要还是用来解决编程泛型的问题。泛型编程主要是解决这样一个问题因为静态类型语言有类型所以相关的算法或是对数据处理的程序会因为类型不同而需要复制一份这样会导致数据类型和算法功能耦合。
我之所以说泛型编程可以解决这样的问题,就是说,在写代码的时候,不用关心处理数据的类型,只需要关心相关的处理逻辑。
泛型编程是静态语言中非常非常重要的特征,如果没有泛型,我们就很难做到多态,也很难完成抽象,这就会导致我们的代码冗余量很大。
## 现实中的类比
为了帮你更好地理解,我举个现实当中的例子。我们用螺丝刀来做打比方,螺丝刀本来只有一个拧螺丝的作用,但是因为螺丝的类型太多,有平口的,有十字口的,有六角的……螺丝还有不同的尺寸,这就导致我们的螺丝刀为了要适配各种千奇百怪的螺丝类型(样式和尺寸),也是各种样式的。
而真正的抽象是,螺丝刀不应该关心螺丝的类型,它只要关注自己的功能是不是完备,并且让自己可以适配不同类型的螺丝就行了,这就是所谓的泛型编程要解决的实际问题。
## Go语方的类型检查
因为Go语言目前并不支持真正的泛型所以只能用 `interface{}` 这样的类似于 `void*` 的过度泛型来玩,这就导致我们要在实际过程中进行类型检查。
Go语言的类型检查有两种技术一种是 Type Assert一种是Reflection。
### Type Assert
这种技术,一般是对某个变量进行 `.(type)`的转型操作它会返回两个值分别是variable和error。 variable是被转换好的类型error表示如果不能转换类型则会报错。
在下面的示例中,我们有一个通用类型的容器,可以进行 `Put(val)``Get()`,注意,这里使用了 `interface{}`做泛型。
```
//Container is a generic container, accepting anything.
type Container []interface{}
//Put adds an element to the container.
func (c *Container) Put(elem interface{}) {
*c = append(*c, elem)
}
//Get gets an element from the container.
func (c *Container) Get() interface{} {
elem := (*c)[0]
*c = (*c)[1:]
return elem
}
```
我们可以这样使用:
```
intContainer := &amp;Container{}
intContainer.Put(7)
intContainer.Put(42)
```
但是,在把数据取出来时,因为类型是 `interface{}` ,所以,你还要做一个转型,只有转型成功,才能进行后续操作(因为 `interface{}`太泛了,泛到什么类型都可以放)。
下面是一个Type Assert的示例
```
// assert that the actual type is int
elem, ok := intContainer.Get().(int)
if !ok {
fmt.Println(&quot;Unable to read an int from intContainer&quot;)
}
fmt.Printf(&quot;assertExample: %d (%T)\n&quot;, elem, elem)
```
### Reflection
对于Reflection我们需要把上面的代码修改如下
```
type Container struct {
s reflect.Value
}
func NewContainer(t reflect.Type, size int) *Container {
if size &lt;=0 { size=64 }
return &amp;Container{
s: reflect.MakeSlice(reflect.SliceOf(t), 0, size),
}
}
func (c *Container) Put(val interface{}) error {
if reflect.ValueOf(val).Type() != c.s.Type().Elem() {
return fmt.Errorf(“Put: cannot put a %T into a slice of %s&quot;,
val, c.s.Type().Elem()))
}
c.s = reflect.Append(c.s, reflect.ValueOf(val))
return nil
}
func (c *Container) Get(refval interface{}) error {
if reflect.ValueOf(refval).Kind() != reflect.Ptr ||
reflect.ValueOf(refval).Elem().Type() != c.s.Type().Elem() {
return fmt.Errorf(&quot;Get: needs *%s but got %T&quot;, c.s.Type().Elem(), refval)
}
reflect.ValueOf(refval).Elem().Set( c.s.Index(0) )
c.s = c.s.Slice(1, c.s.Len())
return nil
}
```
这里的代码并不难懂,这是完全使用 Reflection的玩法我简单解释下。
-`NewContainer()`会根据参数的类型初始化一个Slice。
-`Put()`时,会检查 `val` 是否和Slice的类型一致。
-`Get()`时,我们需要用一个入参的方式,因为我们没有办法返回 `reflect.Value``interface{}`不然还要做Type Assert。
- 不过有类型检查,所以,必然会有检查不对的时候,因此,需要返回 `error`
于是,在使用这段代码的时候,会是下面这个样子:
```
f1 := 3.1415926
f2 := 1.41421356237
c := NewMyContainer(reflect.TypeOf(f1), 16)
if err := c.Put(f1); err != nil {
panic(err)
}
if err := c.Put(f2); err != nil {
panic(err)
}
g := 0.0
if err := c.Get(&amp;g); err != nil {
panic(err)
}
fmt.Printf(&quot;%v (%T)\n&quot;, g, g) //3.1415926 (float64)
fmt.Println(c.s.Index(0)) //1.4142135623
```
可以看到Type Assert是不用了但是用反射写出来的代码还是有点复杂的。那么有没有什么好的方法
## 他山之石
对于泛型编程最牛的语言 C++ 来说,这类问题都是使用 Template解决的。
```
//用&lt;class T&gt;来描述泛型
template &lt;class T&gt;
T GetMax (T a, T b) {
T result;
result = (a&gt;b)? a : b;
return (result);
}
```
```
int i=5, j=6, k;
//生成int类型的函数
k=GetMax&lt;int&gt;(i,j);
long l=10, m=5, n;
//生成long类型的函数
n=GetMax&lt;long&gt;(l,m);
```
C++的编译器会在编译时分析代码根据不同的变量类型来自动化生成相关类型的函数或类在C++里,叫模板的具体化。
这个技术是编译时的问题,所以,我们不需要在运行时进行任何的类型识别,我们的程序也会变得比较干净。
那么我们是否可以在Go中使用C++的这种技术呢答案是肯定的只是Go的编译器不会帮你干你需要自己动手。
## Go Generator
要玩 Go的代码生成你需要三个东西
1. 一个函数模板,在里面设置好相应的占位符;
1. 一个脚本,用于按规则来替换文本并生成新的代码;
1. 一行注释代码。
### 函数模板
我们把之前的示例改成模板,取名为 `container.tmp.go` 放在 `./template/`下:
```
package PACKAGE_NAME
type GENERIC_NAMEContainer struct {
s []GENERIC_TYPE
}
func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer {
return &amp;GENERIC_NAMEContainer{s: []GENERIC_TYPE{}}
}
func (c *GENERIC_NAMEContainer) Put(val GENERIC_TYPE) {
c.s = append(c.s, val)
}
func (c *GENERIC_NAMEContainer) Get() GENERIC_TYPE {
r := c.s[0]
c.s = c.s[1:]
return r
}
```
可以看到,函数模板中我们有如下的占位符:
- `PACKAGE_NAME`:包名
- `GENERIC_NAME` :名字
- `GENERIC_TYPE` :实际的类型
其它的代码都是一样的。
### 函数生成脚本
然后,我们有一个叫`gen.sh`的生成脚本,如下所示:
```
#!/bin/bash
set -e
SRC_FILE=${1}
PACKAGE=${2}
TYPE=${3}
DES=${4}
#uppcase the first char
PREFIX=&quot;$(tr '[:lower:]' '[:upper:]' &lt;&lt;&lt; ${TYPE:0:1})${TYPE:1}&quot;
DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go
sed 's/PACKAGE_NAME/'&quot;${PACKAGE}&quot;'/g' ${SRC_FILE} | \
sed 's/GENERIC_TYPE/'&quot;${TYPE}&quot;'/g' | \
sed 's/GENERIC_NAME/'&quot;${PREFIX}&quot;'/g' &gt; ${DES_FILE}
```
这里需要4个参数
- 模板源文件;
- 包名;
- 实际需要具体化的类型;
- 用于构造目标文件名的后缀。
然后,我们用 `sed` 命令去替换刚刚的函数模板并生成到目标文件中关于sed命令我给你推荐一篇文章《[sed 简明教程](https://coolshell.cn/articles/9104.html)》)。
### 生成代码
接下来,我们只需要在代码中打一个特殊的注释:
```
//go:generate ./gen.sh ./template/container.tmp.go gen uint32 container
func generateUint32Example() {
var u uint32 = 42
c := NewUint32Container()
c.Put(u)
v := c.Get()
fmt.Printf(&quot;generateExample: %d (%T)\n&quot;, v, v)
}
//go:generate ./gen.sh ./template/container.tmp.go gen string container
func generateStringExample() {
var s string = &quot;Hello&quot;
c := NewStringContainer()
c.Put(s)
v := c.Get()
fmt.Printf(&quot;generateExample: %s (%T)\n&quot;, v, v)
}
```
其中,
- 第一个注释是生成包名gen类型是uint32目标文件名以container为后缀。
- 第二个注释是生成包名gen类型是string目标文件名是以container为后缀。
然后,在工程目录中直接执行 `go generate` 命令,就会生成两份代码:
一份文件名为uint32_container.go
```
package gen
type Uint32Container struct {
s []uint32
}
func NewUint32Container() *Uint32Container {
return &amp;Uint32Container{s: []uint32{}}
}
func (c *Uint32Container) Put(val uint32) {
c.s = append(c.s, val)
}
func (c *Uint32Container) Get() uint32 {
r := c.s[0]
c.s = c.s[1:]
return r
}
```
另一份的文件名为 string_container.go
```
package gen
type StringContainer struct {
s []string
}
func NewStringContainer() *StringContainer {
return &amp;StringContainer{s: []string{}}
}
func (c *StringContainer) Put(val string) {
c.s = append(c.s, val)
}
func (c *StringContainer) Get() string {
r := c.s[0]
c.s = c.s[1:]
return r
}
```
这两份代码可以让我们的代码完全编译通过,付出的代价就是需要多执行一步 `go generate` 命令。
## 新版Filter
现在我们再回头看看上节课里的那些用反射整出来的例子,你就会发现,有了这样的技术,我们就不用在代码里,用那些晦涩难懂的反射来做运行时的类型检查了。我们可以写出很干净的代码,让编译器在编译时检查类型对不对。
下面是一个Fitler的模板文件 `filter.tmp.go`
```
package PACKAGE_NAME
type GENERIC_NAMEList []GENERIC_TYPE
type GENERIC_NAMEToBool func(*GENERIC_TYPE) bool
func (al GENERIC_NAMEList) Filter(f GENERIC_NAMEToBool) GENERIC_NAMEList {
var ret GENERIC_NAMEList
for _, a := range al {
if f(&amp;a) {
ret = append(ret, a)
}
}
return ret
}
```
这样,我们可以在需要使用这个的地方,加上相关的 Go Generate 的注释:
```
type Employee struct {
Name string
Age int
Vacation int
Salary int
}
//go:generate ./gen.sh ./template/filter.tmp.go gen Employee filter
func filterEmployeeExample() {
var list = EmployeeList{
{&quot;Hao&quot;, 44, 0, 8000},
{&quot;Bob&quot;, 34, 10, 5000},
{&quot;Alice&quot;, 23, 5, 9000},
{&quot;Jack&quot;, 26, 0, 4000},
{&quot;Tom&quot;, 48, 9, 7500},
}
var filter EmployeeList
filter = list.Filter(func(e *Employee) bool {
return e.Age &gt; 40
})
fmt.Println(&quot;----- Employee.Age &gt; 40 ------&quot;)
for _, e := range filter {
fmt.Println(e)
}
filter = list.Filter(func(e *Employee) bool {
return e.Salary &lt;= 5000
})
fmt.Println(&quot;----- Employee.Salary &lt;= 5000 ------&quot;)
for _, e := range filter {
fmt.Println(e)
}
}
```
## 第三方工具
我们并不需要自己手写 `gen.sh` 这样的工具类,我们可以直接使用第三方已经写好的工具。我给你提供一个列表。
- [Genny](https://github.com/cheekybits/genny)
- [Generic](https://github.com/taylorchu/generic)
- [GenGen](https://github.com/joeshaw/gengen)
- [Gen](https://github.com/clipperhouse/gen)
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,351 @@
<audio id="audio" title="113 | Go编程模式修饰器" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/c4/8d1a208b58df95699dafaee9c6f49bc4.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
之前,我写过一篇文章《[Python修饰器的函数式编程](https://coolshell.cn/articles/11265.html)》,这种模式可以很轻松地把一些函数装配到另外一些函数上,让你的代码更加简单,也可以让一些“小功能型”的代码复用性更高,让代码中的函数可以像乐高玩具那样自由地拼装。
所以一直以来我都对修饰器Decoration这种编程模式情有独钟这节课我们就来聊聊Go语言的修饰器编程模式。
如果你看过我刚说的文章,就一定知道,这是一种函数式编程的玩法——用一个高阶函数来包装一下。
多唠叨一句,关于函数式编程,我之前还写过一篇文章《[函数式编程](https://coolshell.cn/articles/10822.html)》这篇文章主要是想通过详细介绍从过程式编程的思维方式过渡到函数式编程的思维方式带动更多的人玩函数式编程。所以如果你想了解一下函数式编程那么可以点击链接阅读一下这篇文章。其实Go语言的修饰器编程模式也就是函数式编程的模式。
不过要提醒你注意的是Go 语言的“糖”不多,而且又是强类型的静态无虚拟机的语言,所以,没有办法做到像 Java 和 Python 那样写出优雅的修饰器的代码。当然,也许是我才疏学浅,如果你知道更多的写法,请你一定告诉我。先谢过了。
## 简单示例
我们先来看一个示例:
```
package main
import &quot;fmt&quot;
func decorator(f func(s string)) func(s string) {
return func(s string) {
fmt.Println(&quot;Started&quot;)
f(s)
fmt.Println(&quot;Done&quot;)
}
}
func Hello(s string) {
fmt.Println(s)
}
func main() {
decorator(Hello)(&quot;Hello, World!&quot;)
}
```
可以看到,我们动用了一个高阶函数 `decorator()`,在调用的时候,先把 `Hello()` 函数传进去,然后会返回一个匿名函数。这个匿名函数中除了运行了自己的代码,也调用了被传入的 `Hello()` 函数。
这个玩法和 Python 的异曲同工只不过有些遗憾的是Go 并不支持像 Python 那样的 `@decorator` 语法糖。所以,在调用上有些难看。当然,如果你想让代码更容易读,你可以这样写:
```
hello := decorator(Hello)
hello(&quot;Hello&quot;)
```
我们再来看一个计算运行时间的例子:
```
package main
import (
&quot;fmt&quot;
&quot;reflect&quot;
&quot;runtime&quot;
&quot;time&quot;
)
type SumFunc func(int64, int64) int64
func getFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func timedSumFunc(f SumFunc) SumFunc {
return func(start, end int64) int64 {
defer func(t time.Time) {
fmt.Printf(&quot;--- Time Elapsed (%s): %v ---\n&quot;,
getFunctionName(f), time.Since(t))
}(time.Now())
return f(start, end)
}
}
func Sum1(start, end int64) int64 {
var sum int64
sum = 0
if start &gt; end {
start, end = end, start
}
for i := start; i &lt;= end; i++ {
sum += i
}
return sum
}
func Sum2(start, end int64) int64 {
if start &gt; end {
start, end = end, start
}
return (end - start + 1) * (end + start) / 2
}
func main() {
sum1 := timedSumFunc(Sum1)
sum2 := timedSumFunc(Sum2)
fmt.Printf(&quot;%d, %d\n&quot;, sum1(-10000, 10000000), sum2(-10000, 10000000))
}
```
关于这段代码,有几点我要说明一下:
1. 有两个 Sum 函数,`Sum1()` 函数就是简单地做个循环,`Sum2()` 函数动用了数据公式注意start 和 end 有可能有负数);
1. 代码中使用了 Go 语言的反射机制来获取函数名;
1. 修饰器函数是 `timedSumFunc()`
运行后输出:
```
$ go run time.sum.go
--- Time Elapsed (main.Sum1): 3.557469ms ---
--- Time Elapsed (main.Sum2): 291ns ---
49999954995000, 49999954995000
```
## HTTP 相关的一个示例
接下来,我们再看一个处理 HTTP 请求的相关例子。
先看一个简单的 HTTP Server 的代码:
```
package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;strings&quot;
)
func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithServerHeader()&quot;)
w.Header().Set(&quot;Server&quot;, &quot;HelloServer v0.0.1&quot;)
h(w, r)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
log.Printf(&quot;Recieved Request %s from %s\n&quot;, r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, &quot;Hello, World! &quot;+r.URL.Path)
}
func main() {
http.HandleFunc(&quot;/v1/hello&quot;, WithServerHeader(hello))
err := http.ListenAndServe(&quot;:8080&quot;, nil)
if err != nil {
log.Fatal(&quot;ListenAndServe: &quot;, err)
}
}
```
这段代码中使用到了修饰器模式,`WithServerHeader()` 函数就是一个 Decorator它会传入一个 `http.HandlerFunc`,然后返回一个改写的版本。这个例子还是比较简单的,用 `WithServerHeader()` 就可以加入一个 Response 的 Header。
所以,这样的函数我们可以写出好多。如下所示,有写 HTTP 响应头的,有写认证 Cookie 的有检查认证Cookie的有打日志的……
```
package main
import (
&quot;fmt&quot;
&quot;log&quot;
&quot;net/http&quot;
&quot;strings&quot;
)
func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithServerHeader()&quot;)
w.Header().Set(&quot;Server&quot;, &quot;HelloServer v0.0.1&quot;)
h(w, r)
}
}
func WithAuthCookie(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithAuthCookie()&quot;)
cookie := &amp;http.Cookie{Name: &quot;Auth&quot;, Value: &quot;Pass&quot;, Path: &quot;/&quot;}
http.SetCookie(w, cookie)
h(w, r)
}
}
func WithBasicAuth(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Println(&quot;---&gt;WithBasicAuth()&quot;)
cookie, err := r.Cookie(&quot;Auth&quot;)
if err != nil || cookie.Value != &quot;Pass&quot; {
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(&quot;---&gt;WithDebugLog&quot;)
r.ParseForm()
log.Println(r.Form)
log.Println(&quot;path&quot;, r.URL.Path)
log.Println(&quot;scheme&quot;, r.URL.Scheme)
log.Println(r.Form[&quot;url_long&quot;])
for k, v := range r.Form {
log.Println(&quot;key:&quot;, k)
log.Println(&quot;val:&quot;, strings.Join(v, &quot;&quot;))
}
h(w, r)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
log.Printf(&quot;Recieved Request %s from %s\n&quot;, r.URL.Path, r.RemoteAddr)
fmt.Fprintf(w, &quot;Hello, World! &quot;+r.URL.Path)
}
func main() {
http.HandleFunc(&quot;/v1/hello&quot;, WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc(&quot;/v2/hello&quot;, WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc(&quot;/v3/hello&quot;, WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
err := http.ListenAndServe(&quot;:8080&quot;, nil)
if err != nil {
log.Fatal(&quot;ListenAndServe: &quot;, err)
}
}
```
## 多个修饰器的 Pipeline
在使用上,需要对函数一层层地套起来,看上去好像不是很好看,如果需要修饰器比较多的话,代码就会比较难看了。不过,我们可以重构一下。
重构时,我们需要先写一个工具函数,用来遍历并调用各个修饰器:
```
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
}
```
然后,我们就可以像下面这样使用了:
```
http.HandleFunc(&quot;/v4/hello&quot;, Handler(hello,
WithServerHeader, WithBasicAuth, WithDebugLog))
```
这样的代码是不是更易读了一些Pipeline 的功能也就出来了。
## 泛型的修饰器
不过,对于 Go 的修饰器模式,还有一个小问题,那就是好像无法做到泛型。比如上面那个计算时间的函数,其代码耦合了需要被修饰的函数的接口类型,无法做到非常通用。如果这个问题解决不了,那么,这个修饰器模式还是有点不好用的。
因为 Go 语言不像 Python 和 JavaPython是动态语言而 Java 有语言虚拟机所以它们可以实现一些比较“变态”的事。但是Go 语言是一个静态的语言这就意味着类型需要在编译时就搞定否则无法编译。不过Go 语言支持的最大的泛型是 `interface{}` ,还有比较简单的 Reflection 机制,在上面做做文章,应该还是可以搞定的。
废话不说,下面是我用 Reflection 机制写的一个比较通用的修饰器(为了便于阅读,我删除了出错判断代码):
```
func Decorator(decoPtr, fn interface{}) (err error) {
var decoratedFunc, targetFunc reflect.Value
decoratedFunc = reflect.ValueOf(decoPtr).Elem()
targetFunc = reflect.ValueOf(fn)
v := reflect.MakeFunc(targetFunc.Type(),
func(in []reflect.Value) (out []reflect.Value) {
fmt.Println(&quot;before&quot;)
out = targetFunc.Call(in)
fmt.Println(&quot;after&quot;)
return
})
decoratedFunc.Set(v)
return
}
```
这段代码动用了 `reflect.MakeFunc()` 函数,创造了一个新的函数,其中的 `targetFunc.Call(in)` 调用了被修饰的函数。关于 Go 语言的反射机制,你可以阅读下官方文章[**The Laws of Reflection**](https://blog.golang.org/laws-of-reflection),我就不多说了。
这个 `Decorator()` 需要两个参数:
- 第一个是出参 `decoPtr` ,就是完成修饰后的函数;
- 第二个是入参 `fn` ,就是需要修饰的函数。
这样写是不是有些“傻”?的确是的。不过,这是我个人在 Go 语言里所能写出来的最好的代码了。如果你知道更多优雅的写法,请你要一定告诉我!
好了,让我们来看一下使用效果。首先,假设我们有两个需要修饰的函数:
```
func foo(a, b, c int) int {
fmt.Printf(&quot;%d, %d, %d \n&quot;, a, b, c)
return a + b + c
}
func bar(a, b string) string {
fmt.Printf(&quot;%s, %s \n&quot;, a, b)
return a + b
}
```
然后,我们可以这样做:
```
type MyFoo func(int, int, int) int
var myfoo MyFoo
Decorator(&amp;myfoo, foo)
myfoo(1, 2, 3)
```
你会发现,使用 `Decorator()` 时,还需要先声明一个函数签名,感觉好傻啊,一点都不泛型,不是吗?
如果你不想声明函数签名,就可以这样:
```
mybar := bar
Decorator(&amp;mybar, bar)
mybar(&quot;hello,&quot;, &quot;world!&quot;)
```
好吧,看上去不是那么漂亮,但是 it works。
看样子 Go 语言目前本身的特性无法做成像 Java 或 Python 那样对此我们只能期待Go 语言多放“糖”了!
Again 如果你有更好的写法,请你一定要告诉我。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,267 @@
<audio id="audio" title="114 | Go编程模式Pipeline" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/ac/0cd21eaac338025c39cc4a0e01068aac.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
这节课我着重介绍一下Go编程中的Pipeline模式。对于Pipeline用过Unix/Linux命令行的人都不会陌生**它是一种把各种命令拼接起来完成一个更强功能的技术方法**。
现在的流式处理、函数式编程、应用网关对微服务进行简单的API编排其实都是受Pipeline这种技术方式的影响。Pipeline可以很容易地把代码按单一职责的原则拆分成多个高内聚低耦合的小模块然后轻松地把它们拼装起来去完成比较复杂的功能。
## HTTP 处理
这种Pipeline的模式我在[上节课](https://time.geekbang.org/column/article/332608)中有过一个示例,我们再复习一下。
上节课,我们有很多 `WithServerHead()``WithBasicAuth()``WithDebugLog()`这样的小功能代码在需要实现某个HTTP API 的时候,我们就可以很轻松地把它们组织起来。
原来的代码是下面这个样子:
```
http.HandleFunc(&quot;/v1/hello&quot;, WithServerHeader(WithAuthCookie(hello)))
http.HandleFunc(&quot;/v2/hello&quot;, WithServerHeader(WithBasicAuth(hello)))
http.HandleFunc(&quot;/v3/hello&quot;, WithServerHeader(WithBasicAuth(WithDebugLog(hello))))
```
通过一个代理函数:
```
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
}
```
我们就可以移除不断的嵌套,像下面这样使用了:
```
http.HandleFunc(&quot;/v4/hello&quot;, Handler(hello,
WithServerHeader, WithBasicAuth, WithDebugLog))
```
## Channel 管理
当然,如果你要写出一个[泛型的Pipeline框架](https://coolshell.cn/articles/17929.html#%E6%B3%9B%E5%9E%8B%E7%9A%84%E4%BF%AE%E9%A5%B0%E5%99%A8)并不容易,可以使用[Go Generation](https://coolshell.cn/articles/21179.html)实现但是我们别忘了Go语言最具特色的 Go Routine 和 Channel 这两个神器完全可以用来构造这种编程。
Rob Pike在 [Go Concurrency Patterns: Pipelines and cancellation](https://blog.golang.org/pipelines) 这篇博客中介绍了一种编程模式,下面我们来学习下。
### Channel转发函数
首先,我们需要一个 `echo()`函数它会把一个整型数组放到一个Channel中并返回这个Channel。
```
func echo(nums []int) &lt;-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out &lt;- n
}
close(out)
}()
return out
}
```
然后,我们依照这个模式,就可以写下下面的函数。
### 平方函数
```
func sq(in &lt;-chan int) &lt;-chan int {
out := make(chan int)
go func() {
for n := range in {
out &lt;- n * n
}
close(out)
}()
return out
}
```
### 过滤奇数函数
```
func odd(in &lt;-chan int) &lt;-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 != 0 {
out &lt;- n
}
}
close(out)
}()
return out
}
```
### 求和函数
```
func sum(in &lt;-chan int) &lt;-chan int {
out := make(chan int)
go func() {
var sum = 0
for n := range in {
sum += n
}
out &lt;- sum
close(out)
}()
return out
}
```
用户端的代码如下所示你可能会觉得sum()odd() 和 sq()太过于相似其实你可以通过Map/Reduce编程模式或者是Go Generation的方式合并一下
```
var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range sum(sq(odd(echo(nums)))) {
fmt.Println(n)
}
```
上面的代码类似于我们执行了Unix/Linux命令 `echo $nums | sq | sum`。同样,如果你不想有那么多的函数嵌套,就可以使用一个代理函数来完成。
```
type EchoFunc func ([]int) (&lt;- chan int)
type PipeFunc func (&lt;- chan int) (&lt;- chan int)
func pipeline(nums []int, echo EchoFunc, pipeFns ... PipeFunc) &lt;- chan int {
ch := echo(nums)
for i := range pipeFns {
ch = pipeFns[i](ch)
}
return ch
}
```
然后,就可以这样做了:
```
var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range pipeline(nums, gen, odd, sq, sum) {
fmt.Println(n)
}
```
## Fan in/Out
**动用Go语言的 Go Routine和 Channel还有一个好处就是可以写出1对多或多对1的Pipeline也就是Fan In/ Fan Out**。下面我们来看一个Fan in的示例。
假设我们要通过并发的方式对一个很长的数组中的质数进行求和运算,我们想先把数组分段求和,然后再把它们集中起来。
下面是我们的主函数:
```
func makeRange(min, max int) []int {
a := make([]int, max-min+1)
for i := range a {
a[i] = min + i
}
return a
}
func main() {
nums := makeRange(1, 10000)
in := echo(nums)
const nProcess = 5
var chans [nProcess]&lt;-chan int
for i := range chans {
chans[i] = sum(prime(in))
}
for n := range sum(merge(chans[:])) {
fmt.Println(n)
}
}
```
再看我们的 `prime()` 函数的实现
```
func is_prime(value int) bool {
for i := 2; i &lt;= int(math.Floor(float64(value) / 2)); i++ {
if value%i == 0 {
return false
}
}
return value &gt; 1
}
func prime(in &lt;-chan int) &lt;-chan int {
out := make(chan int)
go func () {
for n := range in {
if is_prime(n) {
out &lt;- n
}
}
close(out)
}()
return out
}
```
我来简单解释下这段代码。
- 首先我们制造了从1到10000的数组
- 然后,把这堆数组全部 `echo`到一个Channel里—— `in`
- 此时,生成 5 个 Channel接着都调用 `sum(prime(in))` 于是每个Sum的Go Routine都会开始计算和
- 最后,再把所有的结果再求和拼起来,得到最终的结果。
其中的merge代码如下
```
func merge(cs []&lt;-chan int) &lt;-chan int {
var wg sync.WaitGroup
out := make(chan int)
wg.Add(len(cs))
for _, c := range cs {
go func(c &lt;-chan int) {
for n := range c {
out &lt;- n
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
```
整个程序的结构如下图所示:
<img src="https://static001.geekbang.org/resource/image/f9/b3/f9d2b599620d5bc191194ff239f0a1b3.jpg" alt="">
## 参考文档
如果你还想了解更多类似的与并发相关的技术,我再给你推荐一些资源:
- [Go Concurrency Patterns Rob Pike 2012 Google I/O presents the basics of Gos concurrency primitives and several ways to apply them.](https://www.youtube.com/watch?v=f6kdp27TYZs)
<li>[Advanced Go Concurrency Patterns Rob Pike 2013 Google I/O](https://blog.golang.org/advanced-go-concurrency-patterns)<br>
[covers more complex uses of Gos primitives, especially select.](https://blog.golang.org/advanced-go-concurrency-patterns)</li>
<li>[Squinting at Power Series Douglas McIlroy's paper](https://swtch.com/~rsc/thread/squint.pdf)<br>
[shows how Go-like concurrency provides elegant support for complex calculations.](https://swtch.com/~rsc/thread/squint.pdf)</li>
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

View File

@@ -0,0 +1,312 @@
<audio id="audio" title="115 | Go 编程模式Kubernetes Visitor模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/22/c8/22d985b9137c416367abbaa2b5e6d8c8.mp3"></audio>
你好,我是陈皓,网名左耳朵耗子。
这节课我们来重点讨论一下Kubernetes 的 `kubectl` 命令中的使用到的一个编程模式Visitor其实`kubectl` 主要使用到了两个一个是Builder另一个是Visitor
本来Visitor 是面向对象设计模式中一个很重要的设计模式可以看下Wikipedia [Visitor Pattern词条](https://en.wikipedia.org/wiki/Visitor_pattern)),这个模式是将算法与操作对象的结构分离的一种方法。这种分离的实际结果是能够在不修改结构的情况下向现有对象结构添加新操作,是遵循开放/封闭原则的一种方法。这节课,我们重点学习一下 `kubelet` 中是怎么使用函数式的方法来实现这个模式的。
## 一个简单示例
首先我们来看一个简单设计模式的Visitor的示例。
- 我们的代码中有一个`Visitor`的函数定义,还有一个`Shape`接口,这需要使用 `Visitor`函数作为参数。
- 我们的实例的对象 `Circle``Rectangle`实现了 `Shape` 接口的 `accept()` 方法这个方法就是等外面给我们传递一个Visitor。
```
package main
import (
&quot;encoding/json&quot;
&quot;encoding/xml&quot;
&quot;fmt&quot;
)
type Visitor func(shape Shape)
type Shape interface {
accept(Visitor)
}
type Circle struct {
Radius int
}
func (c Circle) accept(v Visitor) {
v(c)
}
type Rectangle struct {
Width, Heigh int
}
func (r Rectangle) accept(v Visitor) {
v(r)
}
```
然后我们实现两个Visitor一个是用来做JSON序列化的另一个是用来做XML序列化的。
```
func JsonVisitor(shape Shape) {
bytes, err := json.Marshal(shape)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
func XmlVisitor(shape Shape) {
bytes, err := xml.Marshal(shape)
if err != nil {
panic(err)
}
fmt.Println(string(bytes))
}
```
下面是使用Visitor这个模式的代码
```
func main() {
c := Circle{10}
r := Rectangle{100, 200}
shapes := []Shape{c, r}
for _, s := range shapes {
s.accept(JsonVisitor)
s.accept(XmlVisitor)
}
}
```
其实,这段代码的目的就是想解耦数据结构和算法。虽然使用 Strategy 模式也是可以完成的,而且会比较干净,**但是在有些情况下多个Visitor是来访问一个数据结构的不同部分这种情况下数据结构有点像一个数据库而各个Visitor会成为一个个的小应用。** `kubectl`就是这种情况。
## Kubernetes相关背景
接下来,我们再来了解一下相关的知识背景。
- Kubernetes抽象了很多种的Resource比如Pod、ReplicaSet、ConfigMap、Volumes、Namespace、Roles……种类非常繁多这些东西构成了Kubernetes的数据模型你可以看看 [Kubernetes Resources 地图](https://github.com/kubernauts/practical-kubernetes-problems/blob/master/images/k8s-resources-map.png) ,了解下有多复杂)。
- `kubectl` 是Kubernetes中的一个客户端命令操作人员用这个命令来操作Kubernetes。`kubectl` 会联系到 Kubernetes 的API ServerAPI Server会联系每个节点上的 `kubelet` ,从而控制每个节点。
- `kubectl` 的主要工作是处理用户提交的东西包括命令行参数、YAML文件等接着会把用户提交的这些东西组织成一个数据结构体发送给 API Server。
- 相关的源代码在 `src/k8s.io/cli-runtime/pkg/resource/visitor.go` 中([源码链接](https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go))。
`kubectl` 的代码比较复杂,不过,简单来说,基本原理就是**它从命令行和YAML文件中获取信息通过Builder模式并把其转成一系列的资源最后用 Visitor 模式来迭代处理这些Reources**。
下面我们来看看 `kubectl` 的实现。为了简化,我不直接分析复杂的源码,而是用一个小的示例来表明 。
## kubectl的实现方法
### Visitor模式定义
首先,`kubectl` 主要是用来处理 `Info`结构体,下面是相关的定义:
```
type VisitorFunc func(*Info, error) error
type Visitor interface {
Visit(VisitorFunc) error
}
type Info struct {
Namespace string
Name string
OtherThings string
}
func (info *Info) Visit(fn VisitorFunc) error {
return fn(info, nil)
}
```
可以看到,
- 有一个 `VisitorFunc` 的函数类型的定义;
- 一个 `Visitor` 的接口,其中需要 `Visit(VisitorFunc) error` 的方法(这就像是我们上面那个例子的 `Shape`
- 最后,为`Info` 实现 `Visitor` 接口中的 `Visit()` 方法,实现就是直接调用传进来的方法(与前面的例子相仿)。
我们再来定义几种不同类型的 Visitor。
### Name Visitor
这个Visitor 主要是用来访问 `Info` 结构中的 `Name``NameSpace` 成员:
```
type NameVisitor struct {
visitor Visitor
}
func (v NameVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println(&quot;NameVisitor() before call function&quot;)
err = fn(info, err)
if err == nil {
fmt.Printf(&quot;==&gt; Name=%s, NameSpace=%s\n&quot;, info.Name, info.Namespace)
}
fmt.Println(&quot;NameVisitor() after call function&quot;)
return err
})
}
```
可以看到,在这段代码中:
- 声明了一个 `NameVisitor` 的结构体,这个结构体里有一个 `Visitor` 接口成员,这里意味着多态;
- 在实现 `Visit()` 方法时,调用了自己结构体内的那个 `Visitor``Visitor()` 方法这其实是一种修饰器的模式用另一个Visitor修饰了自己关于修饰器模式可以复习下[第113讲](https://time.geekbang.org/column/article/332608))。
### Other Visitor
这个Visitor主要用来访问 `Info` 结构中的 `OtherThings` 成员:
```
type OtherThingsVisitor struct {
visitor Visitor
}
func (v OtherThingsVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println(&quot;OtherThingsVisitor() before call function&quot;)
err = fn(info, err)
if err == nil {
fmt.Printf(&quot;==&gt; OtherThings=%s\n&quot;, info.OtherThings)
}
fmt.Println(&quot;OtherThingsVisitor() after call function&quot;)
return err
})
}
```
实现逻辑同上,我就不再重新讲了。
### Log Visitor
```
type LogVisitor struct {
visitor Visitor
}
func (v LogVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
fmt.Println(&quot;LogVisitor() before call function&quot;)
err = fn(info, err)
fmt.Println(&quot;LogVisitor() after call function&quot;)
return err
})
}
```
### 使用方代码
现在,我们看看使用上面的代码:
```
func main() {
info := Info{}
var v Visitor = &amp;info
v = LogVisitor{v}
v = NameVisitor{v}
v = OtherThingsVisitor{v}
loadFile := func(info *Info, err error) error {
info.Name = &quot;Hao Chen&quot;
info.Namespace = &quot;MegaEase&quot;
info.OtherThings = &quot;We are running as remote team.&quot;
return nil
}
v.Visit(loadFile)
}
```
可以看到,
- Visitor们一层套一层
- 我用 `loadFile` 假装从文件中读取数据;
- 最后执行 `v.Visit(loadfile)` ,这样,我们上面的代码就全部开始激活工作了。
这段代码输出如下的信息,你可以看到代码是怎么执行起来的:
```
LogVisitor() before call function
NameVisitor() before call function
OtherThingsVisitor() before call function
==&gt; OtherThings=We are running as remote team.
OtherThingsVisitor() after call function
==&gt; Name=Hao Chen, NameSpace=MegaEase
NameVisitor() after call function
LogVisitor() after call function
```
上面的代码有以下几种功效:
- 解耦了数据和程序;
- 使用了修饰器模式;
- 还做出了Pipeline的模式。
所以,其实我们可以重构一下上面的代码。
### Visitor修饰器
我们用[修饰器模式](https://coolshell.cn/articles/17929.html)来重构一下上面的代码。
```
type DecoratedVisitor struct {
visitor Visitor
decorators []VisitorFunc
}
func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
if len(fn) == 0 {
return v
}
return DecoratedVisitor{v, fn}
}
// Visit implements Visitor
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
return v.visitor.Visit(func(info *Info, err error) error {
if err != nil {
return err
}
if err := fn(info, nil); err != nil {
return err
}
for i := range v.decorators {
if err := v.decorators[i](info, nil); err != nil {
return err
}
}
return nil
})
}
```
这段代码并不复杂,我来解释下。
- 用一个 `DecoratedVisitor` 的结构来存放所有的`VistorFunc`函数;
- `NewDecoratedVisitor` 可以把所有的 `VisitorFunc`转给它,构造 `DecoratedVisitor` 对象;
- `DecoratedVisitor`实现了 `Visit()` 方法里面就是来做一个for-loop顺着调用所有的 `VisitorFunc`
这样,我们的代码就可以这样运作了:
```
info := Info{}
var v Visitor = &amp;info
v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor)
v.Visit(LoadFile)
```
是不是比之前的那个简单?需要注意的是,这个`DecoratedVisitor` 同样可以成为一个Visitor来使用。上面的这些代码全部存在于 `kubectl` 的代码中,只要你看懂了这里面的代码逻辑,就一定能看懂 `kubectl` 的代码。
好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。