go 编程模式之 functional options | go 技术论坛-江南app体育官方入口

什么是functional options编程模式

functional options 编程模式是一种在 go 语言中构造结构体的模式,这种模式允许用户通过一系列函数来传递配置选项,而不是通过构造函数的参数列表。它在 go 中特别有用,因为 go 没有构造函数,通常通过定义new函数来初始化结构体。

为什么使用functional options模式

在 go 中,如果一个结构体有很多配置选项,传统的方法是为每个不同的配置选项声明一个新的构造函数,或者定义一个新的配置结构体来保存配置信息。但是,这些方法都有局限性,比如增加新的配置选项时需要修改构造函数或配置结构体,这可能会导致代码的维护成本增加。functional options 模式提供了一种更灵活和可扩展的方式来处理这种情况。

实例初始化

在介绍 functional options 模式之前,先来看一下传统的 struct 实例初始化是怎么做的。

假设我们有一个 server 结构体如下:

type server struct {
    host     string
    port     int
    timeout  time.duration
    maxconn  int
}

为了在实例化时更好的控制结构体的初始值,通常需要编写构造函数来实现:

func newserver() *server {
    return &server{
        host:    "127.0.0.1",
        port:    8080,
        timeout: time.second * 5,
        maxconn: 1000,
    }
}
func newcustomserver(host string, port int) *server {
    return &server{
        host: host,
        port: port,
    }
}

由于 go 语言不支持重载函数,所以,创建两种 server 配置就意味着构造两个不同函数名的函数,随着业务的扩展,可能会出现更多不同的 server 配置,就需要创建更多 server 构造函数来满足要求。

为了解决这个问题,通常有以下几种解决方法。

方法一:编写 set 方法

func (s *server) sethost(host string) {
    s.host = host
}
func (s *server) setport(port int) {
    s.port = port
}
func (s *server) settimeout(timeout time.duration) {
    s.timeout = timeout
}
func (s *server) setmaxconn(maxconn int) {
    s.maxconn = maxconn
}

为 server 结构体的每个字段写一个 set 方法,这样我们在初始化实例之后,可以调用对应的 set 方法来为我们想要的字段进行赋值。

func setupserver() {
    s := newcustomserver("192.168.1.1", 8081)
    s.settimeout(time.second * 10)
    s.setmaxconn(2000)
    fmt.println(s)
}

这种方式可以直接地表达我们的意图,即调用一个方法设置一个属性。但缺点是需要创建很多的 set 方法,实例化时也需要很多的行来设置多个属性,所以当你的实例初始化参数比较复杂时,不推荐此种方式。

方法二:使用结构体来传递可选参数

我们把 host 和 port 定义为 server 的必传参数,其他为可选参数,将其他的可选参数用一个新的结构体 config 来表示,然后将 config 嵌入到 server 结构体中。

type server struct {
    host string
    port int
    conf *config
}
type config struct {
    maxconn int
    timeout time.duration
}

这时候我们的构造函数为下面这样:

func newserver(host string, port int, conf *config) *server {
    if conf == nil {
        conf = &config{
            maxconn: 1000,
            timeout: time.second * 5,
        }
    }
    return &server{
        host: host,
        port: port,
        conf: conf,
    }
}

在进行初始化的时候需要先构造一个 config 对象,然后传递给构造函数。

func setupserver() {
    conf := &config{
        maxconn: 2000,
        timeout: time.second * 10,
    }
    // 默认配置
    s1 := newserver("192.168.1.1", 8081, nil)
    // 自定义配置
    s2 := newserver("192.168.1.2", 8082, conf)
}

这种方式使用 config 结构体来存放可选参数,保持了 api 简洁,提高了代码的可读性和可维护性,同时提供了良好的扩展性。但缺点是稍微复杂了一点,也不那么美观,对于默认的配置来说,需要多一个config 参数。另外,需要注意nilconfig{}的区别,虽然我们在构造函数中处理了nil的情况,但仍然存在外部代码误用的风险,例如忘记初始化 config 或者错误地传递了nil值。

方法三 builder 模式

学习过设计模式的人,肯定还会想到使用 builder 模式。这种方式允许我们使用链式调用的方法来初始化实例。仿照 builder 模式,把我们的代码修改为下面的样子:

type server struct {
    host    string
    port    int
    maxconn int
    timeout time.duration
}
func newserver(host string, port int) *server {
    return &server{
        host: host,
        port: port,
    }
}
func (s *server) withmaxconn(maxconn int) *server {
    s.maxconn = maxconn
    return s
}
func (s *server) withtimeout(timeout time.duration) *server {
    s.timeout = timeout
    return s
}

