/
Go function argument patterns

Go function argument patterns

Naive style: “require-everything”

// 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.

Functional option style

MyFn("required arg", WithOptionalArg("foo"), WithAnotherArg("bar"))
  • Pros: Very nice interface at call-site.

  • Cons:

    • 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.

Structured option style

MyFn("required arg", MyFnArgs{ OptionalArg: "foo", AnotherArg: "bar", })
  • Pros: Handles optional args nearly as well as "functional option style" with much less boilerplate.
    No performance overhead if the struct is passed by-value.

  • Cons: Doesn't handle non-zero-valued defaulting as well.