Configuring options in Go
Redowan Delowar
September 5, 2023
Suppose, you have a function that takes an option struct and a message as input. Then it
stylizes the message according to the option fields and prints it. What's the most sensible
API you can offer for users to configure your function? Observe:
In the src package, the function Display takes a pointer to a Style instance and a
msg string as parameters. Then it decorates the msg and prints it according to the style
specified in the option struct. In the wild, I've seen 3 main ways to write APIs that let
users configure options:
- Expose the option struct directly
- Use the option constructor pattern
- Apply functional option constructor pattern
Each comes with its own pros and cons.
Expose the option struct
In this case, you'd export the Style struct with all its fields and let the user configure
them directly. The previous snippet already made the struct and fields public. From another
package, you could import the src package and instantiate Style like this:
To configure option fields, mutate the values in place:
This works but will break users' code if new fields are added to the option struct. But your
users can instantiate the struct with named parameters to avoid breakage:
In this case, the field that wasn't passed would assume the corresponding zero value. For
instance, Bg will be initialized as an empty string. However, this pattern puts the
responsibility of retaining API compatibility on the users' shoulders. So if your code is
meant for external use, there are better ways to achieve option configurability.
Option constructor
Go standard library extensively uses this pattern. Instead of letting the users instantiate
Style directly, you expose a NewStyle constructor function that constructs the struct
instance for them:
It'll be used as follows:
If a new field is added to Style, update NewStyle to have a sensible default value for
it or initialize the struct with named parameters to set the optional fields to their
respective zero values. This avoids breaking users' code as long as the constructor
function's signature doesn't change.
In NewStyle, we implicitly set the value of Und to false but you can be explicit there
depending on your needs. The struct fields can be updated in the same manner as before:
This should cover most use cases. However, if you don't want to export the underlying option
struct, or your struct has tons of optional fields requiring extensibility, you'll need an
extra layer of indirection to avoid the need to accept a zillion config parameters in your
option constructor.
Functional option constructor
As mentioned at the tail of the last section, this approach works better when your struct
contains many optional fields and you need your users to be able to configure them if they
want. Go doesn't allow setting non-zero default values for struct fields. So an extra level
of indirection is necessary to let the users configure them. This approach also allows us to
make the option struct private so that there's no ambiguity around API usage.
Let's say style now has two optional fields und and zigzag that allows users to
decorate the message string with underlines or zigzagged lines:
Now, we'll define a new type called styleoption like this:
The styleoption function accepts a pointer to the option struct and updates a particular
field with a user-provided value. The implementation of this type would look as such:
Next, we'll need to define a higher order config function for each optional field in the
struct where the function will accept the field value and return another function with the
styleoption signature. The WithUnd and WithZigzag wrapper functions will be a part of
the public API that the users will use to configure style:
Finally, our option constructor function needs to be updated to accept variadic options.
Observe how we're looping through the options slice and applying the field config
functions to the struct pointer:
The users will use the code like this to instantiate style and update the optional fields:
The required fields fg and bg must be passed while constructing the option struct. The
optional fields can be configured with the field config functions like WithUnd and
WithZigzag.
The complete snippet looks as follows:
I first came across this pattern in [Rob Pike's post on self-referential functions].
Verdict
While the functional constructor pattern is the most intriguing one among the three, I
almost never reach for it unless I need my users to be able to configure large option
structs with many optional fields. It's rare and the extra indirection makes the code
inscrutable. Also, it renders the IDE suggestions useless.
In most cases, you can get away with exporting the option struct Stuff and a companion
function NewStuff to instantiate it. For another canonical example, see bufio.Read and
bufio.NewReader in the standard library.
Further reading
- [Functional options for friendly APIs - Dave Cheney]
- [Functional options pattern in Go - Matt Boyle]
[rob pike's post on self-referential functions]:
https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
[functional options for friendly apis - dave cheney]:
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
[functional options pattern in go - matt boyle]:
https://twitter.com/MattJamesBoyle/status/1698605808517288428
Discussion in the ATmosphere