2

如何优雅地指定配置项

 3 years ago
source link: https://qcrao.com/2021/07/15/how-to-use-functional-options-pattern/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

如何优雅地指定配置项

最近一个年久失修的库导致了线上事故,不得不去做一些改进。

这个陈年库的作用是调用第三方的 RPC 拿一些比较重要的配置,业务代码中有段逻辑会根据读到的配置调用不同端的下游。如果没拿到配置,就会默认地调一个兜底下游。恰好这个兜底下游最近新上了一些逻辑,不兼容这种跨端调用,直接把它打挂了。

先抛开这个下游不健壮不谈,假设它是健壮的。

陈年库的问题在于:进程启动时它会去调一个下游拿数据,之后会定时更新。但如果启动时调用失败就直接 panic 了,所以之后也不会定时更新。理论上这个也没什么问题,服务在初始化时如果检测到了库的 panic,进程退出,重启就好了。

但是阻塞启动是比较危险的,所以有些服务就会吞掉 panic。于是,整个进程生命周期内这个配置就一直是缺失的状态。

因为阻塞服务的启动风险太高,所以当前的状态是把 panic recover 住了,但是之后这个配置也就一直没有更新的机会了。而陈年库其实是可以在后台静默更新数据的。

因此我要对陈年库要做一点改进:如果初始化时拉取配置失败,不 panic,后台静默修复。这个设置要在调用 Init 函数时设置,因为库就暴露了 Init 和 Get 函数。

但因为这个库有很多使用方,所以不可能更改函数签名和现在的行为,否则影响其他人使用。万一有业务都对这个是强依赖,就是要感知 panic,初始化失败就进程退出,你改了不就 gg 了。

我们知道,Go 语言里面有可变参数,调用它的时候可以不传实参,或者传多个实参。向陈年库函数的 Init 函数签名后加一个可变参数:

func Init(a int)
func Init(a int, opts ...optionFunc)

这样就不影响已有的用户了,并且我可以增加更多的设置项。这里的关键是 optionFunc 的实现原理是什么?

它其实是一个函数类型,它接受 options 结构体指针:

type optionFunc func(*options)

再定义一个 options 结构体用于放 bool 型变量 PanicWhenInitFail,表示 Init 失败后是否 panic:

type options struct {
PanicWhenInitFail bool
}

再来定义一个导出的函数,用户传入 bool 型变量就可以设置 options,而不用定义 options 对象。这种方法美妙的地方就在这里,要多次回味才能感受到:

func WithPanicWhenInitFail() optionFunc {
return func(o *options) {
o.PanicWhenInitFail = true
}
}

初始时,Init 函数的实现如下:

func Init(a int) {
fmt.Println(a)
}
func Init(a int, opts ...optionFunc) {
fmt.Println(a)

var gOpt = &options{PanicWhenInitFail: false}

for _, opt := range opts {
opt(gOpt)
}

fmt.Println(gOpt)

}

这样,main 函数就可以非常优雅地设置 PanicWhenInitFail 了:

func main() {
Init(8)
Init(8, WithPanicWhenInitFail())
}

不管加不加后面的配置,两种调用方式都可以编译成功,不会影响现有的用户,完美。

为什么这篇文章和曹大扯上关系,因为在曹大写的 mosn/homels 这个库里也有类似的代码。当然,本文这种形式很常见,可以算作标配了。不过,有一点点不同之处,曹大定义了一个 interface,不过看起来感觉有点更难懂了。😇

// Option holmes option type.
type Option interface {
apply(*options) error
}

type optionFunc func(*options) error

func (f optionFunc) apply(opts *options) error {
return f(opts)
}

去 Google 上一查,其实这种形式,叫 Functional Options Pattern,早在 2014 年 Rob Pike 就写过一篇博文来说这个事,没几行代码,但是真的很优雅。

总结一下,当我们要修改已有的函数时,为了不破坏原有的签名和行为,可以使用 Functional Options Pattern 的形式增加可变参数,即可以增加设置项,又能兼容已有的代码。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK