写在前面的话

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

完。

祝玩的开心~

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

  1. 【C++】结构体/结构体数组/结构体指针/结构体嵌套/函数参数/const

    一.结构体声明 struct Student { //成员列表 string name; int age; int score; }; //s3;定义时直接声明 int main() { struct ...

  2. go语言基础之结构体做函数参数 值传递和地址传递

    1.结构体做函数参数值传递 示例: package main //必须有个main包 import "fmt" //定义一个结构体类型 type Student struct { ...

  3. go语言结构体作为函数参数,采用的是值传递

    经过验证,go语言结构体作为函数参数,采用的是值传递.所以对于大型结构体传参,考虑到值传递的性能损耗,最好能采用指针传递. 验证代码: package main import ( "fmt& ...

  4. 深入理解C语言-结构体做函数参数

    结构体做函数参数,在C语言中属于常见现象,此时为了内存考虑,不传递结构体,而是传递结构体的地址 结构体定义 struct Man { char name[64]; int age; }; 结构体可以与 ...

  5. 使用类/结构体时关于ZeroMomery用法错误

    今天同事在写了如下结构体: typedef struct _tagInfo { std::list<int> lst; std::vector<int> nVec; } INF ...

  6. 用set、map等存储自定义结构体时容器内部判别各元素是否相同的注意事项

    STL作为通用模板极大地方便了C++使用者的编程,因为它可以存储任意数据类型的元素 如果我们想用set与map来存储自定义结构体时,如下 struct pp { double xx; double y ...

  7. C#中的参数和调用方式(可选参数、具名参数、可空参数)

    具名参数 和 可选参数 是 C# framework 4.0 出来的新特性. 一. 常规方法定义及调用 public void Demo1(string x, int y) { //do someth ...

  8. golang中结构体当做函数参数或函数返回值都会被拷贝

    1. 结构体做函数的参数或返回值时,都会被重新拷贝一份如果不想拷贝,可以传递结构体指针 package main import "fmt" type Person struct { ...

  9. WPF中ItemsControl绑定到Google ProtocolBuffer的结构体时的性能问题

    背景: 最近遇到一个DataGrid的性能问题:里面大概有4000个数据, 绑定的ItemSource的类也只有一层数据,即简单的List(里面每个是Protocol Buffer自动产生的一个类,1 ...

随机推荐

  1. Python-初见

    目录 概述 关键字 标准数据类型 Number String List Tuple Set Dictionary 删除对象 数据类型转换 推导式 运算符 迭代器与生成器 迭代器 生成器 函数 参数传递 ...

  2. 4.Docker容器学习之Dockerfile入门到放弃

    原文地址: 点击直达 0x01 Dockerfile 编写 描述:Dockerfile是一个文本格式的配置文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内 ...

  3. 约定编程与Sping AOP

    一.约定编程 Spring AOP是一种约定流程的编程,咱们可以先通过动态代理模式的实现来理解Spring AOP的概念. 代理的逻辑很简单,例如,当你需要采访一名儿童时,首先需要经过他父母的同意,在 ...

  4. Casdoor + OAuth 实现单点登录 SSO

    简介 Casdoor 是一个基于 OAuth 2.0 / OIDC 的中心化的单点登录(SSO)身份验证平台,简单来说,就是 Casdoor 可以帮你解决用户管理的难题,你无需开发用户登录.注册等与用 ...

  5. 『现学现忘』Git基础 — 7、设置Git Bash终端默认路径

    目录 1.Git Bash默认路径 2.如何查看Git Bash终端默认路径 3.如何修改Git Bash终端的默认路径 4.拓展:指定目录进入Git Bash终端 5.注意事项 如果您不熟悉Git命 ...

  6. 2021.12.08 P1848 [USACO12OPEN]Bookshelf G(线段树优化DP)

    2021.12.08 P1848 [USACO12OPEN]Bookshelf G(线段树优化DP) https://www.luogu.com.cn/problem/P1848 题意: 当农夫约翰闲 ...

  7. 2021.11.02 eleveni的水省选题的记录

    2021.11.02 eleveni的水省选题的记录 因为eleveni比较菜,所以eleveni决定从绿题开始水 --实际上菜菜的eleveni连绿题都不一定能水过/忍不住哭了 [P2217 HAO ...

  8. MySQL基础之写表(创建表)

    我的博客 工具 市面上的SQL可视化工具不少,我一般常用的主要就是这两个. 当然,IDEA也是集成了数据库可视化功能的.除了这些,还有DBeaver.SQLyog等等. 我比较喜欢DataGrip,我 ...

  9. 聊聊buffer和cache的区别以及是什么?

    buffer 众所周知,想把数据写入磁盘,肯定要先把数据文件读到内存中,当修改完这个文件时,不会立即写入磁盘,为了减少磁盘IO,提高性能,所有会留存一段时间再写入磁盘,这就是buffer cache ...

  10. git 配置别名简化命令行和删除别名

    废话不多说直接上添加别名语法 加上--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用. git config --global alias.<自己想要的命令行> & ...