Zig often uses “Options bag” pattern (is it official name?). Lately, I’ve been using it in Go.
Go options bag
package fediverse
type NewClientArgs struct{
HTTPClient *http.Client
UseV2 *bool
FluentOpts []fluent.Opts
}
func (nca NewClientArgs) WithDefaults() NewClientArgs {
if nca.HTTPClient == nil {
nca.HTTPClient = http.DefaultClient
}
if nca.UseV2 == nil{
nca.UseV2 = new(viper.GetBool("some_env_var"))
}
if *nca.UseV2{
nca.FluentOpts = append(nca.FluentOpts, fluent.WithSomethingCool, fluent.WithNewGen)
}
return nca
}
type Client struct{}
func NewClient(url *url.URL, args NewClientArgs) *Client {
args = args.WithDefaults()
return &Client{
// set fields
// client: args.HTTPClient, // etc.
}
}
I like this more: all options are in one place; they can be easily logged; order of initialization is defined and not random.
I think, NewClientOpts name may be more suitable for the case where all fields are allowed to be in zero/default state. But in cases where you need to have required fields in the struct name it ~Args.
Mutating options
In my opinion, Go ecosystem has more than enough argument passing pattern where you pass a slice of interfaces or functions that are going to mutate the state.
type WithOpt func(*privateStruct)
func WithInsanity(level int) WithOpt
func WithPrice(price uint) WithOpt
type privateStruct struct{
// state
}
func New(opts ...WithOpt) *privateStruct {
v := &privateStruct{}
for _, opt := range opts {
opt(v)
}
return v
}
I do not like this approach. We’ve lost: ordering guarantee; easy logging of options; potentially With~ functions may be in different files.
Validation
In case if you need required fields, add Validate method.
type Args struct{
IsWater *bool
IsLiquid *bool
CanCatchFire *bool
}
func (a Args) Validate() error {
var errs []error
if a.IsWater != nil && a.CanCatchFire != nil{
errs = append(errs, errors.New("water is not compatible with fire"))
}
return errors.Join(errs...)
}