#golang#design-patterns#clean-code#backend

Idiomatic Go: Mastering the Functional Options Pattern

"Master the Functional Options pattern in Go to build flexible, backward-compatible, and idiomatic APIs without constructor explosion."

By huud
0 views
~3 min read
Idiomatic Go: Mastering the Functional Options Pattern

Idiomatic Go: Mastering the Functional Options Pattern

Go lacks method overloading, default parameter values, and traditional constructors. Developers often resort to anti-patterns when initializing complex structs: constructor functions with dozens of arguments, or unwieldy Config structs full of nil checks.

The Functional Options pattern solves this elegantly. It provides clean APIs, sensible defaults, and backward compatibility.


The Problem: Constructor Parameter Explosion

Consider a network client struct:

go
type Client struct {
    timeout time.Duration
    retries int
    logger  *log.Logger
    tls     bool
}

Anti-Pattern 1: Telescoping Constructors

go
func NewClient(timeout time.Duration) *Client { ... }
func NewClientWithRetries(timeout time.Duration, retries int) *Client { ... }
func NewClientWithLogger(timeout time.Duration, retries int, logger *log.Logger) *Client { ... }

Breaks as options grow. Pollutes package namespace.

Anti-Pattern 2: The Plain Config Struct

go
type Config struct {
    Timeout time.Duration
    Retries int
    Logger  *log.Logger
    TLS     bool
}

func NewClient(cfg Config) *Client { ... }

Problem: Cannot distinguish zero values (0, false, nil) from unset fields without pointers everywhere.


The Solution: Functional Options

Define an option type as a function signature that mutates the struct:

go
package client

import (
	"log"
	"time"
)

type Client struct {
	timeout time.Duration
	retries int
	logger  *log.Logger
	tls     bool
}

// Option defines a functional configuration signature.
type Option func(*Client)

// WithTimeout sets request timeout.
func WithTimeout(d time.Duration) Option {
	return func(c *Client) {
		c.timeout = d
	}
}

// WithRetries configures retry count.
func WithRetries(n int) Option {
	return func(c *Client) {
		c.retries = n
	}
}

// WithLogger injects custom logger.
func WithLogger(l *log.Logger) Option {
	return func(c *Client) {
		c.logger = l
	}
}

// WithTLS enables secure connection.
func WithTLS(enabled bool) Option {
	return func(c *Client) {
		c.tls = enabled
	}
}

Constructor Implementation

Set defaults first. Apply variadic options after:

go
const (
	defaultTimeout = 30 * time.Second
	defaultRetries = 3
)

func New(opts ...Option) *Client {
	// 1. Set sane production defaults
	c := &Client{
		timeout: defaultTimeout,
		retries: defaultRetries,
		logger:  log.Default(),
		tls:     true,
	}

	// 2. Apply custom overrides
	for _, opt := range opts {
		opt(c)
	}

	return c
}

Usage at Call Sites

Clean and readable:

go
// Default client
c1 := client.New()

// Custom timeout and retries only
c2 := client.New(
	client.WithTimeout(5*time.Second),
	client.WithRetries(5),
)

Handling Validation & Errors

If option validation can fail, return an error from the closure:

go
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 New(opts ...Option) (*Client, error) {
	c := &Client{timeout: defaultTimeout}
	for _, opt := range opts {
		if err := opt(c); err != nil {
			return nil, fmt.Errorf("client configuration failed: %w", err)
		}
	}
	return c, nil
}

Summary & Trade-offs

Factor Functional Options Config Struct
API Cleanliness Excellent Average
Default Handling Automatic Requires manual merge
API Evolution Non-breaking May require struct changes
Overhead Heap allocation per option closure Stack allocation

Rule of thumb: Use Plain Structs for internal/unexported code. Use Functional Options for exported public libraries and core infrastructure clients.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.