Function argument lists
source ↗Function argument lists
Don’t let the signature of a function get too long. As more parameters are added to a function, the role of individual parameters becomes less clear, and adjacent parameters of the same type become easier to confuse. Functions with large numbers of arguments are less memorable and more difficult to read at the call-site.
When designing an API, consider splitting a highly configurable function whose signature is growing complex into several simpler ones. These can share an (unexported) implementation if necessary.
Where a function requires many inputs, consider introducing an option struct for some of the arguments or employing the more advanced variadic options technique. The primary consideration for which strategy to choose should be how the function call looks across all expected use cases.
The recommendations below primarily apply to exported APIs, which are held to a higher standard than unexported ones. These techniques may be unnecessary for your use case. Use your judgment, and balance the principles of clarity and least mechanism.
See also: Go Tip #24: Use Case-Specific Constructions
Option structure
An option structure is a struct type that collects some or all of the arguments of a function or method, that is then passed as the last argument to the function or method. (The struct should be exported only if it is used in an exported function.)
Using an option structure has a number of benefits:
- The struct literal includes both fields and values for each argument, which makes them self-documenting and harder to swap.
- Irrelevant or “default” fields can be omitted.
- Callers can share the option struct and write helpers to operate on it.
- Structs provide cleaner per-field documentation than function arguments.
- Option structs can grow over time without impacting call-sites.
Here is an example of a function that could be improved:
// Bad:
func EnableReplication(ctx context.Context, config *replicator.Config, primaryRegions, readonlyRegions []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
// ...
}
The function above could be rewritten with an option structure as follows:
// 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) {
// ...
}
The function can then be called in a different package:
// 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"},
})
}
Note: Contexts are never included in option structs.
This option is often preferred when some of the following apply:
- All callers need to specify one or more of the options.
- A large number of callers need to provide many options.
- The options are shared between multiple functions that the user will call.
Variadic options
Using variadic options, exported functions are created which return closures
that can be passed to the variadic (...) parameter of a function. The
function takes as its parameters the values of the option (if any), and the
returned closure accepts a mutable reference (usually a pointer to a struct
type) that will be updated based on the inputs.
Using variadic options can provide a number of benefits:
- Options take no space at a call-site when no configuration is needed.
- Options are still values, so callers can share them, write helpers, and accumulate them.
- Options can accept multiple parameters (e.g.
cartesian.Translate(dx, dy int) TransformOption). - The option functions can return a named type to group options together in godoc.
- Packages can allow (or prevent) third-party packages to define (or from defining) their own options.
Note: Using variadic options requires a substantial amount of additional code (see the following example), so it should only be used when the advantages outweigh the overhead.
Here is an example of a function that could be improved:
// Bad:
func EnableReplication(ctx context.Context, config *placer.Config, primaryCells, readonlyCells []string, replicateExisting, overwritePolicies bool, replicationInterval time.Duration, copyWorkers int, healthWatcher health.Watcher) {
...
}
The example above could be rewritten with variadic options as follows:
// 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: none
func 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: false
func 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)
}
}
The function can then be called in a different package:
// 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"})
}
Prefer this option when many of the following apply:
- Most callers will not need to specify any options.
- Most options are used infrequently.
- There are a large number of options.
- Options require arguments.
- Options could fail or be set incorrectly (in which case the option function
returns an
error). - Options require a lot of documentation that can be hard to fit in a struct.
- Users or other packages can provide custom options.
Options in this style should accept parameters rather than using presence to
signal their value; the latter can make dynamic composition of arguments much
more difficult. For example, binary settings should accept a boolean (e.g.
rpc.FailFast(enable bool) is preferable to rpc.EnableFailFast()). An
enumerated option should accept an enumerated constant (e.g.
log.Format(log.Capacitor) is preferable to log.CapacitorFormat()). The
alternative makes it much more difficult for users who must programmatically
choose which options to pass; such users are forced to change the actual
composition of the parameters rather than simply changing the arguments to the
options. Don’t assume that all users will know the full set of options
statically.
In general, options should be processed in order. If there is a conflict or if a non-cumulative option is passed multiple times, the last argument should win.
The parameter to the option function is generally unexported in this pattern, to restrict the options to being defined only within the package itself. This is a good default, though there may be times when it is appropriate to allow other packages to define options.
See Rob Pike’s original blog post and Dave Cheney’s talk for a more in-depth look at how these options can be used.