Go语言必知必会100问题-11 使用选项模式

news/2024/7/5 2:52:37
使用选项模式

在设计API时,可能会遇到一个问题:如何处理可选配置?有效的解决可选配置问题可以提高API的灵活性。本文通过一个具体示例说明处理可选配置的一些方法。该示例的要求是设计一个对外提供创建HTTP服务器的库函数。函数定义如下:

func NewServer(addr string, port int) (*http.Server, error) {
    // ...
}

假设上面的库函数已有人在愉快的使用。但是,在某些时候,有用户开始抱怨这个函数只提供addr和port初始化,缺少其他参数初始化(像写入超时设置和连接上下文等)。如果提供其他参数的初始化,需要修改NewServer函数,破坏了兼容性,迫使调用方也必须修改代码。与此同时,我们希望程序能够更加灵活,实现如下的逻辑。

  • 如果未设置端口,则使用默认端口

  • 如果端口为负,则返回错误

  • 如果端口为0,则使用随机端口

  • 否则,使用客户端提供的端口

在这里插入图片描述

怎么优雅的实现上述要求呢?下面来看看一些处理方法。

配置结构体

由于Go语言不支持函数可选参数,所以一种可能的方法是使用配置结构体来表达哪些是强制性参数,哪些是可选参数。例如,强制参数可以作为函数参数存在,而可选参数可以在Config结构体中处理。

type Config struct {
    Port        int
}
func NewServer(addr string, cfg Config) {
}

通过结构体方式修复了新增参数兼容性的问题,如果以后要添加新的参数,在Config结构体中定义即可。但是,这种方法没有解决上面端口设置策略逻辑。同时,我们应该注意如果没有初始化Config结构体字段,它会被初始为对应的零值。

  • 整数的零值为0

  • 浮点数的零值为0.0

  • 字符串的零值为“”

  • 切片、map、通道、指针、接口和函数的零值为nil

因此,在下面的示例中,结构体c1和c2是等价的。

c1 := httplib.Config{
    Port: 0,
}
c2 := httplib.Config{

}

为了实现端口设置逻辑策略,我们需要找到一种方法来区分是用户特意设置端口为0还是没有设置端口(默认为0)。一种可能的解决方法是将Config结构体中的参数设置为对应类型的指针。使用*int,可以区分出值为0和没有设置值(零指针为nil)之间的差异。

type Config struct {
    Port        *int
}

虽然将Config结构体中的参数设置为指针有效,但是也有几个缺点:

第一个是客户端需要提供整数指针不方便,需要先创建一个整数变量,然后取整数变量的地址赋值给Config,像下面这样。只赋值一个字段问题不大,但是整个Config有很多字段,使用起来就不方便了。此外,添加的选项越多,代码就越复杂。

port := 0
config := httplib.Config{
    Port: &port,
}

第二个是在使用这个库的时候,如果采用默认的配置,客户端需要传递一个空结构对象,代码如下。这行代码看起来不直观友好,使用者不一定了解传空有特定的含义在里面。

httplib.NewServer("localhost", httplib.Config{})
创建者模式

在GoF设计模式书中,有一种模式叫创建者模式,该模式描述的是各种对象如何创建的问题。其核心理念是将对象的创建和对象本身分开,对于上述的Config结构体,需要有一个额外的结构ConfigBuilder,负责接收配置并创建Config对象。下面来看一个具体实现的例子,看看它是如何优雅实现我们所有需求的。

type Config struct {
    Port int
}
type ConfigBuilder struct {
    port *int
}
func (b *ConfigBuilder) Port(
    port int) *ConfigBuilder {
    b.port = &port
    return b
}
func (b *ConfigBuilder) Build() (Config, error) {
    cfg := Config{}
    if b.port == nil {
        cfg.Port = defaultHTTPPort
    } else {
        if *b.port == 0 {
            cfg.Port = randomPort()
        } else if *b.port < 0 {
            return Config{}, errors.New("port should be positive")
        } else {
            cfg.Port = *b.port
        }
    }
    return cfg, nil
}
func NewServer(addr string, config Config) (*http.Server, error) {
    // ...
}

ConfigBuilder结构体包含客户端配置项,并对外暴露一个Port方法用来设置端口值。通常,ConfigBuilder的配置方法会返回它本身,像上面的Port方法第一个返回值是*ConfigBuilder类型,以便可以使用方法链式调用连续设置配置项(像builder.Foo(“foo”).Bar(“bar”))。此外,ConfigBuilder还对外提供了一个Build方法,该方法会处理端口设置策略相关逻辑,并返回一个Config对象。

NOTE:建造者模式并不是只有一种实现方法。例如,有些人可能这样一种方法,即将定义端口值的逻辑放在Port方法里面而不是Build内部。本文的重点是介绍可以通过建造者模式创建对象,而不是枚举分析每种可能的建造者实现方法。

然后,客户端可以通过下面的代码来实现server的初始化(假设上面的实现放在httplib包中)。首先,客户端创建一个ConfigBuilder对象,用它来设置一个可选字段(像本文的端口)。然后,调用它的Build方法并检查错误信息,如果正确无误,则将配置传给NewServer创建一个server对象。

builder := httplib.ConfigBuilder{}
builder.Port(8080)
cfg, err := builder.Build()
if err != nil {
    return err
}
server, err := httplib.NewServer("localhost", cfg)
if err != nil {
    return err
}

采用上述实现方法使得端口管理更方便,不需传递整数指针,因为Port方法接收整数参数。但是,如果客户想要使用默认配置,仍然需要传一个空的配置结构体。

builder := httplib.ConfigBuilder{}
cfg, err := builder.Build()
if err != nil {
    return err
}
server, err := httplib.NewServer("localhost", cfg)

