寫在前面的話
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