主页 > 电脑硬件  > 

【go语言规范】使用函数式选项FunctionalOptions模式处理可选配置

【go语言规范】使用函数式选项FunctionalOptions模式处理可选配置
如何处理可选配置? Config Struct 方式 (config-struct/main.go) 这是最简单的方式,使用一个配置结构体: 定义了一个简单的 Config 结构体,包含 Port 字段创建服务器时直接传入配置对象优点:简单直接缺点:不够灵活,所有字段都必须设置值,即使只想修改其中一个 Builder 模式 (builder/main.go) 使用建造者模式: 定义 ConfigBuilder 结构体来构建配置提供链式调用方法如 Port()通过 Build() 方法验证并生成最终配置优点:支持链式调用,可以进行参数验证缺点:需要编写较多样板代码 函数选项模式 (functional-options/main.go) 这是最灵活的方式: 定义 Option 函数类型用于修改配置使用 WithXXX 函数创建配置选项支持默认值和参数验证可以方便地添加新的配置项使用示例: NewServer("localhost", WithPort(8080))

详细代码转载go语言经典100错

package main import ( "errors" "net/http" ) // 默认HTTP服务端口 const defaultHTTPPort = 8080 // options 结构体用于存储所有配置选项 type options struct { port *int // 使用指针以区分是否设置了端口 } // Option 定义了功能选项的函数类型 // 每个选项都是一个函数,接收 options 指针并返回错误 type Option func(options *options) error // WithPort 创建一个设置端口的选项 // 这是一个工厂函数,返回一个闭包 // 闭包可以访问外部函数 WithPort 中的 port 参数 func WithPort(port int) Option { // 这里返回的匿名函数就是一个闭包 // 它可以访问并持有外部函数 WithPort 的 port 参数 // 即使 WithPort 函数执行完毕,返回的闭包仍然可以访问 port 值 return func(options *options) error { if port < 0 { return errors.New("port should be positive") } options.port = &port return nil } } // NewServer 创建一个新的 HTTP 服务器 // addr: 服务器地址 // opts: 可变参数,包含所有功能选项 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 } } // 确定最终使用的端口 var port int if options.port == nil { // 未设置端口,使用默认值 port = defaultHTTPPort } else { if *options.port == 0 { // 端口为0,使用随机端口 port = randomPort() } else { // 使用指定的端口 port = *options.port } } _ = port return nil, nil } // client 展示如何使用功能选项模式 func client() { _, _ = NewServer("localhost", WithPort(8080)) } // randomPort 返回一个随机端口号 // 注:这是一个示例实现,实际应用中应该使用真正的随机端口生成方法 func randomPort() int { return 4 // 通过掷骰子选择的,保证随机(开玩笑的) }

函数选项模式的优势:

支持默认值参数可选易于扩展支持参数验证API 使用简洁

这是 Go 语言中最推荐的配置模式之一,特别适合那些有很多可选配置项的情况。


在k8s 源码中,可以找到如下例子使用了 functional-options方法

Kubernetes 源码中大量使用了 Functional Options 模式,我来展示一个典型的例子:

ClientSet 的创建 // NewForConfig creates a new Clientset for the given config. func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter( configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset // ... 其他代码 return &cs, nil } // NewForConfigOrDie creates a new Clientset for the given config and // panics if there is an error in the config. func NewForConfigOrDie(c *rest.Config) *Clientset { cs, err := NewForConfig(c) if err != nil { panic(err) } return cs } RESTClient 的选项模式 // RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config object. func RESTClientFor(config *Config) (*RESTClient, error) { // ...existing code... return NewRESTClient( config.Host, config.APIPath, config.ContentConfig, config.QPS, config.Burst, config.RateLimiter, config.WrapTransport, ) } Informer 的创建选项 // SharedInformerOption defines the functional option type for SharedInformerFactory. type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory // WithCustomResyncConfig sets a custom resync period for the specified informer types. func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { for obj, resyncPeriod := range resyncConfig { factory.customResync[obj] = resyncPeriod } return factory } } // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { return func(factory *sharedInformerFactory) *sharedInformerFactory { factory.tweakListOptions = tweakListOptions return factory } } // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. func NewSharedInformerFactoryWithOptions(client kubernetes.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { factory := &sharedInformerFactory{ client: client, namespace: v1.NamespaceAll, defaultResync: defaultResync, informers: make(map[reflect.Type]cache.SharedIndexInformer), startedInformers: make(map[reflect.Type]bool), customResync: make(map[reflect.Type]time.Duration), } // 应用所有选项 for _, opt := range options { factory = opt(factory) } return factory }

这些例子展示了 Kubernetes 如何使用 Functional Options 模式来:

灵活配置

客户端配置资源监控器配置REST 客户端选项

默认值处理

速率限制同步周期API 路径

可扩展性

自定义选项可选功能行为修改

这种模式在 Kubernetes 中广泛使用,因为它:

提供了良好的 API 扩展性保持了向后兼容性使配置更加灵活代码更易于维护和测试
Function Options 模式与接口定义的区别

你提出了一个很好的问题。让我们来分析一下:

