跳到主要内容

选项模式

问题

什么是 Functional Options 模式?为什么它是 Go 中最重要的设计模式之一?

答案

问题:参数膨胀

// ❌ 参数越来越多,难以维护
func NewServer(host string, port int, timeout time.Duration, maxConn int, tls bool) *Server

// ❌ 配置结构体:调用者不知道哪些是必填,零值是默认还是忘了设
func NewServer(config ServerConfig) *Server

选项模式

// 选项函数类型
type Option func(*Server)

type Server struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
}

// 每个可选参数一个选项函数
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(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}

func WithMaxConn(n int) Option {
return func(s *Server) { s.maxConn = n }
}

func WithTLS() Option {
return func(s *Server) { s.tls = true }
}

// 构造函数:必选参数显式传入,可选参数用 Option
func NewServer(host string, port int, opts ...Option) *Server {
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second, // 默认值
maxConn: 100,
}
for _, opt := range opts {
opt(s)
}
return s
}

// 使用
server := NewServer("0.0.0.0", 8080,
WithTimeout(10*time.Second),
WithMaxConn(1000),
WithTLS(),
)

优势

优势说明
良好的默认值零配置即可使用
参数自文档WithTimeoutWithTLS 名字清晰
向后兼容新增选项不影响已有调用
必选 vs 可选分离必选参数放函数签名,可选用 Option

知名项目中的选项模式

// gRPC
conn, _ := grpc.Dial(target,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)

// zap Logger
logger, _ := zap.NewProduction(
zap.AddCaller(),
zap.AddStacktrace(zap.ErrorLevel),
)

// go-redis
rdb := redis.NewClient(&redis.Options{...}) // 这个用的是配置结构体

带错误的选项模式

type Option func(*Server) error

func WithPort(port int) Option {
return func(s *Server) error {
if port <= 0 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
s.port = port
return nil
}
}

func NewServer(opts ...Option) (*Server, error) {
s := &Server{port: 8080}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}

常见面试问题

Q1: 选项模式 vs 配置结构体,怎么选?

答案

维度选项模式配置结构体
默认值内部设置好零值可能有歧义(0 是默认还是没设?)
新增配置加个 WithXxx 函数加字段,已有代码不受影响
必选参数放在函数签名中需要文档说明
私有字段可以修改(闭包)需要导出字段
常见选择库/SDK API应用内部配置

经验法则:面向外部用户的库用选项模式,内部业务配置用结构体。

Q2: 选项模式的缺点?

答案

  1. 代码量:每个选项需要一个函数定义
  2. 不直观:不如结构体字面量所见即所得
  3. IDE 支持:自动补全不如结构体字段友好

但在库/SDK 设计中,可扩展性和默认值优势大于这些缺点。

Q3: 请手写一个选项模式的完整实现

答案

type Client struct {
baseURL string
timeout time.Duration
retries int
headers map[string]string
}

type Option func(*Client)

func WithTimeout(d time.Duration) Option {
return func(c *Client) { c.timeout = d }
}

func WithRetries(n int) Option {
return func(c *Client) { c.retries = n }
}

func WithHeader(key, value string) Option {
return func(c *Client) { c.headers[key] = value }
}

func NewClient(baseURL string, opts ...Option) *Client {
c := &Client{
baseURL: baseURL,
timeout: 10 * time.Second,
retries: 3,
headers: make(map[string]string),
}
for _, opt := range opts {
opt(c)
}
return c
}

相关链接