不要让一个函数的签名变得太长。当一个函数中的参数越多,单个参数的作用就越不明确,同一类型的相邻参数就越容易混淆。有大量参数的函数不容易被记住,在调用点也更难读懂。
在设计 API 时,可以考虑将一个签名越来越复杂的高配置函数分割成几个更简单的函数。如果有必要的话,这些函数可以共享一个(未导出的)实现。
当一个函数需要许多输入时,可以考虑为一些参数引入一个 option 模式,或者采用更高级的变体选项技术。选择哪种策略的主要考虑因素应该是函数调用在所有预期的使用情况下看起来如何。
下面的建议主要适用于导出的 API,它比未导出的 API 的标准要高。这些技术对于你的用例可能是不必要的。使用你的判断,并平衡清晰性和最小机制的原则。
也请参见。Go技巧#24:使用特定案例的结构
option 模式是一种结构类型,它收集了一个函数或方法的部分或全部参数,然后作为最后一个参数传递给该函数或方法。(该结构只有在导出的函数中使用时,才应该导出)。
使用 option 模式有很多好处。
结构体字面量包括每个参数的字段和值,这使得它们可以自己作为文档,并且更难被交换。
不相关的或 “默认 “的字段可以被省略。
调用者可以共享 option 模式,并编写帮助程序对其进行操作。
与函数参数相比,结构体提供了更清晰的每个字段的文档。
option 模式可以随着时间的推移而增长,而不会影响到调用点。
下面是一个可以改进的函数的例子:
// Bad:func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { // ...}
上面的函数可以用一个 option 模式重写如下:
// Good:type ReplicationOptions struct { Config *replicator.Config PrimaryRegions []string ReadonlyRegions []string ReplicateExisting bool OverwritePolicies bool ReplicationInterval time.Duration CopyWorkers int HealthWatcher health.Watcher}func EnableReplication(ctx context.Context, opts ReplicationOptions) { // ...}
然后,该函数可以在不同的包中被调用:
// Good:func foo(ctx context.Context) { // Complex call: storage.EnableReplication(ctx, storage.ReplicationOptions{ Config: config, PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, ReadonlyRegions: []string{"us-east5", "us-central6"}, OverwritePolicies: true, ReplicationInterval: 1 * time.Hour, CopyWorkers: 100, HealthWatcher: watcher, }) // Simple call: storage.EnableReplication(ctx, storage.ReplicationOptions{ Config: config, PrimaryRegions: []string{"us-east1", "us-central2", "us-west3"}, })}
当遇到以下某些情况时,通常首选 option 模式:
使用可变 option 模式,可以创建导出的函数,其返回的闭包可以传递给函数的variadic(...
)参数。该函数将选项的值作为其参数(如果有的话),而返回的闭包接受一个可变的引用(通常是一个指向结构体类型的指针),该引用将根据输入进行更新。
使用可变 option 模式可以提供很多好处。
cartesian.Translate(dx, dy int) TransformOption
)。注意:使用可变 option 模式需要大量的额外代码(见下面的例子),所以只有在好处大于坏处时才可以使用。
下面是一个可以改进的功能的例子:
// Bad:func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) { ...}
上面的例子可以用可变 option 模式重写如下:
// Good:type replicationOptions struct { readonlyCells []string replicateExisting bool overwritePolicies bool replicationInterval time.Duration copyWorkers int healthWatcher health.Watcher}// A ReplicationOption configures EnableReplication.type ReplicationOption func(*replicationOptions)// ReadonlyCells adds additional cells that should additionally// contain read-only replicas of the data.//// Passing this option multiple times will add additional// read-only cells.//// Default: nonefunc ReadonlyCells(cells ...string) ReplicationOption { return func(opts *replicationOptions) { opts.readonlyCells = append(opts.readonlyCells, cells...) }}// ReplicateExisting controls whether files that already exist in the// primary cells will be replicated. Otherwise, only newly-added// files will be candidates for replication.//// Passing this option again will overwrite earlier values.//// Default: falsefunc ReplicateExisting(enabled bool) ReplicationOption { return func(opts *replicationOptions) { opts.replicateExisting = enabled }}// ... other options ...// DefaultReplicationOptions control the default values before// applying options passed to EnableReplication.var DefaultReplicationOptions = []ReplicationOption{ OverwritePolicies(true), ReplicationInterval(12 * time.Hour), CopyWorkers(10),}func EnableReplication(ctx context.Context, config *placer.Config, primaryCells []string, opts ...ReplicationOption) { var options replicationOptions for _, opt := range DefaultReplicationOptions { opt(&options) } for _, opt := range opts { opt(&options) }}
然后,该函数可以在不同的包中被调用:
// Good:func foo(ctx context.Context) { // Complex call: storage.EnableReplication(ctx, config, []string{"po", "is", "ea"}, storage.ReadonlyCells("ix", "gg"), storage.OverwritePolicies(true), storage.ReplicationInterval(1*time.Hour), storage.CopyWorkers(100), storage.HealthWatcher(watcher), ) // Simple call: storage.EnableReplication(ctx, config, []string{"po", "is", "ea"})}
当遇到很多以下情况时,首选可变 option 模式:
这种风格的选项应该接受参数,而不是在命名中标识来表示它们的价值;后者会使参数的动态组合变得更加困难。例如,二进制设置应该接受一个布尔值(例如,rpc.FailFast(enable bool)
比rpc.EnableFailFast()
更合适)。枚举的选项应该接受一个枚举的常数(例如log.Format(log.Capacitor)
比log.CapacitorFormat()
更好)。另一种方法使那些必须以编程方式选择传递哪些选项的用户更加困难;这种用户被迫改变参数的实际组成,而不是改变参数到选项。不要假设所有的用户都会知道全部的选项。
一般来说,option 应该被按顺序处理。如果有冲突或者一个非累积的选项被多次传递,将应用最后一个参数。
在这种模式下,选项函数的参数通常是未导出的,以限制选项只在包本身内定义。这是一个很好的默认值,尽管有时允许其他包定义选项也是合适的。
参见Rob Pike 的原始博文和Dave Cheney的演讲,以更深入地了解这些选项的使用方法。