Go编程模式 - 5.函数式选项

简介: 编程的一大重点,就是要 `分离变化点和不变点`。这里,我们可以将必填项认为是不变点,而非必填则是变化点。

目录

ServerConfig

我们先来看看一个常见的HTTP服务器的配置,它区分了2个必填参数与4个非必填参数

type ServerCfg struct {
   
    Addr     string        // 必填
    Port     int           // 必填
    Protocol string        // 非必填
    Timeout  time.Duration // 非必填
    MaxConns int           // 非必填
    TLS      *tls.Config   // 非必填
}

// 我们要实现非常多种方法,来支持各种非必填的情况,示例如下
func NewServer(addr string, port int) (*Server, error)                                   {
   }
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error)               {
   }
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
   }
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
   }

SplitConfig

编程的一大重点,就是要 分离变化点和不变点。这里,我们可以将必填项认为是不变点,而非必填则是变化点。

我们将非必填的选项拆分出来。

type Config struct {
   
    Protocol string
    Timeout  time.Duration
    MaxConns int
    TLS      *tls.Config
}

type Server struct {
   
    Addr string
    Port int
    Conf *Config
}

func NewServer(addr string, port int, conf *Config) (*Server, error) {
   
    return &Server{
   
        Addr: addr,
        Port: port,
        Conf: conf,
    }, nil
}

func main() {
   
    srv1, _ := NewServer("localhost", 9000, nil)

    conf := Config{
   Protocol: "tcp", Timeout: 60 * time.Second}
    srv2, _ := NewServer("localhost", 9000, &conf)

    fmt.Println(srv1, srv2)
}

到这里,其实已经满足大部分的开发需求了。那么,我们将进入今天的重点。

Functional Option

type Server struct {
   
    Addr     string
    Port     int
    Protocol string
    Timeout  time.Duration
    MaxConns int
    TLS      *tls.Config
}

// 定义一个Option类型的函数,它操作了Server这个对象
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
    }
}

// 用到了不定参数的特性,将任意个option应用到Server上
func NewServer(addr string, port int, options ...Option) (*Server, error) {
   
    // 先填写默认值
    srv := Server{
   
        Addr:     addr,
        Port:     port,
        Protocol: "tcp",
        Timeout:  30 * time.Second,
        MaxConns: 1000,
        TLS:      nil,
    }
    // 应用任意个option
    for _, option := range options {
   
        option(&srv)
    }
    return &srv, nil
}

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

    fmt.Println(s1, s2, s3)
}

耗子哥给出了6个点,但我感受最深的是以下两点:

  1. 可读性强,将配置都转化成了对应的函数项option
  2. 扩展性好,新增参数只需要增加一个对应的方法

那么对应的代价呢?就是需要编写多个Option函数,代码量会有所增加。

如果大家对这个感兴趣,可以去看一下Rob Pike的这篇blog

Further

顺着耗子叔的例子,我们再思考一下,如果配置的过程中有参数限制,那么我们该怎么办呢?

首先,我们改造一下函数Option

// 返回错误
type OptionWithError func(*Server) error

然后,我们改造一下其中两个函数作为示例

func Protocol(p string) OptionWithError {
   
    return func(s *Server) error {
   
        if p == "" {
   
            return errors.New("empty protocol")
        }
        s.Protocol = p
        return nil
    }
}

func Timeout(timeout time.Duration) Option {
   
    return func(s *Server) error {
   
        if timeout.Seconds() < 1 {
   
            return errors.New("time out should not less than 1s")
        }
        s.Timeout = timeout
        return nil
    }
}

我们再做一次改造

func NewServer(addr string, port int, options ...OptionWithError) (*Server, error) {
   
    srv := Server{
   
        Addr:     addr,
        Port:     port,
        Protocol: "tcp",
        Timeout:  30 * time.Second,
        MaxConns: 1000,
        TLS:      nil,
    }
    // 增加了一个参数验证的步骤
    for _, option := range options {
   
        if err := option(&srv); err != nil {
   
            return nil, err
        }
    }
    return &srv, nil
}

改造基本到此完成,希望能给大家带来一定的帮助。

目录
相关文章
|
存储 Go
Go 语言开源项目使用的函数选项模式
Go 语言开源项目使用的函数选项模式
67 0
|
中间件 Go 数据处理
Go语言学习 - RPC篇:gRPC-Gateway定制mux选项
通过上一讲,我们对gRPC的拦截器有了一定的认识,也能定制出很多通用的中间件。 但在大部分的业务系统中,我们面向的还是HTTP协议。那么,今天我们就从gRPC-Gateway的mux选项出发,一起来看看一些很实用的特性。
254 0
|
1月前
|
编译器 Go
go语言编译选项
【10月更文挑战第17天】
42 5
|
存储 设计模式 Go
Go 函数选项模式(Functional Options Pattern)
本文对 Go 函数选项模式(Functional Options Pattern)进行了详细介绍,并通过封装一个消息结构体的例子,展示了如何使用函数选项模式进行代码实现。
174 0
|
设计模式 Kubernetes 监控
Go编程模式 - 8-装饰、管道和访问者模式
装饰、管道和访问者模式的使用频率不高,但在特定场景下会显得很酷
41 0
|
Kubernetes Shell Go
Go编程模式 - 7-代码生成
良好的命名能体现出其价值。尤其是在错误码的处理上,无需再去查询错误码对应的错误内容,直接可以通过命名了解。
68 0
|
SQL 分布式计算 Go
Go编程模式 - 6-映射、归约与过滤
但是,我不建议大家在实际项目中直接使用这一块代码,毕竟其中大量的反射操作是比较耗时的,尤其是在延迟非常敏感的web服务器中。 如果我们多花点时间、直接编写指定类型的代码,那么就能在编译期发现错误,运行时也可以跳过反射的耗时。
58 0
|
Go
Go编程模式 - 4.错误处理
如何Wrap Error,在多人协同开发、多模块开发过程中,很难统一。而一旦不统一,容易出现示例中的过度Unwrap的情况。
37 0
|
Go
Go编程模式 - 3.继承与嵌入
业务逻辑依赖控制逻辑,才能保证在复杂业务逻辑变化场景下,代码更健壮!
54 0
|
JSON 前端开发 Go
Go编程模式 - 2.基础编码下
尽量用`time.Time`和`time.Duration`,如果必须用string,尽量用`time.RFC3339`
31 0