Golang中构建结构体的时候,需要通过可选参数方式创建,我们怎么样设计一个灵活的API来初始化结构体呢。
让我们通过如下的代码片段,一步一步说明基于可选参数模式的灵活 API 怎么设计。
灵活 API 创建结构体说明v1版本
如下 Client 是一个 客户端的sdk结构体,有 host和 port 两个参数,我们一般的用法如下:
package client type Client struct {
host string
port int
} // NewClient 通过传递参数
func NewClient(host string, port int) *Client {
return &Client{
host: host,
port: port,
}
} func (c *Client) Call() error {
// todo ...
return nil
}
我们可以看到通过host和 port 两个参数可以创建一个 client 的 sdk。
调用的代码一般如下所示:
package main import (
"client"
"log"
) func main() {
cli := client.NewClient("localhost", 1122)
if err := cli.Call(); err != nil {
log.Fatal(err)
}
}
突然有一天,sdk 做了升级,增加了新的几个参数,如timeout超时时间,maxConn最大连接数, retry重试次数...
v2版本
sdk中的Client定义和创建结构体的 API变成如下:
package client import "time" type Client struct {
host string
port int
timeout time.Duration
maxConn int
retry int
} // NewClient 通过传递参数
func NewClient(host string, port int) *Client {
return &Client{
host: host,
port: port,
timeout: time.Second,
maxConn: 1,
retry: 0,
}
} // NewClient 通过3个参数创建
func NewClientWithTimeout(host string, port int, timeout time.Duration) *Client {
return &Client{
host: host,
port: port,
timeout: timeout,
maxConn: 1,
retry: 0,
}
} // NewClient 通过4个参数创建
func NewClientWithTimeoutAndMaxConn(host string, port int, timeout time.Duration, maxConn int) *Client {
return &Client{
host: host,
port: port,
timeout: timeout,
maxConn: maxConn,
retry: 0,
}
} // NewClient 通过5个参数创建
func NewClientWithTimeoutAndMaxConnAndRetry(host string, port int, timeout time.Duration, maxConn int, retry int) *Client {
return &Client{
host: host,
port: port,
timeout: timeout,
maxConn: maxConn,
retry: retry,
}
} func (c *Client) Call() error {
// todo ...
return nil
}
通过如上的创建 API 我们发现创建 Client 一下子多了 NewClientWithTimeout/NewClientWithTimeoutAndMaxConn/NewClientWithTimeoutAndMaxConnAndRetry...
我们可以看到通过host和 port 等其他参数可以创建一个 client 的 sdk。
调用的代码一般如下所示:
package main import (
"client"
"log"
"time"
) func main() {
cli := client.NewClientWithTimeoutAndMaxConnAndRetry("localhost", 1122, time.Second, 1, 0)
if err := cli.Call(); err != nil {
log.Fatal(err)
}
}
这个时候,我们发现 v2版本的 API 定义很不友好,参数组合的数量也特别多.
v3版本
我们需要把参数重构一下,是否可以把配置参数合并到一个结构体呢?
好,我们就把参数统一放到 Config 中,Client 中定义一个 cfg 成员
package client import "time" type Client struct {
cfg Config
} type Config struct {
Host string
Port int
Timeout time.Duration
MaxConn int
Retry int
} func NewClient(cfg Config) *Client {
return &Client{
cfg: cfg,
}
} func (c *Client) Call() error {
// todo ...
return nil
}
我们可以看到通过定义好的 Config参数可以创建一个 client 的 sdk。
调用的代码一般如下所示:
package main import (
"client"
"log"
"time"
) func main() {
cli := client.NewClient(client.Config{
Host: "localhost",
Port: 1122,
Timeout: time.Second,
MaxConn: 1,
Retry: 0})
if err := cli.Call(); err != nil {
log.Fatal(err)
}
}
这里我们发现新的问题出现了,Config 配置的成员都需要以大写开头,对外公开才可以使用,但做为一个 sdk,我们一般不建议对外导出这些成员。
我们该怎么办?
v4版本
我们回归到最初的定义,Client还是那个 Client,有很多配置成员变量,我们通过可选参数模式对 sdk 进行重构。
重构后的代码如下
package client import "time" type Client struct {
host string
port int
timeout time.Duration
maxConn int
retry int
} // 通过可选参数创建
func NewClient(opts ...func(client *Client)) *Client {
// 创建一个空的Client
cli := &Client{}
// 逐个调用入参的可选参数函数,把每一个函数配置的参数复制到cli中
for _, opt := range opts {
opt(cli)
}
return cli
} // 把 host参数,传给函数参数 c *Client
func WithHost(host string) func(*Client) {
return func(c *Client) {
c.host = host
}
} func WithPort(port int) func(*Client) {
return func(c *Client) {
c.port = port
}
} func WithTimeout(timeout time.Duration) func(*Client) {
return func(c *Client) {
c.timeout = timeout
}
} func WithMaxConn(maxConn int) func(*Client) {
return func(c *Client) {
c.maxConn = maxConn
}
} func WithRetry(retry int) func(*Client) {
return func(c *Client) {
c.retry = retry
}
} func (c *Client) Call() error {
// todo ...
return nil
}
我们可以通过自由选择参数,创建一个 client 的 sdk。
调用的代码一般如下所示:
package main import (
"client"
"log"
"time"
) func main() {
cli := client.NewClient(
client.WithHost("localhost"),
client.WithPort(1122),
client.WithMaxConn(1),
client.WithTimeout(time.Second))
if err := cli.Call(); err != nil {
log.Fatal(err)
}
}
通过调用的代码可以看到,我们的 sdk 定义变的灵活和优美了。
开源最佳实践
最后我们看看按照这种方式的最佳实践项目。
gRpc
grpc.Dial(endpoint, opts...) // Dial creates a client connection to the given target.
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)
}
// ...
}
完。
祝玩的开心~