CategoryResourceRepost/极客时间专栏/左耳听风/Go语言编程模式/109 | Go 编程模式:Functional Options.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

252 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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