专栏名称: GoCN
最具规模和生命力的 Go 开发者社区
目录
相关文章推荐
安徽省人民政府网  ·  最高25℃!安徽气温大回暖就在下周 ·  15 小时前  
安徽省人民政府网  ·  最高25℃!安徽气温大回暖就在下周 ·  15 小时前  
国际旅游岛商报  ·  刚刚通知!海口这些区域或停水→ ·  2 天前  
吉林省消费者协会  ·  【消费提示】这份关于学生书包的消费提示请查收 ·  2 天前  
吉林省消费者协会  ·  【消费提示】这份关于学生书包的消费提示请查收 ·  2 天前  
木木说卡  ·  太卷了,白金门槛又降低 ·  3 天前  
木木说卡  ·  白金权益,免费送! ·  4 天前  
51好读  ›  专栏  ›  GoCN

GO 编程模式系列(三):FUNCTIONAL OPTIONS

GoCN  · 公众号  ·  · 2021-01-06 18:09

正文

原文作者 陈皓(左耳朵耗子)

内容出

https://coolshell.cn/articles/21146.html


在本篇文章中,我们来讨论一下Functional Options这个编程模式。这是一个函数式编程的应用案例,编程技巧也很好,是目前在Go语言中最流行的一种编程模式。但是,在我们正式讨论这个模式之前,我们需要先来看看要解决什么样的问题。

本文是全系列中第3 / 9篇:Go编程模式

  • Go编程模式:切片,接口,时间和性能
  • Go 编程模式:错误处理
  • Go 编程模式:Functional Options
  • Go编程模式:委托和反转控制
  • Go编程模式:Map-Reduce
  • Go 编程模式:Go Generation
  • Go编程模式:修饰器
  • Go编程模式:Pipeline
  • Go 编程模式:k8s Visitor 模式

目录

  • 配置选项问题
  • 配置对象方案
  • Builder模式
  • Functional Options

配置选项问题


在我们编程中,我们会经常性的需要对一个对象(或是业务实体)进行相关的配置。比如下面这个业务实体(注意,这仅只是一个示例):

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 configuratrionsrv1, _ := 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();

仿照上面这个模式,我们可以把上面代码改写成如下的代码(注:下面的代码没有考虑出错处理,其中关于出错处理的更多内容,请参看《 Go 编程模式:出错处理 》):

//使用一个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 构造,的确是这样的。但是在处理错误的时候可能就有点麻烦(需要为Server结构增加一个error 成员,破坏了Server结构体的“纯洁”),不如一个包装类更好一些。

如果我们想省掉这个包装的结构体,那么就轮到我们的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    }}

上面这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的







请到「今天看啥」查看全文