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
接口,就可以更轻松地找到所有选项并使用 IDEFind Interface Implementation
轻松完成工作。
问:我应该优先考虑“功能选项”吗?
在查看了这些参数传递模式之后,我们发现“功能选项”似乎在各个方面都是赢家。它可读,兼容,似乎应该是所有开发者的首选。而且它在 Go 社区中确实很受欢迎,活跃在许多流行的开源项目中(例如,AWS 的官方 SDK、Kubernetes 客户端)。
“函数选项”确实比“正常传递”和“参数结构”有很多优点,但我们不能忽视缺点。
- 需要更多不那么简单的代码来实现
- 使用基于“功能选项”模式的 API 比使用简单的“参数结构”更难用户找到所有可用选项,并且需要更多的努力
总的来说,最简单的“普通参数传递”、“参数结构”和“函数选项”实现起来越来越困难和灵活,并且每种模式都有自己的适用场景。在设计 API 时,我们需要根据具体需求优先考虑更简单的方法,如果没有必要,不要引入更复杂的传递模式。