【Golang】创建有配置参数的结构体时,可选参数应该怎么传?

写在前面的话

 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) 	}         // ... } 

完。

祝玩的开心~

 

参考:

functional-options的作者Dave Cheney

https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

发表评论

相关文章