为什么将端口的异常值校验放在Build方法中而不是Port中,是因为我们想保持链式调用能力,函数就不能返回错误。如果客户端可以传递多个选项,但想精确处理端口无效的情况,会使错误处理更加复杂。这种情况下,更好的处理方法是采用下面的选项模式。

选项模式

选项模式是解决本文问题的第三种方法,尽管实现起来有细微的差别,但主要思想如下:

  • 有一个未导出的结构体,它包含各配置项:options结构体

  • 每个配置项都是返回一个相同类型的函数:type Option func(options *options) error. 例如,WithPort接收一个表示端口的int参数,并返回一个表示如何更新 options 结构体的Option函数。

在这里插入图片描述

下面是采用选项模式解决本文的问题,代码如下. WithPort返回的是一个闭包函数,并且是匿名的, 它引用函数体外的变量port. 该闭包函数是Option类型,并且实现了端口验证逻辑。options中的每个字段都需要创建一个类似于WithPort对外可导出函数,验证输入参数并更新options结构体中对应的字段值。

type options struct {
    port *int
}
 
type Option func(options *options) error
 
func WithPort(port int) Option {
    return func(options *options) error {
        if port < 0 {
            return errors.New("port should be positive")
        }
        options.port = &port
        return nil
    }
}

采用选项模式时,NewServer实现代码如下。将选项字段作为可变参数传递,因此需要遍历所有选项字段来设置配置结构体值。

func NewServer(addr string, opts ...Option) (
    *http.Server, error) {
    var options options
    for _, opt := range opts {
        err := opt(&options)
        if err != nil {
            return nil, err
        }
    }
 
    // At this stage, the options struct is built and contains the config
    // Therefore, we can implement our logic related to port configuration
    var port int
    if options.port == nil {
        port = defaultHTTPPort
    } else {
        if *options.port == 0 {
            port = randomPort()
        } else {
            port = *options.port
        }
    }
 
    // ...
}

NewServer 内部首先创建一个空的 options结构体,然后,遍历每个可变参数opts, 执行它们更改option结构中的字段值,最后实现端口策略逻辑。 因为 NewServer 第二个参数是可变参数,所以调用方可以传递任意个参数,例如,下面传递端口和超时时间两个参数。

server, err := httplib.NewServer("localhost",
        httplib.WithPort(8080),
        httplib.WithTimeout(time.Second))

假如客户端需要默认配置,调用时就不用提供参数,调用代码如下。

server, err := httplib.NewServer("localhost")

本文讲述三种处理配置值的方法,虽然建造者模式相比配置结构体更好,但它有一些小缺点,使得选项模式成为Go语言中的惯用方法,它提供了一种方便且优雅设置对象字段值的方法,像Go中的gRPC库就采用了这种选项模式。


http://lihuaxi.xjx100.cn/news/2056304.html

相关文章

[MYSQL数据库]--mysql的基础知识

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、数据库…

优思学院|质量工程师需要学习什么软件?

初入职质量工程师的朋友常常会问&#xff1a;质量工程师需要学习什么软件&#xff1f;在质量控制和管理的世界里&#xff0c;拥有强大的数据分析工具是走向成功的关键&#xff0c;因此&#xff0c;对于质量工程师来说&#xff0c;掌握正确的软件不仅能提升工作效率&#xff0c;…

ad18学习笔记十六:如何放置精准焊盘到特定位置,捕抓功能的讲解

网上倒是一堆相关的指导 AD软件熟练度提升&#xff0c;如何设置板框捕捉&#xff1f;_哔哩哔哩_bilibili 关于Altium Designer 20 的捕抓功能的讲解_ad捕捉设置-CSDN博客 AD软件捕捉进阶实例&#xff0c;如何精确的放置布局元器件&#xff1f;_哔哩哔哩_bilibili AD绘制PCB…

MySQLBackup备份数据库

环境&#xff1a;MySQL 8.0.36 1、安装部署 [rootnode5 ~]# wget -c https://edelivery.oracle.com/osdc/softwareDownload?fileNameV1040085-01.zip [rootnode5 ~]# ll total 22776 -rw-------. 1 root root 1066 Jan 21 14:59 anaconda-ks.cfg -r–r–r–. 1 root root 23…

【Linux】进程优先级以及Linux内核进程调度队列的简要介绍

进程优先级 基本概念查看系统进程修改进程的优先级Linux2.6内核进程调度队列的简要介绍和进程优先级有关的概念进程切换 基本概念 为什么会存在进程优先级&#xff1f;   进程优先级用于确定在资源竞争的情况下&#xff0c;哪个进程将被操作系统调度为下一个运行的进程。进程…

Java的编程之旅31——多态

1.多态的简介 在Java中&#xff0c;多态是指通过父类的引用调用子类的对象&#xff0c;实现不同类型的对象的方法调用。 Java中的多态通过继承和重写实现。当子类继承父类并重写父类的方法时&#xff0c;可以通过父类的引用调用子类的对象&#xff0c;并且根据运行时实际的类型…

Orange3数据预处理(索引选择器组件)

组件描述 数据行即使在某些或全部原始变量被来自原始变量的计算变量替换时&#xff0c;也保持其身份。 此小部件获取两个数据表&#xff08;“数据”和“数据子集”&#xff09;&#xff0c;它们可以追溯到同一来源。基于行身份而非实际数据&#xff0c;它会从“数据”中选择所…

老卫带你学---leetcode刷题(191. 位1的个数)

191. 位1的个数 问题 编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中数字位数为 ‘1’ 的个数&#xff08;也被称为汉明重量&#xff09;。 提示&#xff1a; 请注意&#xff0c;在某些语言&…