Go Adapter Pattern Part 1: Functional Variadic Arguments
In the next couple posts I’m going to show how to use the “adapter pattern” in Go. This is very handy pattern that allows for flexibility while keeping your APIs clean and readable. The inspiration for this post comes from a few blog posts I’ve read, but mainly Dave Cheney’s functional options post here.
Suppose you have some struct type:
type Foo struct {
id string
}
You might think to create a NewFoo
constructor that takes a value for bar
and sets it on the instance. That’s fine if that’s all you’ll ever do, but if you want to add more fields (bar
, baz
, etc.) it quickly becomes annoying to maintain.
Instead, you might think to enumerate all possible factory functions callers could need (e.g., NewFooWithBar
), or inject some configuration struct as a dependency (e.g., NewFoo(c Config)
), but these have significant downsides (take a look at Dave Cheney’s blog post for a deeper explanation about the downsides).
Rather than accepting a “typical” configuration depenency, consider accepting variadic functional arguments (i.e., NewFoo(...opts func(*Foo) error)
). This lets us accept some arbitrary number of configuration options (including zero!), while preserving clean API that doesn’t have any potentially surprising behavior.
Going one step further, we can use closures when defining our configuration functions, so we can export a well defined set of configuration options that make things easy for external callers. This might look something like:
type Foo struct {
id string
bar string
}
func NewFoo(id string, ...opts func(*Foo) error) *Foo {
f := &Foo{requiredBar: requiredBar}
for _, opt := range opts {
err = opt(f)
// handle err
}
return f
}
func WithBar(b string) func(*Foo) error {
return func(f *Foo) error {
f.bar = b
}
}
func main() {
f := NewFoo("123")
fb := NewFoo("123", WithBar("4"))
}
That’s all for this post. In the next post I’ll show how this can be adapted slightly to work nicely with the net/http
package to do some neat middleware things.