这样,我们就可以使用下面的方式来初始化代码了:

func setupserver() {
    s := newserver("192.168.1.1", 8081).
        withmaxconn(1000).
        withtimeout(time.second * 10)
}

注意,这里没有考虑错误处理的情况。如果自定义参数不多,参数也都相对比较明确的情况下是可以不考虑错误处理的。但如果参数比较复杂,推荐使用一个包装类,这样最后在处理参数错误的时候会方便很多。大概代码像下面这样:

package options
import (
    "crypto/tls"
    "errors"
    "fmt"
    "time"
)
// server 定义了服务器的结构。
type server struct {
    addr     string
    port     int
    protocol string
    maxconns int
    timeout  time.duration
    tls      *tls.config // 假设这里有一个 tls.config 类型,用于配置 tls
}
// serverbuilder 是用于构建 server 的建造者。
type serverbuilder struct {
    server *server
    err    error
}
// newserverbuilder 创建一个新的 serverbuilder 实例。
func newserverbuilder() *serverbuilder {
    return &serverbuilder{}
}
// withaddr 设置地址,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withaddr(addr string) *serverbuilder {
    if addr == "" {
        sb.err = errors.new("address is required")
        return sb
    }
    sb.server.addr = addr
    return sb
}
// withport 设置端口,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withport(port int) *serverbuilder {
    if port <= 0 || port > 65535 {
        sb.err = errors.new("invalid port number, must be between 1 and 65535")
        return sb
    }
    sb.server.port = port
    return sb
}
// withprotocol 设置协议,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withprotocol(protocol string) *serverbuilder {
    if protocol != "tcp" && protocol != "udp" {
        sb.err = errors.new("invalid protocol, must be 'tcp' or 'udp'")
        return sb
    }
    sb.server.protocol = protocol
    return sb
}
// withmaxconns 设置最大连接数,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withmaxconns(maxconns int) *serverbuilder {
    if maxconns < 0 {
        sb.err = errors.new("max connections cannot be negative")
        return sb
    }
    sb.server.maxconns = maxconns
    return sb
}
// withtimeout 设置超时时间,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withtimeout(timeout time.duration) *serverbuilder {
    if timeout < 0 {
        sb.err = errors.new("timeout cannot be negative")
        return sb
    }
    sb.server.timeout = timeout
    return sb
}
// withtls 设置 tls 配置,并返回建造者本身以便链式调用。
func (sb *serverbuilder) withtls(tlsconfig *tls.config) *serverbuilder {
    sb.server.tls = tlsconfig
    return sb
}
// build 尝试构建 server 实例。
func (sb *serverbuilder) build() (*server, error) {
    if sb.err != nil {
        return nil, sb.err
    }
    return sb.server, nil
}
func setupserver() {
    builder := newserverbuilder()
    server, err := builder.
        withaddr("127.0.0.1").
        withport(8080).
        withprotocol("udp").
        withmaxconns(1024).
        withtimeout(30 * time.second).
        build()
    if err != nil {
        fmt.printf("error creating server: %v\n", err)
        return
    }
    fmt.printf("server created: % v\n", server)
}

functional options模式实现

介绍完上面三中实现方式,终于轮到主角 functional options 登场了。基础结构体还是不变:

type server struct {
    host     string
    port     int
    timeout  time.duration
    maxconn  int
}

创建选项类型和选项函数

为了支持 functional options 模式,我们需要定义一个接受*server指针的函数类型option,并创建设置server属性的选项函数:

type option func(*server)
func withhost(host string) option {
    return func(s *server) {
        s.host = host
    }
}
func withport(port int) option {
    return func(s *server) {
        s.port = port
    }
}
func withtimeout(timeout time.duration) option {
    return func(s *server) {
        s.timeout = timeout
    }
}
func withmaxconn(maxconn int) option {
    return func(s *server) {
        s.maxconn = maxconn
    }
}

定义构造函数和实例化

默认构造函数,使用可选参数,参数类型为我们自定义的 option。接下来是关键点,在构造函数中,使用一个 for 循环,遍历传入的选项函数,完成实例化构造:

func newserver(options ...option) *server {
    svr := &server{
        host:   "localhost",
        port:   8080,
        timeout: time.minute,
        maxconn: 100,
    }
    for _, opt := range options {
        opt(svr)
    }
    return svr
}

