【Golang】建立有設定引數的結構體時,可選引數應該怎麼傳?

2022-06-10 15:01:28

寫在前面的話

 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