Let’s be honest: you’ve seen this before. You’re trying to create a Thing, and its constructor is a nightmare. You either have a function with fourteen arguments where the last nine are almost always the same, or you’ve got a dozen different NewThingWithXAndYButNotZ constructors. It’s a mess. It’s un-Go-like. It’s exactly the kind of ceremony we’re trying to avoid.

Enter the Functional Options pattern. This is one of those patterns that looks like magic the first time you see it, but once you understand it, you’ll wonder how you ever lived without it. The core idea is simple: we pass a variadic slice of functions to our constructor, and each function operates on the struct we’re building. It’s configuration with a functional flair.

The Problem We’re Solving

Imagine we’re building a new server type. We might be tempted to write a constructor like this:

// Don't do this. This is a trap.
func NewServer(host string, port int, timeout time.Duration, maxConnections int, protocol string, tlsConfig *tls.Config) (*Server, error) {
    // ... validation and setup
}

This is awful for the caller. What if they just want a server on localhost:8080 and don’t care about the other settings? They have to look up the zero values for time.Duration and int, pass an empty string for protocol, and pass nil for the TLS config. It’s error-prone and ugly. The functional options pattern solves this elegantly.

The Anatomy of a Functional Option

First, we define our struct with sensible defaults. Then, we define an option type: a function that takes a pointer to that struct and modifies it. Finally, our constructor accepts a variadic number of these option functions.

type Server struct {
    Host        string
    Port        int
    Timeout     time.Duration
    MaxConns    int
    TLSConfig   *tls.Config
}

// Option is the type that makes it all work. It's a function that configures a Server.
type Option func(*Server)

// Our constructor now only requires the absolute essentials.
func NewServer(host string, port int, options ...Option) (*Server, error) {
    // Start with a server configured with sensible defaults.
    srv := &Server{
        Host:     host,
        Port:     port,
        Timeout:  30 * time.Second, // A reasonable default
        MaxConns: 100,              // Another reasonable default
        TLSConfig: nil,              // No TLS by default
    }

    // Apply all the option functions to the server instance.
    for _, option := range options {
        option(srv)
    }

    // We could add validation here, e.g., if Port is out of range.
    if srv.Port < 1 || srv.Port > 65535 {
        return nil, fmt.Errorf("invalid port number: %d", srv.Port)
    }

    return srv, nil
}

Now, how do we create these Option functions? We write public constructor functions for them.

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.Timeout = timeout
    }
}

func WithMaxConnections(max int) Option {
    return func(s *Server) {
        s.MaxConns = max
    }
}

func WithTLSConfig(cfg *tls.Config) Option {
    return func(s *Server) {
        s.TLSConfig = cfg
    }
}

Using the Pattern in the Wild

Now, see how clean the API becomes for the user. They can configure only what they need.

func main() {
    // Just a basic server, using all the defaults.
    basicServer, _ := NewServer("localhost", 8080)

    // A highly customized server.
    tlsConfig := &tls.Config{...}
    customServer, _ := NewServer("db.example.com", 5432,
        WithTimeout(5*time.Minute),
        WithMaxConnections(250),
        WithTLSConfig(tlsConfig),
    )

    fmt.Printf("Basic: %+v\n", basicServer)
    fmt.Printf("Custom: %+v\n", customServer)
}

The beauty is in the readability. The function calls are self-documenting. WithTimeout(5*time.Minute) is infinitely clearer than a positional argument buried in a function signature.

Leveling Up: Handling Errors and Validation

“But wait,” you say, “what if my option needs to validate its input and return an error?” Excellent question. You’ve found the one rough edge of this pattern. The standard approach can’t return an error from the option function itself. The solution is to change the signature of our Option type.

// Option now returns an error, making our options potentially fallible.
type Option func(*Server) error

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) error {
        if timeout < 0 {
            return fmt.Errorf("timeout must be positive")
        }
        s.Timeout = timeout
        return nil
    }
}

// Our constructor must now handle errors from the options.
func NewServer(host string, port int, options ...Option) (*Server, error) {
    srv := &Server{Host: host, Port: port, Timeout: 30 * time.Second}

    for _, option := range options {
        // If any option fails, we bail out immediately.
        if err := option(srv); err != nil {
            return nil, err
        }
    }
    return srv, nil
}

This is the more robust, production-grade version of the pattern. It trades a bit of simplicity for proper error handling, which is almost always a trade worth making.

Why This Is Idiomatic Go

This pattern is beloved because it provides tremendous flexibility without breaking backwards compatibility. You can add new options years later without touching the existing constructor signature or breaking any existing calls. It makes your API clean for the common case and powerful for the complex one. It avoids the need for a massive config struct that you have to pass around (though that’s sometimes a valid pattern too), and it’s completely self-documenting at the call site. It’s not just a pattern; it’s a philosophy of API design: be kind to the user.