有时候一个函数会有很多参数,为了方便函数的使用,我们会给希望给一些参数设定默认值,调用时只需要传与默认值不同的参数即可,类似于 python 里面的默认参数和字典参数,虽然 golang 里面既没有默认参数也没有字典参数,但是我们有选项模式。

可变长参数列表

在这之前,首先需要介绍一下可变长参数列表,顾名思义,就是参数的个数不固定,可以是一个也可以是多个,最典型的用法就是标准库里面的 fmt.Printf,语法比较简单,如下面例子实现任意多个参数的加法

1
2
3
4
5
6
7
8
9
func add(nums ...int) int {
sum := 0
for _, num := range nums {
sum += num
}
return sum
}
So(add(1, 2), ShouldEqual, 3)
So(add(1, 2, 3), ShouldEqual, 6)

在类型前面加 ... 来表示这个类型的变长参数列表,使用上把参数当成 slice 来用即可

选项模式

假设我们要实现这样一个函数,这个函数接受5个参数,三个 string(其中第一个参数是必填参数),两个 int,这里功能只是简单输出这个参数,于是我们可以简单用如下代码实现

1
2
3
4
5
func MyFunc1(requiredStr string, str1 string, str2 string, int1 int, int2 int) {
fmt.Println(requiredStr, str1, str2, int1, int2)
}
// 调用方法
MyFunc1("requiredStr", "defaultStr1", "defaultStr2", 1, 2)

这种实现比较简单,但是同时传入参数较多,对调用方来说,使用的成本就会比较高,而且每个参数的具体含义这里并不清晰,很容易出错

那选项模式怎么实现这个需求呢?先来看下最终的效果

1
2
3
MyFunc2("requiredStr")
MyFunc2("requiredStr", WithOptionStr1("mystr1"))
MyFunc2("requiredStr", WithOptionStr2AndInt2("mystr2", 22), WithOptionInt1(11))

如上面代码所示,你可以根据自己的需求选择你需要传入的参数,大大简化了函数调用的复杂度,并且每个参数都有了清晰明确的含义

那怎么实现上面的功能呢

定义可选项和默认值

首先定义可选项和默认值,这里有4个可选项,第一个参数为必填项

1
2
3
4
5
6
7
8
9
10
11
12
type MyFuncOptions struct {
optionStr1 string
optionStr2 string
optionInt1 int
optionInt2 int
}
var defaultMyFuncOptions = MyFuncOptions{
optionStr1: "defaultStr1",
optionStr2: "defaultStr2",
optionInt1: 1,
optionInt2: 2,
}

实现 With 方法

这些 With 方法看起来有些古怪,接受一个选项参数,返回一个选项方法,而选项方法以选项作为参数负责修改选项的值,如果没看明白没关系,可以先看函数功能如何实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type MyFuncOption func(options *MyFuncOptions)
func WithOptionStr1(str1 string) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionStr1 = str1
}
}
func WithOptionInt1(int1 int) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionInt1 = int1
}
}
func WithOptionStr2AndInt2(str2 string, int2 int) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionStr2 = str2
options.optionInt2 = int2
}
}

这里我们让 optionStr2 和 optionInt2 合并一起设置,实际应用场景中可以用这种方式将相关的参数放到一起设置

实现函数功能

1
2
3
4
5
6
7
func MyFunc2(requiredStr string, opts ...MyFuncOption) {
options := defaultMyFuncOptions
for _, o := range opts {
o(&options)
}
fmt.Println(requiredStr, options.optionStr1, options.optionStr2, options.optionInt1, options.optionInt2)
}

使用 With 方法返回的选项方法作为参数列表,用这些方法去设置选项。

转载自:golang 设计模式之选项模式


将代码直接拿出运行即可知道其中奥妙,这里面合理运行了Go的可变长参数特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
)

type MyFuncOptions struct {
optionStr1 string
optionStr2 string
optionInt1 int
optionInt2 int
}
var defaultMyFuncOptions = MyFuncOptions{
optionStr1: "defaultStr1",
optionStr2: "defaultStr2",
optionInt1: 1,
optionInt2: 2,
}

type MyFuncOption func(options *MyFuncOptions)
func WithOptionStr1(str1 string) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionStr1 = str1
}
}
func WithOptionInt1(int1 int) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionInt1 = int1
}
}
func WithOptionStr2AndInt2(str2 string, int2 int) MyFuncOption {
return func(options *MyFuncOptions) {
options.optionStr2 = str2
options.optionInt2 = int2
}
}

func MyFunc2(requiredStr string, opts ...MyFuncOption) {
options := defaultMyFuncOptions
for _, o := range opts {
o(&options)
}
fmt.Println(requiredStr, options.optionStr1, options.optionStr2, options.optionInt1, options.optionInt2)
}

func main() {

//grpc.Dial("xxx", grpc.WithInsecure(), grpc.WithTimeout(time.Duration(10*time.Second)))


MyFunc2("requiredStr")
MyFunc2("requiredStr", WithOptionStr1("mystr1"))
MyFunc2("requiredStr", WithOptionStr2AndInt2("mystr2", 22), WithOptionInt1(11))
}

GRPC

1
2
grpc.Dial(addr, grpc.WithInsecure())
grpc.Dial(addr, grpc.WithInsecure(), grpc.WithTimeout(time.Duration(10*time.Second)))

grpc中,这样的设计在google.golang.org/grpc/clientconn.goDial(target string, opts ...DialOption),其中DialOption各函数在google.golang.org/grpc/dialoptions.go中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
cc := &ClientConn{
target: target,
csMgr: &connectivityStateManager{},
conns: make(map[*addrConn]struct{}),
dopts: defaultDialOptions(),
blockingpicker: newPickerWrapper(),
czData: new(channelzData),
firstResolveEvent: grpcsync.NewEvent(),
}
...

// 调用传入的设置函数 拿出来一个一个的设置
for _, opt := range opts {
opt.apply(&cc.dopts)
}

...
}

而这些函数设计思路和上面讲述是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
func WithDisableRetry() DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.disableRetry = true
})
}

...

func WithTimeout(d time.Duration) DialOption {
return newFuncDialOption(func(o *dialOptions) {
o.timeout = d
})
}

此外,当我们需要加入的新的参数时,我们只需要加入新函数选项,以前的调用则给定一个默认值。

函数式选择模式比较常见的用法即是在GRPC上所用,在其他开源基本也是如此用法。