TL;DR / Executive Summary
When creating APIs or constructors in Go, we often face the challenge of handling multiple optional parameters. This can lead to messy code with multiple constructor functions, confusing nil arguments, or cumbersome configuration structs. The functional options pattern provides an elegant solution by allowing users to pass a variable number of self-describing functions that configure an object, resulting in a clean, readable, and highly extensible API.
The Problem / Context
Imagine you’re designing a component. Initially, it’s simple, so your constructor is straightforward: NewComponent(db *sql.DB). But soon, requirements evolve. You need to add an optional logger, then a configurable timeout, then a custom cache size.
How do you handle this?
- Multiple Constructors?
NewComponent(db),NewComponentWithLogger(db, logger),NewComponentWithTimeout(db, timeout)… This quickly becomes unmanageable and violates the DRY (Don’t Repeat Yourself) principle. - A Long List of Arguments?
NewComponent(db, logger, timeout, cacheSize, retries). This is brittle. What if a user wants to setretriesbut not the logger or timeout? They might have to passnilfor several arguments:NewComponent(db, nil, nil, 0, 5). This is ugly and error-prone. - A Configuration Struct?
NewComponent(db, Config{Logger: log, Timeout: 5}). This is a better approach and very common in Go. However, it has drawbacks. It can be verbose for the caller, and it’s not always clear what the zero-value for a field means. Is aTimeoutof0intentional, or was it simply omitted?
There must be a better way.
The Solution: Functional Options
Inspired by the legendary post “Functional options for friendly APIs” by Dave Cheney, this pattern offers a more robust and “friendly” alternative.
Let’s walk through a practical example from a small “Choose Your Own Adventure” web application I built. The core of the app is an http.Handler that serves story chapters. I needed a way to construct this handler, allowing for future customizations without breaking the existing API.
Here is the core setup:
// handler holds the dependencies for our web handler.
// Note that the fields are unexported.
type handler struct {
s Story
t *template.Template
fn func(r *http.Request) string
}
// NewHandler is the constructor for our handler.
// It takes the required Story object and any number of options.
func NewHandler(s Story, opts ...HandlerOptions) http.Handler {
// Start with sensible defaults.
h := handler{
s: s,
t: defaultTemplate,
fn: defaultPathFn,
}
// Apply all the options passed by the user.
// Each option is a function that modifies the handler.
for _, opt := range opts {
opt(&h)
}
return h
}
The magic lies in the HandlerOptions type and the opts ...HandlerOptions variadic parameter.
The
HandlerOptionsTypeIt’s simply a function type that takes a pointer to the object being configured.
type HandlerOptions func(h *handler)The “With” Functions
Next, we create exported functions (our “options”) that return a function of type
HandlerOptions. These are the building blocks of our friendly API. For instance, if I want to allow the user to provide a custom HTML template:func WithTemplate(t *template.Template) HandlerOptions { return func(h *handler) { h.t = t } }This
WithTemplatefunction captures the desired template in a closure and returns a function that, when executed, will assign that template to ourhandlerinstance.Putting It All Together
Now, the caller has a beautiful, self-documenting API.
To create a handler with just the defaults, it’s simple:
// Uses the default template h := NewHandler(myStory)To create a handler with a custom template, it’s equally simple and highly readable:
// Uses a custom template customTmpl := template.Must(template.ParseFiles("./custom.html")) h := NewHandler(myStory, WithTemplate(customTmpl))
Architectural Takeaways
Adopting this pattern brings several key benefits:
- Readability and Discoverability: The calling code becomes self-documenting.
NewHandler(story, WithTemplate(t))clearly states its intent. Your IDE’s autocomplete will also help users discover available options by typingWith.... - Extensibility: Adding a new option is trivial. You just create a new
With...function. This requires no changes to theNewHandlerconstructor signature, ensuring you don’t break existing user code. - No More
nilHell: Users only provide the options they care about. The order doesn’t matter, and there are no confusingnilplaceholders for options they wish to skip. - Sane Defaults: Your constructor is the single source of truth for default values, making them easy to manage. The user only needs to intervene when the defaults aren’t right for their use case.
By embracing functional options, you can create Go APIs that are not only powerful but also a pleasure to use, guiding your users towards correct and maintainable code.