Go 的几种函数参数传递方式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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(ListAppsOptionslimit: 5, offset: 0, owner: "piglei")
ListApplications(ListAppsOptionslimit: 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(ListAppsOptionslimit: 5, offset: 0, hasDeployed: true)
而当我们不需要通过“部署状态”进行过滤时,我们可以删除该hasDeployed
字段并ListApplications
使用以下代码调用该函数。
ListApplications(ListAppsOptionslimit: 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(ListAppsOptionslimit: 5, offset: 0)
当调用者想要过滤时hasDeployed
,可以使用以下。
wantHasDeployed := true
ListApplications(ListAppsOptionslimit: 5, offset: 0, hasDeployed: &wantHasDeployed)
在 golang 中,实际上可以通过以下方式快速创建一个非 nil 指针变量。
ListAppsOptionslimit: 5, offset: 0, hasDeployed: &[]booltrue[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 := ListAppsOptionslimit: 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 := ListAppsOptionslimit: 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(WithPagerlimit: 2, offset: 5, WithOwner("piglei"))
ListApplications(WithOwner("piglei"), WithHasDeployed(false))
一旦将选项从工厂功能更改为Option
接口,就可以更轻松地找到所有选项并使用 IDEFind Interface Implementation
轻松完成工作。
问:我应该优先考虑“功能选项”吗?
在查看了这些参数传递模式之后,我们发现“功能选项”似乎在各个方面都是赢家。它可读,兼容,似乎应该是所有开发者的首选。而且它在 Go 社区中确实很受欢迎,活跃在许多流行的开源项目中(例如,AWS 的官方 SDK、Kubernetes 客户端)。
“函数选项”确实比“正常传递”和“参数结构”有很多优点,但我们不能忽视缺点。
- 需要更多不那么简单的代码来实现
- 使用基于“功能选项”模式的 API 比使用简单的“参数结构”更难用户找到所有可用选项,并且需要更多的努力
总的来说,最简单的“普通参数传递”、“参数结构”和“函数选项”实现起来越来越困难和灵活,并且每种模式都有自己的适用场景。在设计 API 时,我们需要根据具体需求优先考虑更简单的方法,如果没有必要,不要引入更复杂的传递模式。
以上是关于Go 的几种函数参数传递方式的主要内容,如果未能解决你的问题,请参考以下文章