天天看點

Go 的幾種函數參數傳遞方式

一般傳遞

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 比使用簡單的“參數結構”更難使用者找到所有可用選項,并且需要更多的努力