// Bad: brittle, hard to understand.
MyFn("required arg", "foo", 0, nil, "bar", DefaultValue)
// Still bad: more readable, but still brittle. If an argument is added
// in the definition, or the order of arguments is changed,
// the call sites can become wrong without any compile error.
MyFn("required arg", "foo" /* argName1 */, 0 /* argName2 */,
nil /* argName3 */, "bar" /* argName4 */,
DefaultValue /* argName5 */)
Pros: Simplest implementation.
Cons: Verbose, fragile, confusing.
Conclusion: Great for a small number of naturally required args, when all arguments have different types.
Refactor away from this as soon as the argument list starts using multiple values of the same type.
Lots of supporting boilerplate makes code bloated and distracts from the actual implementation.
Clutters namespace with symbols useless outside the scope of the argument list.
Having identically-named arguments to multiple functions in the same namespace is tricky.
Large performance overhead due to mandatory heap allocations.
Conclusion: Since this may be the best caller experience, it's great for API and component boundaries, especially when the API exports only one or two functions so the namespace issues are less apparent. The boilerplate involved makes it overkill for more internal functions.
The performance overhead restricts applicability to functions that are only occasionally called.
Idea: It shouldn't be hard to target this convention using code-generation to adapt from the "structured option style". The boilerplate code could then live in its own file to be less distracting to engineers browsing the implementation.
Call() handles non-zero-value defaulting, making explicitly passing zero-values difficult.
It goes a little against the principle of "letting things be what they are." If a thing exists solely to be called, shouldn't that thing be a function instead of a struct? At the same time, Go offers better flexibility for defining struct values than it does for argument lists.