The main benefit is you can have configuration options without having to specify all values, and also have non-zero-value defaults. Lets say you had something like Sarama's config struct which contains 50 or so config knobs. The following is will lead to some terrible defaults:
Here, with this config, there is a config option `MaxMessageBytes` which will be set to 0, which will reject all your messages. What Sarama does is, you can pass a `nil` config which will load defaults, or:
and so on. This is ok but can be cumbersome, especially if you just need to change one or 2 options or if some config options need to be initialized. Also someone can still do &Config{...} and shoot themselves in the foot. The functional options style is more concise.
I used to be a fan of this style, and I even have an ORM built around this style (ex. Query(WithID(4), WithFriends(), WithGroup(4))), but I think for options like these a Builder pattern is actually better if your intention is clarity.
The problem isn't that users are accidentally using it. People are using it intentionally.
The problem is that some of the config fields are mandatory, and some are optional, but the developer can't tell whether the fields were intentionally set to a weird value or just not specified, and users can't tell which fields need to be set to not have strange behavior.
You would need to add a Verified field for each config option so you can tell whether users set it or it's a default.
Alternately, you can use pointers, but that gets clunky because you can't do
conf := sarama.Connect(Config{ ClientID: &"foo"})
You have to declare a variable above, and then reference the var.
Zero values are sometimes valid config options, but not what most people actually want to do. Structs cause issues because there's no way to specify just a couple of fields in the struct; the rest will get zero values.
1. If the verified field is not set, your program blows up at runtime. I don't think it's acceptable for programs to blow up at runtime for errors that can be caught at compile time.
2. Your example still has the same problem when it comes to `MaxMessageBytes`, what if I really meant to set it to 0? The `WithDefaults` function would eat that. You could use a pointer, but then that just complicates the API (you need to do new(int) as a separate variable and assign it
func DoSomethingVerbosely(foo *Foo, verbosity int) {
prev := foo.Option(pkg.Verbosity(verbosity))
defer foo.Option(prev)
// ... do some stuff with foo under high verbosity.
}
but I don’t see why that’s better than
func DoSomethingVerbosely(foo *Foo, verbosity int) {
prev := foo.setVerbosity(verbosity))
defer foo.setVerbosity(prev)
// ... do some stuff with foo under high verbosity.
}
It also mentions that it allows you to “set lots of options in a given call”. That, you could sort of accomplish by having the setProperty methods return the changed object, thus allowing chaining (e.g. foo.setVerbosity(v).setDryRun(true)).
This allows both that defer and setting lots of options in a single call.
Given the limited features of go, it’s a nice hack, but I don’t like it. To me, it doesn’t feel like it fits the philosophy of go.
One could even add all the With methods to the struct to get some fluent/builder pattern.
Edit: is it so that you don’t need to instantiate a new struct at each call site?