Functional Options Pattern
The functional options pattern configures complex types through a variadic list of Option functions passed to a constructor.
Search across all documentation pages
The functional options pattern configures complex types through a variadic list of Option functions passed to a constructor.
It replaces telescoping New overloads and mutable builder structs with composable, order-independent settings.
Functional options model each configuration knob as func(*T) applied inside New.
Callers pass only the settings they care about; the constructor applies defaults first, then runs each option.
The pattern is idiomatic for libraries and infrastructure clients where many orthogonal settings exist and binary compatibility matters.
It trades a small amount of boilerplate for APIs that stay readable as they grow.
Quick-reference recipe card - copy-paste ready.
type Option func(*Client) error
func WithTimeout(d time.Duration) Option {
return func(c *Client) error {
if d <= 0 { return errors.New("timeout must be positive") }
c.timeout = d
return nil
}
}
func NewClient(opts ...Option) (*Client, error) {
c := &Client{timeout: 30 * time.Second}
for _, opt := range opts {
if err := opt(c); err != nil { return nil, err }
}
return c, nil
}When to reach for this:
package weather
import (
"context"
"errors"
"fmt"
"net/http"
"time"
)
type Client struct {
base string
http *http.Client
apiKey string
}
type Option func(*Client) error
func WithBaseURL(url string) Option {
return func(c *Client) error {
if url == "" { return errors.New("weather: base URL required") }
c.base = url
return nil
}
}
func WithAPIKey(key string) Option {
return func(c *Client) error {
if key == "" { return errors.New("weather: API key required") }
c.apiKey = key
return nil
}
}
func WithHTTPClient(hc *http.Client) Option {
return func(c *Client) error {
if hc == nil { return errors.New("weather: http client required") }
c.http = hc
return nil
}
}
func New(opts ...Option) (*Client, error) {
c := &Client{
base: "https://api.weather.example",
http: &http.Client{Timeout: 10 * time.Second},
}
for _, opt := range opts {
if err := opt(c); err != nil {
return nil, err
}
}
if c.apiKey == "" {
return nil, errors.New("weather: API key required")
}
return c, nil
}
func (c *Client) Forecast(ctx context.Context, city string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
fmt.Sprintf("%s/v1/forecast?city=%s", c.base, city), nil)
if err != nil { return "", err }
req.Header.Set("Authorization", "Bearer "+c.apiKey)
resp, err := c.http.Do(req)
if err != nil { return "", err }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("weather: status %d", resp.StatusCode)
}
return "sunny", nil
}
func main() {
client, err := New(
WithAPIKey("dev-key"),
WithHTTPClient(&http.Client{Timeout: 5 * time.Second}),
)
if err != nil { panic(err) }
fmt.Println(client.Forecast(context.Background(), "Austin"))
}What this demonstrates:
base, http.Client timeout) live inside NewWith* function validates its own inputNew enforces cross-field rules (API key required)Option is a function type closed over configuration valuesNew allocates the target, sets defaults, then applies opts in orderWith* helpers; keep Option unexported when possible| Scenario | Behavior | Recommendation |
|---|---|---|
| Independent settings | Order does not matter | Document as order-independent |
| Last wins | Later option overwrites earlier | Document explicitly or reject duplicates |
| Mutually exclusive TLS modes | Invalid combination | Return error from second option or from New |
| Variant | Signature | Use When |
|---|---|---|
| Silent options | func(*T) | Simple settings, panic-free validation in New |
| Validating options | func(*T) error | Per-option validation failures |
| Functional options struct | Exported Config + New(Config) | All fields always provided together |
// Prefer unexported Option type at package scope.
type option func(*Server)
// Export constructors for discoverability.
func WithAddr(addr string) option { /* ... */ }( *T, error) from New when validation can failWithTimeout, not SetTimeout (mutation implied)New and invariants break. Fix: keep config fields unexported.New(a, b) is clearer than New(WithA(a), WithB(b)) for trivial types. Fix: reserve options for 3+ orthogonal settings.WithHTTPClient(nil) panics on use. Fix: validate non-nil in the option function.WithTTL to mean a different field breaks callers silently. Fix: add new With* functions; deprecate old names across releases.| Alternative | Use When | Don't Use When |
|---|---|---|
| Config struct parameter | All fields set together, few optional knobs | Many optional orthogonal settings |
| Builder struct with methods | Fluent API for internal DSLs | Public library API (harder to evolve) |
| Functional options | Library constructors with defaults | One or two required parameters only |
| Environment variables only | CLI tools, twelve-factor apps | Libraries consumed by other modules |
The pattern appears in early Go blog posts and is used widely in google.golang.org/grpc, OpenTelemetry, and stdlib-adjacent libraries.
It matches Go's preference for functions over inheritance.
Usually no - export With* functions and keep type option func(*T) unexported so callers cannot forge options that bypass validation.
Design independent options to commute.
When order matters, document it or detect conflicts and return errors.
Table-test New with option subsets: defaults only, single override, invalid option, conflicting options.
Keep options cheap; defer heavy work (TLS cert loading) to New after all options apply, or lazy-init on first use.
Config structs group required fields; options shine when most fields have defaults and callers override a few.
There is no fixed limit - grpc has dozens.
Group related settings behind nested option helpers if names clutter godoc.
Go 1.18+ can type-parameterize helpers, but the classic Option func(*T) pattern remains the community default for constructors.
Return an interface or opaque type from New when the concrete struct must stay hidden.
Return error from New or options for library code.
Reserve panic for programmer errors in internal packages only.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026