Once you've loaded it and mutated it for testing purposes or for copying from ENV vars into the config, you can then freeze it before passing it down to all your app level code.
Having this wrapper object that can be frozen and has a `get()` method to read JSON like data make it effectively not mutable.
BTW kudos on zanzibar. Love the tech and the code).
type Server struct { val bool }
type Config struct { Val bool }
func NewServer(... config *Config ...) http.Handler { if config == nil { config = &Config{} } return &Server{ val: config.Val } }
It took me a long time to settle on this pattern and I admit it's tedious to copy configuration over to the server struct, but I've found that it ends up being the least verbose and maintainable long term while making sure callers can't mutate config after the fact.
I can pass nil to NewServer to say "just the usual, please", customize everything, or surgically change a single option.
It's also useful for maintaining backwards compatibility. I'm free to refactor config on my server struct and "upgrade" deprecated config arguments inside my NewServer function.
Update: What's funny was, in our design the config object was kinda immutable. You have to use the WARNING_DO_NOT_USE api to make modification. We did mutate the object and we caused a sev
package config
type options struct {
name string
}
type Option func(o *options)
func Name(name string) Option {
return func(o *options) {
o.name = name
}
}
type Config struct {
opts *options
}
func New(opts ...Option) *Config {
o := &options{}
for _, option := range opts {
option(o)
}
return &Config{opts: o}
}
func (c *Config) Name() string {
return c.opts.name
}
Use it with: cfg := config.New(config.Name("Emanon"))
fmt.Println(cfg.Name())It's a lot of boilerplate to create something that's not actually immutable. It also makes it harder to figure out which options are available, since now you can't just look at the documentation of the type, you have to look at the whole module package to figure out what the various options are. If one of the fields is a slice or map you can just mutate that slice or map in place, so it's not really immutable. The pattern as Pike describes it has the benefit that supplying an option returns an option that reverses the effect of supplying the option so that you can use the options somewhat like Python context objects that have enter and exit semantics, but in practice I've found that to be useful in a small portion of situations.
Sure you can. Option func is a constructor for option type, and constructors are auto-included above methods in the docs.
PLS completion works for them as well.
- main constructor is easily available from the main type's docs,
- option type is easily available from the main constructor's docs,
- all option funcs are easily available from the option type's docs (because in fact these option funcs are constructors for the option type).
Excerpt from grpc godoc index:
type Server
func NewServer(opt ...ServerOption) *Server
...
...
type ServerOption
func ChainStreamInterceptor(interceptors ...StreamServerInterceptor) ServerOption
func ChainUnaryInterceptor(interceptors ...UnaryServerInterceptor) ServerOption
func ConnectionTimeout(d time.Duration) ServerOption
func Creds(c credentials.TransportCredentials) ServerOption
etc...
One more hop compared to a flat argument list, that's true. But if you only commonly use maybe 0-5 arguments out of 30-50 available, it does not look like a bad deal.How is not actually immutable? How could cfg.opts.name be modified after New() returns?
> It also makes it harder to figure out which options are available
I find it easier, the go tooling helps a lot. For example, all the options are grouped together at https://pkg.go.dev/github.com/go-kit/log/level#Option
It's also easy to use "Find Usages" in my editor, and filter on return type.
Functional options have some niceties - largely their ability to evolve without breaking changes - but as the GP points out, completely break discoverability with intellisense. Having to do some dance with filtering usage to find the options available is just worse than a static config struct with zero values that are meaningful for anything not set.
How do you create immutable structs in Go? I didn't think you could, which makes this more a Go problem than a `passing around Config object` problem.
(One of my pet peeves, coming to Go from C, is how little of stronger typing there actually is. In C, I pass and return const objects everywhere I can, my enums are not just ints because the compiler can warn when I forget one in a switch statement, etc).
one of my biggest pet peeves is when people take a Config object, which represents the configuration of an entire system, and pass it around mutably. When you do that, you're coupling everything together through the config object. I've worked on systems where you had to configure the parts in a specific order in order for things to work, because someone decided to write back to the config object when it was passed to them. Or another case was where I've seen it such that you couldn't disable a portion of the system because it wrote data into the config object that was read by some other subsystem later. The pattern of "your configuration is one big value, which is mutable" is one of the more annoying patterns that I've seen before, both in Go and in other languages.