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


    写在前面的话

     Golang中构建结构体的时候,需要通过可选参数方式创建,我们怎么样设计一个灵活的API来初始化结构体呢。

    让我们通过如下的代码片段,一步一步说明基于可选参数模式的灵活 API 怎么设计。

     

    灵活 API 创建结构体说明

    v1版本

    如下 Client 是一个 客户端的sdk结构体,有 host和 port 两个参数,我们一般的用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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 ...<br>        return nil
    }

    我们可以看到通过host和 port 两个参数可以创建一个 client 的 sdk。

    调用的代码一般如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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变成如下:

    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
    55
    56
    57
    58
    59
    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 ...<br>        return nil
    }

    通过如上的创建 API 我们发现创建 Client 一下子多了 NewClientWithTimeout/NewClientWithTimeoutAndMaxConn/NewClientWithTimeoutAndMaxConnAndRetry...

    我们可以看到通过host和 port 等其他参数可以创建一个 client 的 sdk。

    调用的代码一般如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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 成员

    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
    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。

    调用的代码一般如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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 进行重构。

    重构后的代码如下

    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
    55
    56
    57
    58
    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。

    调用的代码一般如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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

  • 相关阅读:
    【C++入门】文件流(fstream)介绍和使用
    html学习笔记
    零零信安-D&D数据泄露报警日报【第43期】
    Ubuntu20.04安装pycharm
    编程笔记 html5&css&js 084 JavaScript 变量的作用域
    C++ 中的模板函数简介
    计算机毕业设计(附源码)python游戏推荐系统
    centos上部署Ollama平台,实现语言大模型本地部署
    Day76-Spring Boot实践,开发社区登录模块-生成二维码
    【毕业设计】基于单片机红外热成像仪 - stm32 物联网 嵌入式
  • 原文地址:https://www.cnblogs.com/voipman/p/16362779.html