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?

  1. Multiple Constructors? NewComponent(db), NewComponentWithLogger(db, logger), NewComponentWithTimeout(db, timeout)… This quickly becomes unmanageable and violates the DRY (Don’t Repeat Yourself) principle.
  2. A Long List of Arguments? NewComponent(db, logger, timeout, cacheSize, retries). This is brittle. What if a user wants to set retries but not the logger or timeout? They might have to pass nil for several arguments: NewComponent(db, nil, nil, 0, 5). This is ugly and error-prone.
  3. 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 a Timeout of 0 intentional, 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.

  1. The HandlerOptions Type

    It’s simply a function type that takes a pointer to the object being configured.

    type HandlerOptions func(h *handler)
    
  2. 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 WithTemplate function captures the desired template in a closure and returns a function that, when executed, will assign that template to our handler instance.

  3. 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 typing With....
  • Extensibility: Adding a new option is trivial. You just create a new With... function. This requires no changes to the NewHandler constructor signature, ensuring you don’t break existing user code.
  • No More nil Hell: Users only provide the options they care about. The order doesn’t matter, and there are no confusing nil placeholders 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.