写在前面的话

 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. c++对c的拓展_指针的引用

    套用引用公式:Type & ref =val; 假设:type 类型为int * 由公式得 int * & ref = val; // int * *const ref=&va ...

  2. Prometheus介绍及docker安装方式

    一.介绍 Prometheus是主要基于Go编写,最初在SoundCloud上构建的开源系统监视和警报工具包,它启发于 Google 的 borgmon 监控系统,由工作在 SoundCloud 的 ...

  3. Machine Learning 学习笔记 01 Typora、配置OSS、导论

    Typora 安装与使用. Typora插件. OSS图床配置. 机器学习导论. 机器学习的基本思路. 机器学习实操的7个步骤

  4. 使用Harr特征的级联分类器实现目标检测

    前言  最近在学习人脸的目标检测任务时,用了Haar人脸检测算法,这个算法实现起来太简洁了,读入个.xml,调用函数就能用.但是深入了解我发现这个算法原理很复杂,也很优秀.究其根源,于是我找了好些篇相 ...

  5. JavaScript学习高级1

    Doucment(Dom)文档对象,用户控制html文档中的元素,   <span id="span" onclick="fun();">1111& ...

  6. Java语言学习day11--7月10日

    今日内容介绍1.自定义类型的定义及使用2.自定义类的内存图3.ArrayList集合的基本功能4.随机点名器案例及库存案例代码优化 ###01引用数据类型_类 * A: 数据类型 * a: java中 ...

  7. 安全开发运维必备,如何进行Nginx代理Web服务器性能优化与安全加固配置,看这篇指南就够了

    本章目录 1.引言 1.1 目的 1.2 目标范围 1.3 读者对象 2.参考说明 2.1 帮助参考 2.2 参数说明 3.3 模块说明 3.服务优化 3.1 系统内核 3.2 编译优化 3.3 性能 ...

  8. Java 字符串Split方法的一个坑

    java字符串的split,只传一个参数,后面空白的字符串会被忽略: public static void main(String[] args) { String str = "ab|c| ...

  9. 不care工具,在大数据平台中Hive能自动处理SQL

    摘要:有没有更简单的办法,可以直接将SQL运行在大数据平台? 本文分享自华为云社区<Hive执行原理>,作者: JavaEdge . MapReduce简化了大数据编程的难度,使得大数据计 ...

  10. MySQL基础合集

     我的小站 1.MySQL的优势 运行速度快 使用成本低 可移植性强 适用用户广 2.MySQL的运行机制 一个SQL语句,如select * from tablename ,从支持接口进来后,进入连 ...