Functional Options Pattern:
- 定义一个Options结构体(
StuffClientOptions
),包含所有的可选项;- 定义一个函数类型,参数是Options结构指针(
StuffClientOption
);- 创建一个Options类型的变量,包含所有的默认值(
defaultStuffClientOptions
);- 对所有的可选项定义设置函数(
WithXXX
),在里面对选项进行设置;- 定义一个接口,指定我们提供的服务(
StuffClient
);- 创建一个包内可见的变量,实现接口(
stuffClient
);- 定义一个构造函数,使用可变参数(
NewStuffClient
)。
Go语言让程序员困扰的一个问题就是如何编写一个可选参数的函数。当一个类具有多个选项,我们在构造这个类的时候可能希望一些选项使用默认值,而有时候还需要进行更精细的控制。
这个在很多语言中都很容易。比如在C类语言中,我们可以通过函数重载的方式,提供具有不同个数参数的同名字函数;在PHP中可以对参数设置一个默认值。但是在Go中这些方法都行不通。
这个时候就可以用功能选项模式(Functional Options Pattern)来完成。这里通过一个简单的例子逐步介绍这个模式。
假如我们提供一个服务叫做StuffClient
,这个服务有两个可选项(超时timeout和重试次数retries):
type StuffClient interface {
DoStuff() error
}
type stuffClient struct {
conn Connection
timeout int
retires int
}
这个结构体是私有的,所以我们需要一个构造函数:
func NewStuffClient(conn Connection, timeout, retries int) StuffClient {
return &stuffClient {
conn: conn,
timeout: timeout,
retries: retries,
}
}
但是这样每次构造一个新的对象都需要传递全部参数,但是大多数时候我们使用默认值就可以了。我们同样不能定义多个具有相同名字但不同参数的函数,然后回报redelared
错误。
这时我们可以定义一个不同的构造函数:
func NewStuffClient(conn Connection) StuffClient {
return &stuffClient {
conn: conn,
timeout: DEFAULT_TIMEOUT,
retries: DEFAULT_RETRIES,
}
}
func NewStuffClientWithOptions(conn Connection, timeout, retries int) StuffClient {
return &stuffClient {
conn: conn,
timeout: timeout,
retries: retries,
}
}
但是这样太麻烦了,尤其当我们需要再添加一个选项的时候,需要改动的地方太多了。
我们可以传递一个配置对象config:
type StuffClientOptions struct {
Retries int // number of times to retry the request before giving up
Timeout int // connection timeout in seconds
}
func NewStuffClient(conn Connection, options StuffClientOptions) StuffClient {
return &stuffClient {
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
但是这样也有点麻烦,我们需要每次都要构造一个config然后传进去,即使我们不想指定某些值。
最后我们的功能选项模式出场了。通过Go的闭包,可以更加简洁地实现上面的需求:
type StuffClientOption func(*StuffClientOptions)
type StuffClientOptions struct {
Retries int // number of times to retry the request before giving up
Timeout int // connection timeout in seconds
}
func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
首先,我们为我们的结构StuffClient
定义了一个可选项的结构StuffClientOptions
,又定义了一个函数类型StuffClientOption
并以上面的Option结构作为参数。然后还有几个WithXXX
函数返回一个闭包。现在,我们可以这样:
var defaultStuffClientOptions = StuffClientOptions {
Retries: 3,
Timeout: 2,
}
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient {
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
这里我们调整了构造函数的定义,接收一个可变参数,这个可变参数是上面函数类型的数组,然后将所有的函数执行一遍,就对我们的选项进行了设置,没有设置的使用默认值。
完整的代码:
package main
import (
"fmt"
)
// 1、定义一个Option结构体,包含所有需要的可选项
type StuffClientOptions struct {
Retries int // # of times to retry the request before giving up
Timeout int // connection timeout in seconds
}
// 2、定义一个函数类型,这个函数的参数是Option结构体的指针类型
type StuffClientOption func(*StuffClientOptions)
// 3、创建一个Option类型的变量,包括所有的默认值
var defaultStuffClientOptions = StuffClientOptions {
Retries: 3,
Timeout: 2,
}
// 4、WithXXX函数,参数是选项的值,返回2中定义的函数,内部进行设置
func WithRetries(r int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Retries = r
}
}
func WithTimeout(t int) StuffClientOption {
return func(o *StuffClientOptions) {
o.Timeout = t
}
}
// 5、定义接口
type StuffClient interface {
DoStuff() error
}
type Connection struct{}
// 6、包内可见的一个变量
type stuffClient struct {
conn Connection
timeout int
retries int
}
// 构造函数
func NewStuffClient(conn Connection, opts ...StuffClientOption) StuffClient {
options := defaultStuffClientOptions
for _, o := range opts {
o(&options)
}
return &stuffClient {
conn: conn,
timeout: options.Timeout,
retries: options.Retries,
}
}
func (c stuffClient) DoStuff() error {
return nil
}
func main() {
x := NewStuffClient(Connection{})
fmt.Printf("%+v\n", x) // print &{conn:{} timeout:2 retries:3}
x = NewStuffClient(
Connection{},
WithRetries(1),
)
fmt.Printf("%+v\n", x) // print &{conn:{} timeout:2 retries:1}
x = NewStuffClient(
Connection{},
WithRetries(1),
WithTimeout(100),
)
fmt.Printf("%+v\n", x) // print &{conn:{} timeout:100 retries:1}
}
更简单一点,我们可以不定义StuffClientOptions
,直接使用我们的StuffClient
:
var defaultStuffClient = struffClient {
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
}
这里没有使用config结构,但是在一些场景中使用config结构更加合适,比如在构造函数中对config有进一步的操作但是并不保存数据,或者把这个config传递到其他地方的时候,使用config结构更加方便。