Go函数选项模式
Go函数选项模式(Functional Options)
写Go的时候,经常会遇到一个问题:一个结构体有很多配置项,有的必填有的选填,怎么设计API才好用?
函数选项模式就是解决这个问题最优雅的方式。
问题是什么
假设我们要创建一个HTTP服务器:
type Server struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
certFile string
keyFile string
}
方案一:直接传参
func NewServer(host string, port int, timeout time.Duration,
maxConn int, tls bool, certFile string, keyFile string) *Server {
return &Server{
host: host,
port: port,
timeout: timeout,
maxConn: maxConn,
tls: tls,
certFile: certFile,
keyFile: keyFile,
}
}
问题:参数太多,记不住顺序,不传TLS还得传空字符串。
方案二:传结构体
type ServerConfig struct {
Host string
Port int
Timeout time.Duration
MaxConn int
TLS bool
CertFile string
KeyFile string
}
func NewServer(cfg ServerConfig) *Server {
return &Server{
host: cfg.Host,
port: cfg.Port,
timeout: cfg.Timeout,
maxConn: cfg.MaxConn,
}
}
问题:没法区分必填和选填,调用者不知道哪些字段必须设置。而且零值是合法值还是”未设置”?无法区分。
方案三:Builder模式
Go里写Builder太啰嗦了,不符合Go的简洁风格。而且Builder方法没法在goroutine间安全使用(除非加锁)。
函数选项模式
核心思路:用函数作为参数,每个函数负责设置一个选项。
定义选项类型
type Option func(*Server)
一个函数,接收 *Server,修改它的字段。
定义选项函数
func WithTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConn(max int) Option {
return func(s *Server) {
s.maxConn = max
}
}
func WithTLS(certFile, keyFile string) Option {
return func(s *Server) {
s.tls = true
s.certFile = certFile
s.keyFile = keyFile
}
}
构造函数
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
}
调用
// 最简调用
s := NewServer("localhost", 8080)
// 按需配置
s := NewServer("localhost", 8080,
WithTimeout(10*time.Second),
WithMaxConn(200),
)
// 开启TLS
s := NewServer("localhost", 443,
WithTLS("cert.pem", "key.pem"),
WithMaxConn(500),
)
优点一目了然:
- 必填参数(host、port)放在前面,选填通过选项设置
- 调用者只写关心的配置,其他的用默认值
- 参数有名字,可读性好
- 新增选项不用改函数签名,向后兼容
实际项目中的例子
标准库和知名项目都在用
grpc-go:
conn, err := grpc.Dial("localhost:50051",
grpc.WithInsecure(),
grpc.WithTimeout(5*time.Second),
grpc.WithBlock(),
)
uber-go/zap:
logger := zap.NewExample(
zap.WithClock(zap.DefaultClock(zap.ISO8601)),
zap.WithCaller(false),
)
进阶用法
带校验的选项
选项函数里可以做校验:
func WithMaxConn(max int) Option {
return func(s *Server) {
if max <= 0 {
panic("maxConn must be positive")
}
s.maxConn = max
}
}
更好的做法是让构造函数返回 error:
func NewServer(host string, port int, opts ...Option) (*Server, error) {
if host == "" {
return nil, errors.New("host is required")
}
if port <= 0 || port > 65535 {
return nil, errors.New("invalid port")
}
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
maxConn: 100,
}
for _, opt := range opts {
opt(s)
}
if s.tls && (s.certFile == "" || s.keyFile == "") {
return nil, errors.New("TLS requires cert and key files")
}
return s, nil
}
选项可以组合
func WithProduction() Option {
return func(s *Server) {
s.timeout = 60 * time.Second
s.maxConn = 1000
s.tls = true
}
}
// 一行搞定生产配置
s := NewServer("0.0.0.0", 443, WithProduction())
接口型选项(带返回值)
有时候选项需要告诉调用者一些信息:
type Option func(*Server) error
func WithTLS(certFile, keyFile string) Option {
return func(s *Server) error {
if certFile == "" || keyFile == "" {
return errors.New("cert and key files are required")
}
s.tls = true
s.certFile = certFile
s.keyFile = keyFile
return nil
}
}
func NewServer(host string, port int, opts ...Option) (*Server, error) {
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
什么时候该用
适合用的场景:
- 结构体有多个可选配置项
- 需要提供合理的默认值
- API需要长期保持兼容(新增配置不破坏旧代码)
- 开源库的公开API
不需要用的场景:
- 只有两三个参数,直接传就行
- 配置项全是必填的
- 内部使用的简单结构体
别过度设计,三行能解决的事情不要写成三十行。
完整示例
package server
import (
"errors"
"time"
)
type Server struct {
host string
port int
timeout time.Duration
maxConn int
tls bool
certFile string
keyFile string
}
type Option func(*Server) error
func WithTimeout(d time.Duration) Option {
return func(s *Server) error {
if d <= 0 {
return errors.New("timeout must be positive")
}
s.timeout = d
return nil
}
}
func WithMaxConn(n int) Option {
return func(s *Server) error {
if n <= 0 {
return errors.New("maxConn must be positive")
}
s.maxConn = n
return nil
}
}
func WithTLS(certFile, keyFile string) Option {
return func(s *Server) error {
if certFile == "" || keyFile == "" {
return errors.New("cert and key files are required for TLS")
}
s.tls = true
s.certFile = certFile
s.keyFile = keyFile
return nil
}
}
func NewServer(host string, port int, opts ...Option) (*Server, error) {
s := &Server{
host: host,
port: port,
timeout: 30 * time.Second,
maxConn: 100,
}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
return s, nil
}
总结
函数选项模式的三个要点:
- 必填参数正常传,选填参数用
...Option - 构造函数里设默认值,选项函数覆盖默认值
- 新增选项不改签名,完美向后兼容
这是Go社区里最成熟的配置模式之一,grpc、zap、kubernetes都用它。如果你的结构体有多个可选配置,用这个模式准没错。