2021-08-29 02:14:09

Go语言编程模式实战-左耳朵耗子

[TOC]

《Go语言编程模式实战》

Go编程模式:切片、接口、时间和性能

Slice

首先,我来介绍下 Slice,中文翻译叫“切片”,这个东西在 Go 语言中不是数组,而是一个结构体,其定义如下:

type slice struct { array unsafe.Pointer //指向存放数据的数组指针 len int //长度有多大 cap int //容量有多大 }

一个空的 Slice 的表现如下图所示:

img

熟悉 C/C++ 的同学一定会知道在结构体里用数组指针的问题——数据会发生共享!下面我们来看看 Slice 的一些操作:

foo = make([]int, 5) foo[3] = 42 foo[4] = 100 bar := foo[1:4] bar[1] = 99 我来解释下这段代码: * 首先,创建一个 foo 的 Slice,其中的长度和容量都是 5; * 然后,开始对 foo 所指向的数组中的索引为 34 的元素进行赋值; * 最后,对 foo 做切片后赋值给 bar,再修改 bar[1]。 为了方便你理解,我画了一张图: ![img](../media/fb0574yye57002dfc435efe9db3c88c6.png) 从这张图片中,我们可以看到,因为 foo 和 bar 的内存是共享的,所以,foo 和 bar 对数组内容的修改都会影响到对方。 接下来,我们再来看一个数据操作 append() 的示例: ​```go 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

从图中,我们可以看到,append()操作让 a 的容量变成了 64,而长度是 33。这里你需要重点注意一下,append()这个函数在 cap 不够用的时候,就会重新分配内存以扩大容量,如果够用,就不会重新分配内存了!

我们再来看一个例子:

func main() { path := []byte("AAAA/BBBBBBBBB") sepIndex := bytes.IndexByte(path,'/') dir1 := path[:sepIndex] dir2 := path[sepIndex+1:] fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB dir1 = append(dir1,"suffix"...) fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB}

在这个例子中,dir1 和 dir2 共享内存,虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。下面是相关的图示(注意上图中 dir1 和 dir2 结构体中的 cap 和 len 的变化):

img

如果要解决这个问题,我们只需要修改一行代码。我们要把代码

dir1 := path[:sepIndex]

修改为:

dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作会导致重新分配内存。

深度比较

当我们复制一个对象时,这个对象可以是内建数据类型、数组、结构体、Map……在复制结构体的时候,如果我们需要比较两个结构体中的数据是否相同,就要使用深度比较,而不只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual() ,下面是几个示例:

import ( "fmt" "reflect" ) func main() { v1 := data{} v2 := data{} fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2)) //prints: v1 == v2: true m1 := map[string]string{"one": "a","two": "b"} m2 := map[string]string{"two": "b", "one": "a"} fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2)) //prints: m1 == m2: true s1 := []int{1, 2, 3} s2 := []int{1, 2, 3} fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2)) //prints: s1 == s2: true }

接口编程

下面,我们来看段代码,其中是两个方法,它们都是要输出一个结构体,其中一个使用一个函数,另一个使用一个“成员函数”。

func PrintPerson(p *Person) { fmt.Printf("Name=%s, Sexual=%s, Age=%d\n", p.Name, p.Sexual, p.Age) } func (p *Person) Print() { fmt.Printf("Name=%s, Sexual=%s, Age=%d\n", p.Name, p.Sexual, p.Age) } func main() { var p = Person{ Name: "Hao Chen", Sexual: "Male", Age: 44, } PrintPerson(&p) p.Print() }

你更喜欢哪种方式呢?在 Go 语言中,使用“成员函数”的方式叫“Receiver”,这种方式是一种封装,因为 PrintPerson()本来就是和 Person强耦合的,所以理应放在一起。更重要的是,这种方式可以进行接口编程,对于接口编程来说,也就是一种抽象,主要是用在“多态”,这个技术,我在《Go 语言简介(上):接口与多态》中讲过,你可以点击链接查看。

在这里,我想讲另一个 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 {"China"} c2 := City {"Beijing"} 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{ "China"}} c2 := City { WithName{"Beijing"}} 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 "Country = " + c.Name } func (c City) ToString() string{ return "City = " + c.Name } func PrintStr(p Stringable) { fmt.Println(p.ToString()) } d1 := Country {"USA"} d2 := City{"Los Angeles"} 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("%d\n",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)

这样就做到了强验证的方法。

时间

对于时间来说,这应该是编程中比较复杂的问题了,相信我,时间是一种非常复杂的事(比如《你确信你了解时间吗?》 《关于闰秒》等文章)。而且,时间有时区、格式、精度等问题,其复杂度不是一般人能处理的。所以,一定要重用已有的时间处理,而不是自己干。在 Go 语言中,你一定要使用 time.Time 和 time.Duration 这两个类型。

  • 在命令行上,flag 通过 time.ParseDuration 支持了 time.Duration。

  • JSON 中的 encoding/json 中也可以把time.Time 编码成 RFC 3339 的格式。

  • 数据库使用的 database/sql 也支持把 DATATIME 或 TIMESTAMP 类型转成 time.Time。

  • YAML 也可以使用 gopkg.in/yaml.v2 支持 time.Time 、time.Duration 和 RFC 3339 格式。

如果你要和第三方交互,实在没有办法,也请使用 RFC 3339 的格式。最后,如果你要做全球化跨时区的应用,一定要把所有服务器和时间全部使用 UTC 时间。

性能提示

Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面我给你提供一份在编程方面和性能相关的提示。

  • 如果需要把数字转换成字符串,使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。

  • 尽可能避免把String转成[]Byte ,这个转换会导致性能下降。使用下面的方式。

    // ToString 将 []byte 转换为 string func ToString(p []byte) string { return *(*string)(unsafe.Pointer(&p)) } // ToBytes 将 string 转换为 []byte func ToBytes(str string) []byte { return *(*[]byte)(unsafe.Pointer(&str)) }

    都是不安全的,需要确保只读,不修改。

  • 如果在 for-loop 里对某个 Slice 使用 append(),请先把 Slice 的容量扩充到位,这样可以避免内存重新分配以及系统自动按 2 的 N 次方幂进行扩展但又用不到的情况,从而避免浪费内存。

  • 使用StringBuffer 或是StringBuild 来拼接字符串,性能会比使用 + 或 +=高三到四个数量级。

  • 尽可能使用并发的 goroutine,然后使用 sync.WaitGroup 来同步分片操作。

  • 避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能使用 sync.Pool 来重用对象。

  • 使用 lock-free 的操作,避免使用 mutex,尽可能使用 sync/Atomic包(关于无锁编程的相关话题,可参看《无锁队列实现》《无锁 Hashmap 实现》)。

  • 使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用 bufio.NewWrite() 和 bufio.NewReader() 可以带来更高的性能。

  • 对于在 for-loop 里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会提升两个数量级。

  • 如果你需要更高性能的协议,就要考虑使用 protobufmsgp 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。

  • 你在使用 Map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。

资料汇总

《Go 语言简介(上):接口与多态》

《你确信你了解时间吗?》

《关于闰秒》

RFC 3339

《无锁队列实现》

《无锁 Hashmap 实现》

Effective Go

Uber Go Style

50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

Go Advice

Practical Go Benchmarks

Benchmarks of Go serialization methods

Debugging performance issues in Go programs

Go code refactoring: the 23x performance hunt

Go 编程模式:错误处理

错误处理一直是编程必须要面对的问题。错误处理如果做得好的话,代码的稳定性会很好。不同的语言有不同的错误处理的方式。Go 语言也一样,这节课,我们来讨论一下 Go 语言的错误出处,尤其是那令人抓狂的 if err != nil 。在正式讨论“Go 代码里满屏的 if err != nil 怎么办”这件事儿之前,我想先说一说编程中的错误处理。

C 语言的错误检查

首先,我们知道,处理错误最直接的方式是通过错误码,这也是传统的方式,在过程式语言中通常都是用这样的方式处理错误的。比如 C 语言,基本上来说,其通过函数的返回值标识是否有错,然后通过全局的 errno 变量加一个 errstr 的数组来告诉你为什么出错。

为什么是这样的设计呢?道理很简单,除了可以共用一些错误,更重要的是这其实是一种妥协,比如:read()、 write()、 open() 这些函数的返回值其实是返回有业务逻辑的值,也就是说,这些函数的返回值有两种语义:

  • 一种是成功的值,比如 open() 返回的文件句柄指针 FILE* ;
  • 另一种是错误 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, &endptr, 10); //10的意思是10进制 //如果无法转换 if (endptr == str) { fprintf(stderr, "No digits were found\n"); exit(EXIT_FAILURE); } //如果整型溢出了 if ((errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)) { fprintf(stderr, "ERROR: number out of range for LONG\n"); exit(EXIT_FAILURE); } //如果是其它错误 if (errno != 0 && val == 0) { perror("strtol"); exit(EXIT_FAILURE); }

虽然,strtol() 函数解决了 atoi() 函数的问题,但是我们还是能感觉到不是很舒服,也不是很自然。因为这种用返回值 + errno 的错误检查方式会有一些问题:程序员一不小心就会忘记检查返回值,从而造成代码的 Bug;函数接口非常不纯洁,正常值和错误值混淆在一起,导致语义有问题。

  • 程序员一不小心就会忘记检查返回值,从而造成代码的 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 想到的》,你可以点击链接看一下)。
  • C++ 语言:一般来说使用 RAII 模式,通过面向对象的代理模式,把需要清理的资源交给一个代理类,然后再析构函数来解决。
  • Java 语言:可以在 finally 语句块里进行清理。
  • Go 语言:使用 defer 关键词进行清理。

下面是一个 Go 语言的资源清理的示例:

func Close(c io.Closer) { err := c.Close() if err != nil { log.Fatal(err) } } func main() { r, err := Open("a") if err != nil { log.Fatalf("error opening 'a'\n") } defer Close(r) // 使用defer关键字在函数退出时关闭文件。 r, err = Open("b") if err != nil { log.Fatalf("error opening 'b'\n") } 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, &p.Longitude); err != nil { return nil, err } if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil { return nil, err } if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil { return nil, err } if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil { return nil, err } if err := binary.Read(r, binary.BigEndian, &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(&p.Longitude) read(&p.Latitude) read(&p.Distance) read(&p.ElevationGain) read(&p.ElevationLoss) if err != nil { return &p, err } return &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(&p.Longitude) r.read(&p.Latitude) r.read(&p.Distance) r.read(&p.ElevationGain) r.read(&p.ElevationLoss) if r.err != nil { return nil, r.err } return &p, nil }

有了刚刚的这个技术,我们的“流式接口 Fluent Interface”也就很容易处理了。如下所示:

package main import ( "bytes" "encoding/binary" "fmt" ) // 长度不够,少一个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(&p.Name) return p } func (p *Person) ReadAge() *Person { p.read(&p.Age) return p } func (p *Person) ReadWeight() *Person { p.read(&p.Weight) return p } func (p *Person) Print() *Person { if p.err == nil { fmt.Printf("Name=%s, Age=%d, Weight=%d\n",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("something failed: %v", err) }

另外,在 Go 语言的开发者中,更为普遍的做法是将错误包装在另一个错误中,同时保留原始内容:

type authorizationError struct { operation string err error // original error } func (e *authorizationError) Error() string { return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err) }

这个过时了。Go1.13已经支持了嵌套错误。

资料汇总

Golang Error Handling lesson by Rob Pike

Errors are values

“流式接口 Fluent Interface”

《由苹果的低级 BUG 想到的》
RAII

Go 编程模式:Functional Options

这节课,我们来讨论一下 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 &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil } func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) { return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil } func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) { return &Server{addr, port, "tcp", timeout, 100, nil}, nil } func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) { return &Server{addr, port, "tcp", 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("localhost", 9000, nil) conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration} srv2, _ := NewServer("locahost", 9000, &conf)

这段代码算是不错了,大多数情况下,我们可能就止步于此了。但是,对于有洁癖的、有追求的程序员来说,他们会看到其中不太好的一点,那就是Config 并不是必需的,所以,你需要判断是否是 nil 或是 Empty—— Config{}会让我们的代码感觉不太干净。

Builder模式

如果你是一个 Java 程序员,熟悉设计模式的一定会很自然地使用 Builder 模式。比如下面的代码:

User user = new User.Builder() .name("Hao Chen") .email("haoel@hotmail.com") .nickname("左耳朵") .build();

仿照这个模式,我们可以把刚刚的代码改写成下面的样子

//使用一个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("127.0.0.1", 8080). WithProtocol("udp"). 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: "tcp", Timeout: 30 * time.Second, MaxConns: 1000, TLS: nil, } for _, option := range options { option(&srv) } //... return &srv, nil }

于是,我们在创建 Server 对象的时候,就可以像下面这样:

s1, _ := NewServer("localhost", 1024) s2, _ := NewServer("localhost", 2048, Protocol("udp")) s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))

怎么样,是不是高度整洁和优雅?这不但解决了“使用 Config 对象方式的需要有一个 config 参数,但在不需要的时候,是放 nil 还是放 Config{}”的选择困难问题,也不需要引用一个 Builder 的控制对象,直接使用函数式编程,在代码阅读上也很优雅。所以,以后,你要玩类似的代码时,我强烈推荐你使用 Functional Options 这种方式,这种方式至少带来了 6 个好处:

  • 直觉式的编程;
  • 高度的可配置化;
  • 很容易维护和扩展;
  • 自文档;
  • 新来的人很容易上手;
  • 没有什么令人困惑的事(是 nil 还是空)。

资料汇总

Self referential functions and design, by Rob Pike

Go编程模式:委托和反转控制

控制反转(Inversion of Control,loC )是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。

我之前在《IoC/DIP 其实是一种管理思想》这篇文章中,举过一个开关和电灯的例子。其实,这里的开关就是控制逻辑,电器是业务逻辑。我们不要在电器中实现开关,而是要把开关抽象成一种协议,让电器都依赖它。这样的编程方式可以有效降低程序复杂度,并提升代码重用度。面向对象的设计模式我就不提了,我们来看看 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}, "State:"} 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("%p:Label.Paint(%q)\n", &label, label.Text) } //因为这个接口可以通过 Label 的嵌入带到新的结构体, //所以,可以在 Button 中重载这个接口方法 func (button Button) Paint() { // Override fmt.Printf("Button.Paint(%s)\n", button.Text) } func (button Button) Click() { fmt.Printf("Button.Click(%s)\n", button.Text) } func (listBox ListBox) Paint() { fmt.Printf("ListBox.Paint(%q)\n", listBox.Texts) } func (listBox ListBox) Click() { fmt.Printf("ListBox.Click(%q)\n", listBox.Texts) }

说到这儿,我要重点提醒你一下,Button.Paint() 接口可以通过 Label 的嵌入带到新的结构体,如果 Button.Paint() 不实现的话,会调用 Label.Paint() ,所以,在 Button 中声明 Paint() 方法,相当于 Override。

嵌入结构多态

从下面的程序中,我们可以看到整个多态是怎么执行的。

button1 := Button{Label{Widget{10, 70}, "OK"}} button2 := NewButton(50, 70, "Cancel") listBox := ListBox{Widget{10, 40}, []string{"AL", "AK", "AZ", "AR"}, 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("No functions to undo") } 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("No functions to undo") } 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 的代码就可以复用了。

资料汇总

loC

《IoC/DIP 其实是一种管理思想》

05 | Go 编程模式:Map-Reduce

这节课,我们来学习一下函数式编程中非常重要的 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{"Hao", "Chen", "MegaEase"} x := MapStrToStr(list, func(s string) string { return strings.ToUpper(s) }) fmt.Printf("%v\n", x) //["HAO", "CHEN", "MEGAEASE"] y := MapStrToInt(list, func(s string) int { return len(s) }) fmt.Printf("%v\n", 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{"Hao", "Chen", "MegaEase"} x := Reduce(list, func(s string) int { return len(s) }) fmt.Printf("%v\n", 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("%v\n", out) out = Filter(intset, func(n int) bool { return n > 5 }) fmt.Printf("%v\n", out)

为了方便你理解呢,我给你展示一张图,它形象地说明了 Map-Reduce 的业务语义,在数据处理中非常有用。

img

业务示例

通过刚刚的一些示例,你现在应该有点明白了,Map、Reduce、Filter 只是一种控制逻辑,真正的业务逻辑是以传给它们的数据和函数来定义的。是的,这是一个很经典的“业务逻辑”和“控制逻辑”分离解耦的编程模式。接下来,我们来看一个有业务意义的代码,来进一步帮助你理解什么叫“控制逻辑”与“业务逻辑”分离。

员工信息

首先,我们有一个员工对象和一些数据:

type Employee struct { Name string Age int Vacation int Salary int } var list = []Employee{ {"Hao", 44, 0, 8000}, {"Bob", 34, 10, 5000}, {"Alice", 23, 5, 9000}, {"Jack", 26, 0, 4000}, {"Tom", 48, 9, 7500}, {"Marry", 29, 0, 6000}, {"Mike", 32, 8, 4000}, }

相关的 Reduce、Fitler 函数

然后,我们有下面的几个函数:

func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int { count := 0 for i, _ := range list { if fn(&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(&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(&list[i]) } return sum }

简单说明一下:

  • EmployeeConutIf 和 EmployeeSumIf 分别用于统计满足某个条件的个数或总数。它们都是 Filter + Reduce 的语义。
  • EmployeeFilterIn 就是按某种条件过滤,就是 Fitler 的语义。

各种自定义的统计示例

于是,我们就可以有接下来的代码了。

  1. 统计有多少员工大于 40 岁

    old := EmployeeCountIf(list, func(e *Employee) bool { return e.Age > 40 }) fmt.Printf("old people: %d\n", old) //old people: 2
  2. 统计有多少员工的薪水大于 6000

    high_pay := EmployeeCountIf(list, func(e *Employee) bool { return e.Salary > 6000 }) fmt.Printf("High Salary people: %d\n", high_pay) //High Salary people: 4
  3. 列出有没有休假的员工

    no_vacation := EmployeeFilterIn(list, func(e *Employee) bool { return e.Vacation == 0 }) fmt.Printf("People no vacation: %v\n", 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("Total Salary: %d\n", total_pay) //Total Salary: 43500
  5. 统计 30 岁以下员工的薪资总和

    younger_pay := EmployeeSumIf(list, func(e *Employee) int { if e.Age < 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 < 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{"Hao", "Chen", "MegaEase"} 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("transform: not slice") } //check the function signature fn := reflect.ValueOf(function) elemType := sliceInType.Type().Elem() if !verifyFuncSignature(fn, elemType, nil) { panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType") } sliceOutType := sliceInType if !inPlace { sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len()) } for i := 0; i < 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 < 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 && fn.Type().Out(0) != outType { return false } return true }

代码一下子就复杂起来了,可见,复杂的代码都是在处理异常的地方。我不打算 Walk through 所有的代码,别看代码多,还是可以读懂的。

我来列一下代码中的几个要点。

  • 代码中没有使用 Map 函数,因为和数据结构有含义冲突的问题,所以使用Transform,这个来源于 C++ STL 库中的命名。
  • 有两个版本的函数,一个是返回一个全新的数组 Transform(),一个是“就地完成” TransformInPlace()。
  • 在主函数中,用 Kind() 方法检查了数据类型是不是 Slice,函数类型是不是 Func。
  • 检查函数的参数和返回类型是通过 verifyFuncSignature() 来完成的:NumIn()用来检查函数的“入参”;NumOut() :用来检查函数的“返回值”。
  • 如果需要新生成一个 Slice,会使用 reflect.MakeSlice() 来完成。好了,有了这段代码,我们的代码就很可以很开心地使用了:

好了,有了这段代码,我们的代码就很可以很开心地使用了:

  1. 可以用于字符串数组:

    list := []string{"1", "2", "3", "4", "5", "6"} result := Transform(list, func(a string) string{ return a +a +a }) //{"111","222","333","444","555","666"}
  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{ {"Hao", 44, 0, 8000}, {"Bob", 34, 10, 5000}, {"Alice", 23, 5, 9000}, {"Jack", 26, 0, 4000}, {"Tom", 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("reduce: wrong type, not slice") } 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("reduce: function must be of type func(" + t + ", " + t + ") " + 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 < 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("FilterInPlace: wrong type, " + "not a pointer to slice") } _, 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("filter: wrong type, not a slice") } fn := reflect.ValueOf(function) elemType := sliceInType.Type().Elem() if !verifyFuncSignature(fn, elemType, boolType) { panic("filter: function must be of type func(" + elemType.String() + ") bool") } var which []int for i := 0; i < 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. 使用反射来做这些东西会有一个问题,那就是代码的性能会很差。所以,上面的代码不能用在需要高性能的地方。怎么解决这个问题,我会在下节课给你介绍下。

  2. 这节课中的代码大量地参考了 Rob Pike 的版本,你可以点击这个链接查看: https://github.com/robpike/filter。

  3. 其实,在全世界范围内,有大量的程序员都在问 Go 语言官方什么时候在标准库中支持 Map、Reduce。Rob Pike 说,这种东西难写吗?还要我们官方来帮你们写吗?这种代码我多少年前就写过了,但是,我一次都没有用过,我还是喜欢用“For 循环”,我觉得你最好也跟我一起用 “For 循环”。

我个人觉得,Map、Reduce 在数据处理的时候还是很有用的,Rob Pike 可能平时也不怎么写“业务逻辑”的代码,所以,他可能也不太了解业务的变化有多么频繁……

当然,好还是不好,由你来判断,但多学一些编程模式,一定是对自己很有帮助的。

好了,这节课就到这里。如果你觉得今天的内容对你有所帮助,欢迎你帮我分享给更多人。

资料汇总

https://github.com/robpike/filter

06 | Go 编程模式:Go Generation

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 := &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("Unable to read an int from intContainer") } fmt.Printf("assertExample: %d (%T)\n", elem, elem)

Reflection

对于 Reflection,我们需要把上面的代码修改如下:

type Container struct { s reflect.Value } func NewContainer(t reflect.Type, size int) *Container { if size <=0 { size=64 } return &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", 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("Get: needs *%s but got %T", 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(&g); err != nil { panic(err) } fmt.Printf("%v (%T)\n", g, g) //3.1415926 (float64) fmt.Println(c.s.Index(0)) //1.4142135623

可以看到,Type Assert 是不用了,但是用反射写出来的代码还是有点复杂的。那么,有没有什么好的方法?

他山之石

对于泛型编程最牛的语言 C++ 来说,这类问题都是使用 Template 解决的。

//用<class T>来描述泛型 template <class T> T GetMax (T a, T b) { T result; result = (a>b)? a : b; return (result); }
int i=5, j=6, k; //生成int类型的函数 k=GetMax<int>(i,j); long l=10, m=5, n; //生成long类型的函数 n=GetMax<long>(l,m);

C++ 的编译器会在编译时分析代码,根据不同的变量类型来自动化生成相关类型的函数或类,在 C++ 里,叫模板的具体化。这个技术是编译时的问题,所以,我们不需要在运行时进行任何的类型识别,我们的程序也会变得比较干净。那么,我们是否可以在 Go 中使用 C++ 的这种技术呢?答案是肯定的,只是 Go 的编译器不会帮你干,你需要自己动手。

Go Generator

要玩 Go 的代码生成,你需要三个东西:

  1. 一个函数模板,在里面设置好相应的占位符;
  2. 一个脚本,用于按规则来替换文本并生成新的代码;
  3. 一行注释代码。

函数模板

我们把之前的示例改成模板,取名为 container.tmp.go 放在 ./template/下:

package PACKAGE_NAME type GENERIC_NAMEContainer struct { s []GENERIC_TYPE } func NewGENERIC_NAMEContainer() *GENERIC_NAMEContainer { return &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="$(tr '[:lower:]' '[:upper:]' <<< ${TYPE:0:1})${TYPE:1}" DES_FILE=$(echo ${TYPE}| tr '[:upper:]' '[:lower:]')_${DES}.go sed 's/PACKAGE_NAME/'"${PACKAGE}"'/g' ${SRC_FILE} | \ sed 's/GENERIC_TYPE/'"${TYPE}"'/g' | \ sed 's/GENERIC_NAME/'"${PREFIX}"'/g' > ${DES_FILE}

这里需要 4 个参数:

  • 模板源文件;

  • 包名;

  • 实际需要具体化的类型;

  • 用于构造目标文件名的后缀。

    然后,我们用 sed 命令去替换刚刚的函数模板,并生成到目标文件中(关于 sed 命令,我给你推荐一篇文章:《sed 简明教程》)。

生成代码

接下来,我们只需要在代码中打一个特殊的注释:

//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("generateExample: %d (%T)\n", v, v) } //go:generate ./gen.sh ./template/container.tmp.go gen string container func generateStringExample() { var s string = "Hello" c := NewStringContainer() c.Put(s) v := c.Get() fmt.Printf("generateExample: %s (%T)\n", v, v) }

其中,

  • 第一个注释是生成包名 gen,类型是 uint32,目标文件名以 container 为后缀。

  • 第二个注释是生成包名 gen,类型是 string,目标文件名是以 container 为后缀。

然后,在工程目录中直接执行 go generate 命令,就会生成两份代码:

这两份代码可以让我们的代码完全编译通过,付出的代价就是需要多执行一步 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(&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{ {"Hao", 44, 0, 8000}, {"Bob", 34, 10, 5000}, {"Alice", 23, 5, 9000}, {"Jack", 26, 0, 4000}, {"Tom", 48, 9, 7500}, } var filter EmployeeList filter = list.Filter(func(e *Employee) bool { return e.Age > 40 }) fmt.Println("----- Employee.Age > 40 ------") for _, e := range filter { fmt.Println(e) } filter = list.Filter(func(e *Employee) bool { return e.Salary <= 5000 }) fmt.Println("----- Employee.Salary <= 5000 ------") for _, e := range filter { fmt.Println(e) } }

第三方工具

我们并不需要自己手写 gen.sh 这样的工具类,我们可以直接使用第三方已经写好的工具。我给你提供一个列表。

genny

generic

gengen

gen

汇总资料

genny

generic

gengen

gen

《sed 简明教程》

07 | Go编程模式:修饰器

之前,我写过一篇文章《Python 修饰器的函数式编程》,这种模式可以很轻松地把一些函数装配到另外一些函数上,让你的代码更加简单,也可以让一些“小功能型”的代码复用性更高,让代码中的函数可以像乐高玩具那样自由地拼装。所以,一直以来,我都对修饰器(Decoration)这种编程模式情有独钟,这节课,我们就来聊聊 Go 语言的修饰器编程模式。如果你看过我刚说的文章,就一定知道,这是一种函数式编程的玩法——用一个高阶函数来包装一下。

多唠叨一句,关于函数式编程,我之前还写过一篇文章《函数式编程》,这篇文章主要是想通过详细介绍从过程式编程的思维方式过渡到函数式编程的思维方式,带动更多的人玩函数式编程。所以,如果你想了解一下函数式编程,那么可以点击链接阅读一下这篇文章。其实,Go 语言的修饰器编程模式,也就是函数式编程的模式。不过,要提醒你注意的是,Go 语言的“糖”不多,而且又是强类型的静态无虚拟机的语言,所以,没有办法做到像 Java 和 Python 那样写出优雅的修饰器的代码。当然,也许是我才疏学浅,如果你知道更多的写法,请你一定告诉我。先谢过了。

简单示例

我们先来看一个示例:

package main import "fmt" func decorator(f func(s string)) func(s string) { return func(s string) { fmt.Println("Started") f(s) fmt.Println("Done") } } func Hello(s string) { fmt.Println(s) } func main() { decorator(Hello)("Hello, World!") }

可以看到,我们动用了一个高阶函数 decorator(),在调用的时候,先把 Hello() 函数传进去,然后会返回一个匿名函数。这个匿名函数中除了运行了自己的代码,也调用了被传入的 Hello() 函数。

这个玩法和 Python 的异曲同工,只不过,有些遗憾的是,Go 并不支持像 Python 那样的 @decorator 语法糖。所以,在调用上有些难看。当然,如果你想让代码更容易读,你可以这样写:

hello := decorator(Hello) hello("Hello")

我们再来看一个计算运行时间的例子:

package main import ( "fmt" "reflect" "runtime" "time" ) 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("--- Time Elapsed (%s): %v ---\n", getFunctionName(f), time.Since(t)) }(time.Now()) return f(start, end) } } func Sum1(start, end int64) int64 { var sum int64 sum = 0 if start > end { start, end = end, start } for i := start; i <= end; i++ { sum += i } return sum } func Sum2(start, end int64) int64 { if start > end { start, end = end, start } return (end - start + 1) * (end + start) / 2 } func main() { sum1 := timedSumFunc(Sum1) sum2 := timedSumFunc(Sum2) fmt.Printf("%d, %d\n", sum1(-10000, 10000000), sum2(-10000, 10000000)) }

关于这段代码,有几点我要说明一下:

  • 有两个 Sum 函数,Sum1() 函数就是简单地做个循环,Sum2() 函数动用了数据公式(注意:start 和 end 有可能有负数);
  • 代码中使用了 Go 语言的反射机制来获取函数名;
  • 修饰器函数是 timedSumFunc()。

HTTP 相关的一个示例

接下来,我们再看一个处理 HTTP 请求的相关例子。

先看一个简单的 HTTP Server 的代码:

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 的,有打日志的……

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

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

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("/v4/hello", Handler(hello, WithServerHeader, WithBasicAuth, WithDebugLog))

这样的代码是不是更易读了一些?Pipeline 的功能也就出来了。

泛型的修饰器

不过,对于 Go 的修饰器模式,还有一个小问题,那就是好像无法做到泛型。比如上面那个计算时间的函数,其代码耦合了需要被修饰的函数的接口类型,无法做到非常通用。如果这个问题解决不了,那么,这个修饰器模式还是有点不好用的。

因为 Go 语言不像 Python 和 Java,Python 是动态语言,而 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("before") out = targetFunc.Call(in) fmt.Println("after") return }) decoratedFunc.Set(v) return }

这段代码动用了 reflect.MakeFunc() 函数,创造了一个新的函数,其中的 targetFunc.Call(in) 调用了被修饰的函数。关于 Go 语言的反射机制,你可以阅读下官方文章The Laws of Reflection,我就不多说了。

这个 Decorator() 需要两个参数:

  • 第一个是出参 decoPtr ,就是完成修饰后的函数;

  • 第二个是入参 fn ,就是需要修饰的函数。

这样写是不是有些“傻”?的确是的。不过,这是我个人在 Go 语言里所能写出来的最好的代码了。如果你知道更多优雅的写法,请你要一定告诉我!好了,让我们来看一下使用效果。首先,假设我们有两个需要修饰的函数:

func foo(a, b, c int) int { fmt.Printf("%d, %d, %d \n", a, b, c) return a + b + c } func bar(a, b string) string { fmt.Printf("%s, %s \n", a, b) return a + b }

然后,我们可以这样做:

type MyFoo func(int, int, int) int var myfoo MyFoo Decorator(&myfoo, foo) myfoo(1, 2, 3)

你会发现,使用 Decorator() 时,还需要先声明一个函数签名,感觉好傻啊,一点都不泛型,不是吗?如果你不想声明函数签名,就可以这样:

mybar := bar Decorator(&mybar, bar) mybar("hello,", "world!")

看样子 Go 语言目前本身的特性无法做成像 Java 或 Python 那样,对此,我们只能期待 Go 语言多放“糖”了!

资料汇总

《Python 修饰器的函数式编程》

《函数式编程》

The Laws of Reflection

08 | Go编程模式:Pipeline

这节课,我着重介绍一下 Go 编程中的 Pipeline 模式。对于 Pipeline,用过 Unix/Linux 命令行的人都不会陌生,它是一种把各种命令拼接起来完成一个更强功能的技术方法。

现在的流式处理、函数式编程、应用网关对微服务进行简单的 API 编排,其实都是受 Pipeline 这种技术方式的影响。Pipeline 可以很容易地把代码按单一职责的原则拆分成多个高内聚低耦合的小模块,然后轻松地把它们拼装起来,去完成比较复杂的功能。

HTTP 处理

上节课,我们有很多 WithServerHead() 、WithBasicAuth() 、WithDebugLog()这样的小功能代码,在需要实现某个 HTTP API 的时候,我们就可以很轻松地把它们组织起来。原来的代码是下面这个样子:

http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello))) http.HandleFunc("/v2/hello", WithServerHeader(WithBasicAuth(hello))) http.HandleFunc("/v3/hello", 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("/v4/hello", Handler(hello, WithServerHeader, WithBasicAuth, WithDebugLog))

Channel 管理

当然,如果你要写出一个泛型的 Pipeline 框架并不容易,可以使用Go Generation实现,但是,我们别忘了,Go 语言最具特色的 Go Routine 和 Channel 这两个神器完全可以用来构造这种编程。Rob Pike 在 Go Concurrency Patterns: Pipelines and cancellation 这篇博客中介绍了一种编程模式,下面我们来学习下。

略。就是一个channel -> 另一个channel->…

Fan in/Out

略。

参考文档

https://www.youtube.com/watch?v=f6kdp27TYZs

https://blog.golang.org/advanced-go-concurrency-patterns

https://blog.golang.org/advanced-go-concurrency-patterns

https://swtch.com/~rsc/thread/squint.pdf

汇总文档

https://www.youtube.com/watch?v=f6kdp27TYZs

https://blog.golang.org/advanced-go-concurrency-patterns

https://blog.golang.org/advanced-go-concurrency-patterns

https://swtch.com/~rsc/thread/squint.pdf

泛型的 Pipeline 框架

Go Concurrency Patterns: Pipelines and cancellation

09 | Go 编程模式:Kubernetes Visitor模式

这节课,我们来重点讨论一下,Kubernetes 的 kubectl 命令中的使用到的一个编程模式:Visitor(其实,kubectl 主要使用到了两个,一个是 Builder,另一个是 Visitor)。

本来,Visitor 是面向对象设计模式中一个很重要的设计模式(可以看下 Wikipedia Visitor Pattern 词条),这个模式是将算法与操作对象的结构分离的一种方法。这种分离的实际结果是能够在不修改结构的情况下向现有对象结构添加新操作,是遵循开放 / 封闭原则的一种方法。这节课,我们重点学习一下 kubelet 中是怎么使用函数式的方法来实现这个模式的。

一个简单示例

首先,我们来看一个简单设计模式的 Visitor 的示例。

  • 我们的代码中有一个Visitor的函数定义,还有一个Shape接口,这需要使用 Visitor函数作为参数。
  • 我们的实例的对象 Circle和 Rectangle实现了 Shape 接口的 accept() 方法,这个方法就是等外面给我们传递一个 Visitor。
package main import ( "encoding/json" "encoding/xml" "fmt" ) 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 地图 ,了解下有多复杂)。
  • kubectl 是 Kubernetes 中的一个客户端命令,操作人员用这个命令来操作 Kubernetes。kubectl 会联系到 Kubernetes 的 API Server,API Server 会联系每个节点上的 kubelet ,从而控制每个节点。
  • kubectl 的主要工作是处理用户提交的东西(包括命令行参数、YAML 文件等),接着会把用户提交的这些东西组织成一个数据结构体,发送给 API Server。
  • 相关的源代码在 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("NameVisitor() before call function") err = fn(info, err) if err == nil { fmt.Printf("==> Name=%s, NameSpace=%s\n", info.Name, info.Namespace) } fmt.Println("NameVisitor() after call function") return err }) }

可以看到,在这段代码中:

  • 声明了一个 NameVisitor 的结构体,这个结构体里有一个 Visitor 接口成员,这里意味着多态;
  • 在实现 Visit() 方法时,调用了自己结构体内的那个 Visitor的 Visitor() 方法,这其实是一种修饰器的模式,用另一个 Visitor 修饰了自己

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("OtherThingsVisitor() before call function") err = fn(info, err) if err == nil { fmt.Printf("==> OtherThings=%s\n", info.OtherThings) } fmt.Println("OtherThingsVisitor() after call function") 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("LogVisitor() before call function") err = fn(info, err) fmt.Println("LogVisitor() after call function") return err }) }

使用方代

现在,我们看看使用上面的代码:

func main() { info := Info{} var v Visitor = &info v = LogVisitor{v} v = NameVisitor{v} v = OtherThingsVisitor{v} loadFile := func(info *Info, err error) error { info.Name = "Hao Chen" info.Namespace = "MegaEase" info.OtherThings = "We are running as remote team." return nil } v.Visit(loadFile) }

可以看到,

  • Visitor 们一层套一层;
  • 我用 loadFile 假装从文件中读取数据;
  • 最后执行 v.Visit(loadfile) ,这样,我们上面的代码就全部开始激活工作了。

上面的代码有以下几种功效:解耦了数据和程序;使用了修饰器模式;还做出了 Pipeline 的模式。所以,其实我们可以重构一下上面的代码。

Visitor 修饰器

我们用修饰器模式来重构一下上面的代码。

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 = &info v = NewDecoratedVisitor(v, NameVisitor, OtherVisitor) v.Visit(LoadFile)

资料大汇总

《Go 语言简介(上):接口与多态》

《你确信你了解时间吗?》

《关于闰秒》

RFC 3339

《无锁队列实现》

《无锁 Hashmap 实现》

Effective Go

Uber Go Style

50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

Go Advice

Practical Go Benchmarks

Benchmarks of Go serialization methods

Debugging performance issues in Go programs

Go code refactoring: the 23x performance hunt

Golang Error Handling lesson by Rob Pike

Errors are values

“流式接口 Fluent Interface”

《由苹果的低级 BUG 想到的》
RAII

Self referential functions and design, by Rob Pike

loC

《IoC/DIP 其实是一种管理思想》

https://github.com/robpike/filter

genny

generic

gengen

gen

《sed 简明教程》

《Python 修饰器的函数式编程》

《函数式编程》

The Laws of Reflection

https://www.youtube.com/watch?v=f6kdp27TYZs

https://blog.golang.org/advanced-go-concurrency-patterns

https://blog.golang.org/advanced-go-concurrency-patterns

https://swtch.com/~rsc/thread/squint.pdf

泛型的 Pipeline 框架

Go Concurrency Patterns: Pipelines and cancellation

本文链接:http://blog.go2live.cn/post/go-prictise.html

-- EOF --