Configuring options in Go

Redowan Delowar September 5, 2023
Source
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

Loading comments...