一個活躍在眾多 Go 項目中的編程模式
今天我們介紹一個在 Go 語言中非常流行的編程模式:函數式選項模式(Functional Options)。該模式解決的問題是,如何更動態靈活地為對象配置參數。可能讀者不太明白該痛點,不急,我們將在下文詳細詳解。
問題
假設我們在代碼中定義了一個用戶的結構體對象 User,它擁有以下屬性。
type User struct {
ID string // 必需項
Name string // 必需項
Age int // 非必需項
Gender bool // 非必需項
}
初始化該對象時,最簡單的方式是直接填充屬性值,例如
u := &User{ID: "12glkui234d", Name: "菜刀", Age: 18, Gender: true}
但是這里存在一個問題:User 對象中的屬性并不一定都是可導出的,例如 User 有一個屬性字段為 password(首字母小寫,非導出),如果在其他模塊中需要構造 User 對象,這樣就不能填充該 password 字段了。
所以我們需要定義構造 User 對象的函數,首先能想到最簡單的構造函數方式如下。
func NewUser(id, name string, age int, gender bool) *User {
return &User{
ID: id,
Name: name,
Age: age,
Gender: gender,
}
}
但是這樣也存在一些問題:對于 User 對象而言,只有 ID、Name 屬性是必須的,Age 與 Gender 為非必需項,且并不能設置默認值,例如 Age 的默認值為 0,Gender 的默認值是 false ,這顯然不太合理。
面對該問題,我們可以采用的解決方案有哪些呢?
方案一:多函數構造
我們能想到最粗暴地解決方法是:為每種參數情況設置一種構造函數。如下代碼所示
func NewUser(id, name string) *User {
return &User{ID: id, Name: name}
}
func NewUserWithAge(id, name string, age int) *User {
return &User{ID: id, Name: name, Age: age}
}
func NewUserWithGender(id, name string, gender bool) *User {
return &User{ID: id, Name: name, Gender: gender}
}
func NewUserWithAgeGender(id, name string, age int, gender bool) *User {
return &User{ID: id, Name: name, Age: age, Gender: gender}
}
這種方式適合參數較少且不易發生變化的情況。該方式在 Go 標準庫中也有使用,例如 net 包中的 Dial 和 DialTimeout 方法。
func Dial(network, address string) (Conn, error) {}
func DialTimeout(network, address string, timeout time.Duration) (Conn, error) {}
但該方式的缺陷也很明顯:試想,如果構造對象 User 增加了參數字段 Phone,那么我們需要新增多少個組合函數?
方案二:配置化
另外一種常見的方式是配置化,我們將所有可選的參數放入一個 Config 的配置結構體中。
type User struct {
ID string
Name string
Cfg *Config
}
type Config struct {
Age int
Gender bool
}
func NewUser(id, name string, cfg *Config) *User {
return &User{ID: id, Name: name, Cfg: cfg}
}
這樣,我們只需要一個 NewUser() 函數,不管之后增加多少配置選項,NewUser 函數都不會得到破壞。
但是,這種方式,我們需要先構造 Config 對象,這時候對 Config 的構造又回到了方案一中存在的問題。
方案三:函數式選項模式
面對這樣的問題,我們還可以選擇函數式選項模式。
首先,我們定義一個 Option 函數類型
type Option func(*User)
然后,為每個屬性值定義一個返回 Option 函數的函數
func WithAge(age int) Option {
return func(u *User) {
u.Age = age
}
}
func WithGender(gender bool) Option {
return func(u *User) {
u.Gender = gender
}
}
此時,我們將 User 對象的構造函數改為如下所示
func NewUser(id, name string, options ...Option) *User {
u := &User{ID: id, Name: name}
for _, option := range options {
option(u)
}
return u
}
按照這種構造方式,我們就可以這樣配置 User 對象了
u := NewUser("12glkui234d", "菜刀", WithAge(18), WithGender(true))
以后不管 User 增加任何參數 XXX,我們只需要增加對應的 WithXXX 函數即可,是不是非常地優雅?
Functional Options 這種編程模式,我們經常能在各種項目中找到它的身影。例如,我在 tidb 項目中僅使用 opts ... 關鍵字搜索,就能看到這么多使用了 Functional Options 的代碼(截圖還未包括全部)。

總結
函數式選項模式解決了如何動態靈活地為對象配置參數的問題, 但是需要在合適的場景才使用它。
當對象的配置參數復雜,例如可選參數多、非導入字段、參數可能隨版本增加等情況,這時函數式選項模式就可以很好地幫助到我們。

































