发现
最近在使用 grpc 的时候,对于 client/server 初始化的配置,是通过传入函数进行对配置的处理的。
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
通过传入 grpc.WithInsecure()
,grpc.WithBlock()
两个方法,启用了 grpc 的 tls 和 block,感觉对配置的可选自定义化非常方便。
继续看这个是怎么实现的。
func Dial(target string, opts ...DialOption) (*ClientConn, error)
这是 client 端新建 conn 的方法,有两个参数target
目标地址、opts
是一个DialOption
数组
type DialOption interface {
apply(*dialOptions)
}
DialOption
接口有一个 apply 方法。 grpc.WithInsecure()
, grpc.WithBlock()
都实现了这个接口。
dialOptions
中则是拨号的所有配置, apply
方法就是提供来方便自定义修改参数的。
func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
//省略代码
cc.safeConfigSelector.UpdateConfigSelector(&defaultConfigSelector{nil})
cc.ctx, cc.cancel = context.WithCancel(context.Background())
for _, opt := range opts {
opt.apply(&cc.dopts)
}
//省略代码
}
沿着 Dial
方法继续看,可以定位到如上。所有传入的 opts 都会按传入的顺序先后调用一次。
模式
查阅资料,这是一种设计模式。
Functional Options Pattern(函数式选项模式),提供了将一个函数的参数设置为可选的功能,也就是说我们可以选择参数中的某几个,并且可以按任意顺序传入参数。
应用场景
- 一个函数需要不同的参数;而众所周知,go 目前不支持函数重载,所以就可以使用这种可变长参数的形式来模拟函数重载这种情况。
- 一个复杂的结构体,在业务中可能会频繁的添加字段,如果把
NewXXX()
等方法写死,那么后续修改起来非常麻烦。此时使用选项模式,对其初始化,新增一个字段,就新增对应的一个函数来处理字段的初始化,应用程序的扩展性大大的提高了。 - 可读性好,给代码的维护带来了便利。
举个例子
var defaultStuffClient = stuffClient{
retries: 3,
timeout: 2,
}
type StuffClientOption func(*stuffClient)
func WithRetries(r int) StuffClientOption {
return func(o *stuffClient) {
o.retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *stuffClient) {
o.timeout = t
}
}
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retries int
}
type Connection struct{}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
client := defaultStuffClient
for _, o := range opts {
o(&client)
}
client.conn = conn
return client
}
func (c stuffClient) DoStuff() error {
return nil
}
以上实现了一个简单函数式可选模式,对 StuffClient 设定了默认值,并提供了 StuffClientOption
可以用来进行自定义更改。
上一篇: 多渠道账号的表设计想法
下一篇: 构建 CICD 遇到的 Docker in Docker 问题