选项模式
问题
什么是 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(),
)
优势
| 优势 | 说明 |
|---|---|
| 良好的默认值 | 零配置即可使用 |
| 参数自文档 | WithTimeout、WithTLS 名字清晰 |
| 向后兼容 | 新增选项不影响已有调用 |
| 必选 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: 选项模式的缺点?
答案:
- 代码量:每个选项需要一个函数定义
- 不直观:不如结构体字面量所见即所得
- 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
}