接口与 Function Options 的不同点 接口定义的原则 // 接口应该在使用处定义 type userRepository interface { Save(user User) error Find(id string) (*User, error) } type UserService struct { repo userRepository // 在使用处定义接口 } Function Options 的特点 // 选项定义在包的实现端 type Option func(*options) error func WithPort(port int) Option { return func(o *options) error { o.port = &port return nil } } 为什么 Function Options 不需要在客户端定义

不是抽象依赖

Function Options 是配置模式,不是依赖抽象它是实现细节的一部分,不是接口契约

封装性

// 实现包中 type options struct { port *int // 私有字段 timeout *time.Duration } // 客户端使用 server, _ := NewServer("localhost", WithPort(8080), WithTimeout(5 * time.Second), ) API 稳定性 选项函数提供了稳定的公共 API内部 options 结构体可以随时更改而不影响客户端 实际例子 // 服务端定义(正确的方式) package server type Option func(*options) error func WithPort(port int) Option { ... } func WithTimeout(t time.Duration) Option { ... } // 客户端使用(简洁清晰) package client func main() { server.NewServer("localhost", server.WithPort(8080), server.WithTimeout(5 * time.Second), ) } 总结 接口定义在使用处是为了依赖抽象Function Options 定义在实现处是为了配置灵活性两者解决的是不同的问题: 接口:依赖反转Options:配置管理
Go Functional Options 模式与 Java 构建器模式的对比 Java 构建器模式 public class Server { private final String addr; private final int port; private final int timeout; private Server(Builder builder) { this.addr = builder.addr; this.port = builder.port; this.timeout = builder.timeout; } public static class Builder { private String addr; private int port = 8080; // 默认值 private int timeout = 30; // 默认值 public Builder(String addr) { this.addr = addr; } public Builder port(int port) { this.port = port; return this; } public Builder timeout(int timeout) { this.timeout = timeout; return this; } public Server build() { return new Server(this); } } } Go Functional Options 模式 type options struct { port *int timeout *time.Duration } type Option func(*options) error func WithPort(port int) Option { return func(o *options) error { if port < 0 { return errors.New("invalid port") } o.port = &port return nil } } func NewServer(addr string, opts ...Option) (*Server, error) { var options options for _, opt := range opts { if err := opt(&options); err != nil { return nil, err } } // ... } 主要区别

语法风格

Java: 链式调用,方法返回 thisGo: 函数式风格,使用闭包

错误处理

Java: 通常在 build() 时检查参数有效性Go: 每个选项函数都可以返回错误

使用方式

// Java Server server = new Server.Builder("localhost") .port(8080) .timeout(30) .build(); // Go server, err := NewServer("localhost", WithPort(8080), WithTimeout(30 * time.Second))

扩展性

Java: 需要修改 Builder 类Go: 只需添加新的选项函数

默认值处理

Java: 在 Builder 构造函数中设置Go: 在 NewServer 函数中处理 Go 方式的优势

更符合 Go 语言特性

利用函数式编程特性使用错误返回值而不是异常

更灵活的扩展性

选项函数可以在任何包中定义不需要修改核心代码

更好的参数验证

每个选项都可以独立验证立即返回错误

更简洁的 API

没有额外的 Builder 类参数传递更直观 使用场景建议 Java Builder: 当参数较多且有复杂的构建逻辑时Go Options: 当需要灵活配置且重视可扩展性时

是的,Java 的 Builder 模式和 Go 的 Functional Options 模式都是配置模式的不同实现。让我对比一下:

Java Builder 模式 public class Server { private final int port; private final int timeout; public static class Builder { // 可配置项,有默认值 private int port = 8080; private int timeout = 30; // 链式配置方法 public Builder port(int port) { this.port = port; return this; } // 构建最终对象 public Server build() { // 参数验证 if (port < 0) { throw new IllegalArgumentException("port must be positive"); } return new Server(this); } } } Go Functional Options 模式 type options struct { port *int timeout *time.Duration } type Option func(*options) error func WithPort(port int) Option { return func(o *options) error { if port < 0 { return errors.New("port must be positive") } o.port = &port return nil } } 共同点

配置的灵活性

都支持可选参数都有默认值机制都支持参数验证

构建过程控制

Java: 通过 build() 方法Go: 通过 NewServer() 函数

参数验证

都可以在构建过程中进行参数验证 主要区别 语法风格 // Java: 链式调用 Server server = new Server.Builder() .port(8080) .timeout(30) .build(); // Go: 函数式风格 server, err := NewServer("localhost", WithPort(8080), WithTimeout(30 * time.Second), )

错误处理

Java: 使用异常Go: 返回错误值

扩展方式

Java: 需要修改 Builder 类Go: 只需添加新的选项函数 选择建议

使用 Java Builder 模式当:

需要严格的参数校验对象构建过程复杂需要不可变对象

使用 Go Functional Options 模式当:

需要高度灵活性配置项可能在不同包中扩展错误处理更为重要

两种模式都是优秀的配置模式实现,选择哪种主要取决于:

使用的编程语言项目的具体需求团队的编程风格偏好
标签:

【go语言规范】使用函数式选项FunctionalOptions模式处理可选配置由讯客互联电脑硬件栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【go语言规范】使用函数式选项FunctionalOptions模式处理可选配置