tezvyn:

Go's Functional Options Pattern for Flexible APIs

AI-drafted, machine-checkedSource: golang.designintermediate

The functional options pattern uses functions to set optional struct fields, making APIs flexible and readable. It's common for complex constructors like servers or DB clients.

WHY IT EXISTS Go lacks named arguments and function overloading. This makes initializing a struct with many optional fields messy, often leading to constructors with long, confusing argument lists or requiring users to manually build a config struct. The functional options pattern provides a clean, extensible, and self-documenting API for this exact problem.

THE MENTAL MODEL Think of it as giving a list of instruction cards to a builder. Instead of a rigid blueprint, you hand over cards like "UsePort(8080)" or "WithTimeout(5s)". The builder applies whichever cards you provide, in any order, and uses sensible defaults for anything you didn't specify. This makes the initialization process flexible and the calling code highly readable.

HOW IT WORKS First, you define an Option type as a function that accepts a pointer to your struct, e.g., type Option func(*Server). Second, you create exported functions that return these Option functions. For instance, func WithPort(p int) Option { return func(s *Server) { s.port = p } }. Finally, your constructor, like NewServer, accepts a variable number of options (...Option), creates a default struct, and then iterates through the provided options, applying each one to the struct before returning it.

WHEN TO USE IT This pattern is ideal for public library APIs where you need to initialize complex objects. It allows you to add new configuration options in the future without breaking existing user code. Deprecating an option is as simple as making its function a no-op. The call site becomes very clear: NewClient(WithRetries(3), WithLogger(log.Default())).

WHEN NOT TO USE IT For simple structs with few or no optional fields, this pattern is overkill and adds unnecessary boilerplate. The main drawback is the verbosity and code duplication required when multiple different structs need similar options; each struct requires its own distinct Option type and a full set of option-generating functions.

ONE CANONICAL EXAMPLE To create a server object, a user would call a constructor with a variable number of options: srv := NewServer(WithPort(8080), WithHost("localhost")). The NewServer function accepts ...Option, creates a default server struct, and then loops over the provided options to configure the instance. A call with no arguments, NewServer(), would simply return a server with all default values.

Read the original → golang.design

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.