函数参数列表

不要让一个函数的签名变得太长。当一个函数中的参数越多,单个参数的作用就越不明确,同一类型的相邻参数就越容易混淆。有大量参数的函数不容易被记住,在调用点也更难读懂。

在设计 API 时,可以考虑将一个签名越来越复杂的高配置函数分割成几个更简单的函数。如果有必要的话,这些函数可以共享一个(未导出的)实现。

当一个函数需要许多输入时,可以考虑为一些参数引入一个 option 模式,或者采用更高级的变体选项技术。选择哪种策略的主要考虑因素应该是函数调用在所有预期的使用情况下看起来如何。

下面的建议主要适用于导出的 API,它比未导出的 API 的标准要高。这些技术对于你的用例可能是不必要的。使用你的判断,并平衡清晰性和最小机制的原则。

也请参见。Go技巧#24:使用特定案例的结构

option 模式

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 模式:

  • 所有调用者都需要指定一个或多个选项。
  • 大量的调用者需要提供许多选项。
  • 用户要调用的多个函数之间共享这些选项。

可变 option 模式

使用可变 option 模式,可以创建导出的函数,其返回的闭包可以传递给函数的variadic(...)参数。该函数将选项的值作为其参数(如果有的话),而返回的闭包接受一个可变的引用(通常是一个指向结构体类型的指针),该引用将根据输入进行更新。

使用可变 option 模式可以提供很多好处。

  • 当不需要配置时,选项在调用点不占用空间。
  • 选项仍然是值,所以调用者可以共享它们,编写帮助程序,并积累它们。
  • 选项可以接受多个参数(例如:cartesian.Translate(dx, dy int) TransformOption)。
  • 选项函数可以返回一个命名的类型,以便在 godoc 中把选项组合起来。
  • 包可以允许(或阻止)第三方包定义(或不定义)自己的选项。

注意:使用可变 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的演讲,以更深入地了解这些选项的使用方法。