假設我們在程式碼中定義了一個使用者的結構體物件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 的代码(截图还未包括全部)。
函數式選項模式解決如何動態靈活地為物件配置參數的問題, 但是需要在適當的場景中才使用它。
當物件的配置參數複雜,例如可選參數多、非導入欄位、參數可能隨版本增加等情況,這時函數式選項模式就可以很好地幫助到我們。
以上是一個活躍在眾多 Go 專案中的程式設計模式的詳細內容。更多資訊請關注PHP中文網其他相關文章!