一般傳遞
Go 語言支援通過順序傳遞參數來調用函數,如以下示例函數所示。
// ListApplications Query Application List
func ListApplications(limit, offset int) []Application {
return allApps[offset : offset+limit]
}
調用代碼
ListApplications(5, 0)
當您想添加新參數時,隻需更改函數簽名即可。例如,以下代碼
owner
向
ListApplications
.
func ListApplications(limit, offset int, owner string) []Application {
if owner != "" {
// ...
}
return allApps[offset : offset+limit]
}
調用代碼需要相應更改。
ListApplications(5, 0, "piglei")
// Do not use "owner" filtering
ListApplications(5, 0, "")
顯然,這種常見的傳遞參數模型存在幾個明顯的問題。
- 可讀性差:僅支援位置,不支援區分參數的關鍵字,添加更多參數後,每個參數的含義難以一目了然。
- 破壞性相容性:添加新參數後,必須修改原來的調用代碼,
如上例,在參數位置傳入空字元串ListApplications(5, 0, "")
。owner
為了解決這些問題,通常的做法是引入參數結構(struct)類型。
2. 使用參數結構
建立一個包含函數需要支援的所有參數的新結構類型
// ListAppsOptions is optional when querying the application list
type ListAppsOptions struct {
limit int
offset int
owner string
}
修改原始函數以直接接受此結構類型作為唯一參數。
// ListApplications Query the application list, using the structure-based query option.
func ListApplications(opts ListAppsOptions) []Application {
if opts.owner != "" {
// ...
}
return allApps[opts.offset : opts.offset+opts.limit]
}
調用代碼如下所示。
ListApplications(ListAppsOptions{limit: 5, offset: 0, owner: "piglei"})
ListApplications(ListAppsOptions{limit: 5, offset: 0})
與普通模型相比,使用參數結構有幾個優點。
- 在構造參數結構時,可以顯式指定每個參數的字段名,這樣更具可讀性。
- 對于非必要的參數,您可以在不傳遞值的情況下建構它們,例如省略
上面。owner
但是,有一個普通模式或參數結構都不支援的常見使用場景:真正的可選參數。
3.隐藏在可選參數中的陷阱
為了示範“可選參數”的問題,我們在
ListApplications
函數中添加了一個新選項:
hasDeployed
– 根據應用程式是否已部署來過濾結果。
參數結構調整如下。
// ListAppsOptions is optional when querying the application list
type ListAppsOptions struct {
limit int
offset int
owner string
hasDeployed bool
}
查詢功能也做了相應的調整。
// ListApplications Query application list, add filtering for HasDeployed
func ListApplications(opts ListAppsOptions) []Application {
// ...
if opts.hasDeployed {
// ...
} else {
// ...
}
return allApps[opts.offset : opts.offset+opts.limit]
}
當我們要過濾已部署的應用程式時,可以這樣調用。
ListApplications(ListAppsOptions{limit: 5, offset: 0, hasDeployed: true})
而當我們不需要通過“部署狀态”進行過濾時,我們可以删除該
hasDeployed
字段并
ListApplications
使用以下代碼調用該函數。
ListApplications(ListAppsOptions{limit: 5, offset: 0})
等等……好像有些不對勁。
hasDeployed
是布爾類型,這意味着當我們不為其提供任何值時,程式将始終使用布爾類型的零值:
false
.
是以,現在的代碼實際上根本沒有得到“未按部署狀态過濾”的結果,
hasDeployed
要麼是要麼
true
不
false
存在其他狀态。
4.可選地引入指針類型支援
要解決上述問題,最直接的辦法就是引入指針類型。與普通值類型不同,Go 中的指針類型有一個特殊的零值:
nil
. 是以,簡單地
hasDeployed
從布爾類型 (
bool
) 更改為指針類型 (
*bool
) 就可以更好地支援可選參數。
type ListAppsOptions struct {
limit int
offset int
owner string
// Enable pointer types
hasDeployed *bool
}
查詢功能也需要一些調整。
// ListApplications Query application list, add filtering for HasDeployed
func ListApplications(opts ListAppsOptions) []Application {
// ...
if opts.hasDeployed == nil {
// No filtering by default
} else {
// Filter by whether hasDeployed is true or false
}
return allApps[opts.offset : opts.offset+opts.limit]
}
調用函數時,如果調用者沒有指定字段的值,則代碼不經過任何過濾
hasDeployed
就轉到分支。
if opts.hasDeployed == nil
ListApplications(ListAppsOptions{limit: 5, offset: 0})
當調用者想要過濾時
hasDeployed
,可以使用以下。
wantHasDeployed := true
ListApplications(ListAppsOptions{limit: 5, offset: 0, hasDeployed: &wantHasDeployed})
在 golang 中,實際上可以通過以下方式快速建立一個非 nil 指針變量。
ListAppsOptions{limit: 5, offset: 0, hasDeployed: &[]bool{true}[0]}
如您所見,由于
hasDeployed
現在是指針類型
*bool
,我們必須先建立一個臨時變量,然後擷取它的指針來調用函數。
不用說,這很麻煩,不是嗎?有沒有辦法解決傳遞函數參數時的上述痛點,又不會讓調用過程像“手動建構指針”那樣繁瑣?
然後是功能選項模式發揮作用的時候了。
5.“功能選項”模式
除了普通的傳參模式外,Go 實際上還支援可變數量的參數,使用該特性的函數統稱為“可變參數函數”。例如,
append
并且
fmt.Println
屬于這一類。
nums := []int{}
// When calling append, multiple arguments can be passed
nums = append(nums, 1, 2, 3, 4)
為了實作“功能選項”模式,我們首先修改
ListApplications
函數的簽名以采用可變數量的類型參數
func(*ListAppsOptions)
。
// ListApplications Query the list of applications, using variable arguments
func ListApplications(opts ...func(*ListAppsOptions)) []Application {
config := ListAppsOptions{limit: 10, offset: 0, owner: "", hasDeployed: nil}
for _, opt := range opts {
opt(&config)
}
// ...
return allApps[config.offset : config.offset+config.limit]
}
然後,為調整選項定義了一系列工廠函數。
func WithPager(limit, offset int) func(*ListAppsOptions) {
return func(opts *ListAppsOptions) {
opts.limit = limit
opts.offset = offset
}
}
func WithOwner(owner string) func(*ListAppsOptions) {
return func(opts *ListAppsOptions) {
opts.owner = owner
}
}
func WithHasDeployed(val bool) func(*ListAppsOptions) {
return func(opts *ListAppsOptions) {
opts.hasDeployed = &val
}
這些名為 的工廠函數通過傳回閉包函數來
With*
修改函數選項對象。
ListAppsOptions
調用時的代碼如下。
// No arguments are used
ListApplications()
// Selectively enable certain options
ListApplications(WithPager(2, 5), WithOwner("piglei"))
ListApplications(WithPager(2, 5), WithOwner("piglei"), WithHasDeployed(false))
與使用“參數結構”相比,“功能選項”模型具有以下特點。
- 更友好的可選參數:例如,不再手動擷取
.hasDeployed
- 更大的靈活性:可以輕松地将附加邏輯附加到每個
功能With*
- 良好的前向相容性:添加任何新選項而不影響現有代碼
- prettier API:當參數結構複雜時,該模式提供的 API 更漂亮,更可用
但是,直接使用工廠函數實作的“功能選項”模式并不是非常使用者友好。因為每一個
With*
都是獨立的工廠函數,可能分布在不同的地方,調用者很難在一個地方找出該函數支援的所有選項。
為了解決這個問題,對“功能選項”模式進行了一些小的優化:用接口類型替換工廠函數。
6. 使用接口實作“功能選項”
首先,定義一個名為 的接口類型
Option
,它隻包含一個方法
applyTo
。
type Option interface {
applyTo(*ListAppsOptions)
}
然後,把這批
With*
工廠函數改成各自的自定義類型,實作
Option
接口。
type WithPager struct {
limit int
offset int
}
func (r WithPager) applyTo(opts *ListAppsOptions) {
opts.limit = r.limit
opts.offset = r.offset
}
type WithOwner string
func (r WithOwner) applyTo(opts *ListAppsOptions) {
opts.owner = string(r)
}
type WithHasDeployed bool
func (r WithHasDeployed) applyTo(opts *ListAppsOptions) {
val := bool(r)
opts.hasDeployed = &val
}
做好這些準備後,查詢功能應該做相應的調整。
// ListApplications Query application list, using variable arguments, Option interface type
func ListApplications(opts ...Option) []Application {
config := ListAppsOptions{limit: 10, offset: 0, owner: "", hasDeployed: nil}
for _, opt := range opts {
// Adjusting the call method
opt.applyTo(&config)
}
// ...
return allApps[config.offset : config.offset+config.limit]
}
調用代碼和上一個類似,如下。
ListApplications(WithPager{limit: 2, offset: 5}, WithOwner("piglei"))
ListApplications(WithOwner("piglei"), WithHasDeployed(false))
一旦将選項從工廠功能更改為
Option
接口,就可以更輕松地找到所有選項并使用 IDE
Find Interface Implementation
輕松完成工作。
問:我應該優先考慮“功能選項”嗎?
在檢視了這些參數傳遞模式之後,我們發現“功能選項”似乎在各個方面都是赢家。它可讀,相容,似乎應該是所有開發者的首選。而且它在 Go 社群中确實很受歡迎,活躍在許多流行的開源項目中(例如,AWS 的官方 SDK、Kubernetes 用戶端)。
- 需要更多不那麼簡單的代碼來實作
- 使用基于“功能選項”模式的 API 比使用簡單的“參數結構”更難使用者找到所有可用選項,并且需要更多的努力