这样在实例化的时候,就可以这样使用:

func setupserver() {
    svr := newserver(
        withhost("localhost"),
        withport(8080),
        withtimeout(time.minute),
        withmaxconn(120),
    )
}

functional options 模式在处理复杂配置选项时是非常有用的,尤其是在这些选项可能来自不同来源(如文件、环境变量等)的情况下,或者更具外部情况决定要开启什么配置的时候。

options 模式最佳实践

  1. 默认值初始化:new 方法中设置默认值,用户可以仅覆盖需要的部分。
func newserver(opts ...option) *server {
    server := &server{
        host:          "localhost", // 默认值
        port:          80,
        timeout:       30,
        maxconnections: 100,
    }
    for _, opt := range opts {
        opt(server)
    }
    return server
}
// 使用方式
server := newserver(withport(8080))

好处:开发者不需要显式传递所有参数,默认值会自动填充未指定的字段。

  1. 组合 options:将多个逻辑相关的配置合并为一个复合选项。
func withhighperformancesettings() option {
        return func(s *server) {
            s.timeout = 10
            s.maxconnections = 1000
        }
}
// 使用方式
server := newserver(withport(8080), withhighperformancesettings())

好处:提高代码复用性,让复杂配置简单化。

  1. 条件选项:根据某些条件动态设置参数。
func withconditionaltimeout(condition bool, timeout int) option {
    return func(s *server) {
        if condition {
            s.timeout = timeout
        }
    }
}
// 使用方式
istest := true
server := newserver(withconditionaltimeout(istest, 5))

好处:通过条件选项可以处理上下文敏感的逻辑。

  1. 链式 option:支持链式调用,用更简洁的语法配置对象。
func (s *server) apply(opts ...option) *server {
    for _, opt := range opts {
        opt(s)
    }
    return s
}
// 使用方式
server := (&server{}).apply(withport(8080), withtimeout(20))

好处:允许对已经创建的对象动态调整配置。

  1. 类型安全的泛型 option:通过泛型封装更类型安全的选项:
type configurable[t any] struct {
    value t
}
type option[t any] func(*configurable[t])
func withvalue[t any](value t) option[t] {
    return func(c *configurable[t]) {
        c.value = value
    }
}
func newconfigurable[t any](opts ...option[t]) *configurable[t] {
    c := &configurable[t]{}
    for _, opt := range opts {
        opt(c)
    }
    return c
}
// 使用方式
config := newconfigurable[int](withvalue(42))
fmt.println(config.value) // 输出 42

好处:让 option 模式支持更加复杂的类型和配置。

  1. 调试友好的 option:为每个 option 添加日志或注释,便于排查问题。
func withloggedport(port int) option {
    return func(s *server) {
        fmt.printf("setting port to %d\n", port)
        s.port = port
    }
}

好处:对复杂配置进行跟踪和调试。

  1. 互斥或依赖的 option:option 中处理参数间的依赖关系或互斥逻辑。
func withsecuremode(enable bool) option {
    return func(s *server) {
        if enable {
            s.port = 443
            s.host = "https"
        }
    }
}
// 使用方式
server := newserver(withsecuremode(true))

好处:避免调用者在使用时混淆配置规则。

  1. 延迟计算的 option:某些参数可以在构造对象时动态计算,而不是直接传递。
func withdynamicport(getport func() int) option {
    return func(s *server) {
        s.port = getport()
    }
}
// 使用方式
server := newserver(withdynamicport(func() int { return 8080 }))

好处:支持运行时动态设置值,适合依赖外部条件的场景。

  1. option 校验:new 方法中校验 option 的合法性,避免不一致的配置。
func newserver(opts ...option) (*server, error) {
    server := &server{}
    for _, opt := range opts {
        opt(server)
    }
    if server.port == 0 {
        return nil, fmt.errorf("port must be specified")
    }
    return server, nil
}

好处:在初始化时捕获潜在错误。

总结

functional options 模式增强了代码的灵活性和可维护性,尤其在处理复杂的配置选项时表现尤为突出。本文旨在介绍 functional options 模式的概念及其应用,并讨论了几种常见的实例初始化方法。需强调的是,本文无意贬低或推崇任一特定方法,只是建议在面对复杂的实例化需求时,可以考虑采用 functional options 模式作为江南app体育官方入口的解决方案之一。选择最适合当前场景的方法,才是正确的打开方式

本作品采用《cc 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
网站地图