go 编程模式之 Functional Options
什么是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 参数。另外,需要注意nil
和Config{}
的区别,虽然我们在构造函数中处理了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 模式最佳实践
- 默认值初始化:在
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))
好处:开发者不需要显式传递所有参数,默认值会自动填充未指定的字段。
- 组合 Options:将多个逻辑相关的配置合并为一个复合选项。
func WithHighPerformanceSettings() Option {
return func(s *Server) {
s.timeout = 10
s.maxConnections = 1000
}
}
// 使用方式
server := NewServer(WithPort(8080), WithHighPerformanceSettings())
好处:提高代码复用性,让复杂配置简单化。
- 条件选项:根据某些条件动态设置参数。
func WithConditionalTimeout(condition bool, timeout int) Option {
return func(s *Server) {
if condition {
s.timeout = timeout
}
}
}
// 使用方式
isTest := true
server := NewServer(WithConditionalTimeout(isTest, 5))
好处:通过条件选项可以处理上下文敏感的逻辑。
- 链式 Option:支持链式调用,用更简洁的语法配置对象。
func (s *Server) Apply(opts ...Option) *Server {
for _, opt := range opts {
opt(s)
}
return s
}
// 使用方式
server := (&Server{}).Apply(WithPort(8080), WithTimeout(20))
好处:允许对已经创建的对象动态调整配置。
- 类型安全的泛型 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 模式支持更加复杂的类型和配置。
- 调试友好的 Option:为每个
Option
添加日志或注释,便于排查问题。
func WithLoggedPort(port int) Option {
return func(s *Server) {
fmt.Printf("Setting port to %d\n", port)
s.port = port
}
}
好处:对复杂配置进行跟踪和调试。
- 互斥或依赖的 Option:在
Option
中处理参数间的依赖关系或互斥逻辑。
func WithSecureMode(enable bool) Option {
return func(s *Server) {
if enable {
s.port = 443
s.host = "https"
}
}
}
// 使用方式
server := NewServer(WithSecureMode(true))
好处:避免调用者在使用时混淆配置规则。
- 延迟计算的 Option:某些参数可以在构造对象时动态计算,而不是直接传递。
func WithDynamicPort(getPort func() int) Option {
return func(s *Server) {
s.port = getPort()
}
}
// 使用方式
server := NewServer(WithDynamicPort(func() int { return 8080 }))
好处:支持运行时动态设置值,适合依赖外部条件的场景。
- 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 模式作为解决方案之一。选择最适合当前场景的方法,才是正确的打开方式
本作品采用《CC 协议》,转载必须注明作者和本文链接