New 42-day free trial Get it now
Smarty

Functional options pattern in Go: Flexibility that won’t make future-you sigh loudly

Functional options pattern in Go: Flexibility that won’t make future-you sigh loudly
Published February 10, 2026
Tags
Functional options pattern in Go: Flexibility that won’t make future-you sigh loudly

SDK authors live in a permanent tug-of-war:

  • Users want a simple constructor they can paste and ship.
  • Maintainers want room to grow without breaking everybody’s build on the next release.

That second part matters a lot right now, because a lot of people are still relatively early in their software careers. 

Approximately one in three developers has coded professionally for four years or less. That matters because unclear or fragile APIs disproportionately hurt newer developers—they don’t have scars yet.

If you’ve ever stared at a public API and thought, “How do I extend this without setting the ecosystem on fire,” congrats: you’re thinking like a library author.

Smarty has discovered that a combination of functional options and namespaces (searchable labels for those functional options) is one of the most effective solutions to this problem in Go. 

Here’s what you can expect to learn in this blog:

We’ll be the first to admit this looks a little ironic—our Go SDK doesn’t yet follow the namespace guidelines we’re about to share. It’s a lesson we learned the hard way, and we’re hoping to spare you from making the same mistake.

While namespaces require a bit of extra code and some careful design, the payoff is well worth it. Done right, they dramatically improve usability and searchability, making life easier for both your internal teams and anyone building with your Go SDK.

You can test out any of our products in our Live API playground for free, or continue reading. This blog is for anyone designing public APIs in Go—especially SDKs, libraries, or internal platforms with multiple users—and it aims to show newer developers (and even experienced ones who may not have learned) how to leverage more functional options while working in Go. 

First, a quick clarification: Go absolutely lets you pass parameters

Go isn’t allergic to parameters. What Go doesn’t have is:

  • Optional parameters/default arguments: In languages with optional parameters, someone might say something like “I’ll take a burger… and if you don’t hear otherwise, add fries.” Meaning the kitchen would just add fries by default if someone doesn’t mention them. But Go doesn’t allow for optional parameters. You must say exactly what you do want. “I’ll take a burger with fries,” or “I’ll take a burger without fries.” Go doesn’t read between the lines.
  • Function overloading (same name, different signatures): Go does not support function or method overloading. You can't have multiple functions with the same name but different parameter lists within the same scope. Instead, Go encourages idiomatic alternatives like distinct function names, variadic functions, interfaces & generics, and the functional options pattern. This is because Go is optimized for readability, maintainability, large teams, and future-proofing projects. 

Which means if you design a long parameter list constructor like:

NewClient(authID, authToken, baseURL, timeout, retries, enableTracing, ...)

The function signature becomes your public interface. And in Go, or any language really, changing the parameters in a public function signature is one of the most direct ways to cause breaking changes for every downstream user. 

You’ve basically built a future bug farm. Every new feature either forces a breaking change, adds more positional arguments, or introduces subtle misconfiguration bugs that are hard to see at the call site. The compiler will keep you honest (picky, but fair), and your users will feel every breaking change immediately.

This is why Smarty relies on functional options. Functional options let you keep the constructor stable and make everything else additive.

Rather than inventing a workaround, Go’s core team embraced a pattern that fits the language’s constraints instead of fighting them.

Rob Pike’s functional options idea 

Smarty's dev team lookin' at code

Rob Pike laid out a clean approach way back in 2014: define an Option as a function that mutates some internal state, then accept a variadic list of options in your constructor. (For a more up-to-date version of this same concept, you can also check out Golang Cafe’s or Medium’s take on the matter of functional options.)

More simply put, instead of cramming every possible setting into the function itself, pass in little instructions that tweak things after the object is created, those little instructions being options.

That “variadic list” piece is the magic because (...Option) can be empty, or it can contain a dozen knobs, but your public signature stays the same.

What “an option” means in practice

In the simplest form, an option is “a function that manipulates a configuration struct.”

Let’s code:

type Option func(*config)

Here’s an example of a simple option definition:

func WithAPICredentials(key string) Option {
        return func(c *config) {
               c.key = key
        }
}

This might look a bit strange–our function is returning another function, which will be invoked by the builder below to assign the user-supplied options to the config struct:

func BuildUSStreetAPIClient(options ...Option) *Client {
    var config
    for _, option := range append(defaultOptions, options) {
        option(&config)
    }
    return Client{...}
}

That’s the core pattern.

Why variadic options “stand the test of time”

First, a variadic function is one that receives an indefinite number of arguments /parameters. 

When we’re talking about variadic functions, we simply mean that there’s an unknown, undefined number of parameters for that function.

Because anyone can keep adding new capabilities without changing the signature:

  • v1 adds Timeout(...)
  • v1.1 adds MaxRetry(...)
  • v1.2 adds DebugHTTPTracing()
  • v1.3 adds WithHTTPClient(...)

Users who don’t care never have to update their code. Users who do care can opt in, one flag at a time if they wish, ignoring irrelevant updates that don’t affect their core functionalities. 

In Smarty’s Go SDK, the wireup package exposes a stable set of client builders like BuildUSStreetAPIClient(options ...Option) and a growing menu of options (credentials, custom base URL, headers, retry behavior, HTTP debugging, etc.). 

Configuration structs vs functional options

You’ll sometimes see Go libraries use a configuration struct instead of functional options:

cfg := Config{
    Timeout: 5 * time.Second,
    Retries: 3,
}
client := NewClient(cfg)

This approach works well for static or data-heavy configurations. 

But! 

For public SDKs, it tends to leak internal structure into the API, making defaults, validation, and conditional behavior harder to centralize as the surface area grows.

Functional Options can do all that a struct would do, but structs can’t do all the things Functional Options patterns do.

For example, functional arguments (unlike structs) can return errors, so you can include validation inside the option itself. 

Structs are good if you only have a small number of options, but Functional Options have better readability and documentation.

Functional options keep the public constructor stable while allowing the internal configuration to evolve without exposing every knob upfront.

Required vs. optional

A good mental model is that:

  1. Required inputs belong in the constructor signature.
  2. Optional behavior belongs in functional options.

An infographic showing which inputs are required and which are optional

If a value must always be provided for the client to function correctly, make it explicit. If it modifies behavior or enables a feature, make it an option.

For example, an API key or credential is typically required for most use cases at Smarty. 

Retry policies, timeouts, debugging flags, and transport tweaks are optional. 

Keeping that distinction clear makes APIs easier to read and harder to misuse or break.

“Exported” vs “unexported”: The uppercase rule that keeps APIs clean

Go doesn’t use public/private keywords. Instead:

  • Uppercase identifier = exported (package users can see it)
  • Lowercase identifier = unexported (package users can’t see it)

That matters because functional options usually mutate something you don’t want callers to touch directly (like config or a clientBuilder). You can keep the struct unexported and only expose the option functions.

Leveraging options + namespaces

One of the underrated wins of implementing this pattern in your code is searchability.

If your library exposes options in a consistent “namespace,” users can type “wireup.“ (period included) and instantly discover what’s available (in autocomplete, docs, and IDE symbol search). 

One way to do this is by using a namespace type as a method receiver, with an Options variable of that type, which can effectively scope all options ‘behind’ that namespace.

The library/SDK author might write:

type namespace struct{}
var Options namespace

Now, we can add a receiver to our option definitions, promoting them to methods of the namespace type:

code lines

func (namespace) WithAPICredentials(key string) Option {
	return func(c *config) {
		c.key = key
	}
}

The caller now invokes the BuildUSStreetAPIClient, referencing any desired options from behind the newly defined Options namespace.

client := wireup.BuildUSStreetAPIClient(
    wireup.Options.WithCredentials(apiKey),
    wireup.Options.WithTimeout(5*time.Second),
    wireup.Options.WithRetries(3),
    wireup.Options.WithDebugHTTP(),
)

As soon as a user types ”wireup.Options.”, their editor displays all available options (and nothing else).

And yes—this is the same reason teams suffix Slack channels with stuff like -help-meetup, and  -announcements. We want to make sense of the world around us, understand our options, and then implement the ones that make sense to us, disregarding those that aren’t useful.

Tim Peters said it best, and you can see it with this sneaky dev hack:

$ python3
>>> import this

(The Zen of Python)

 “Namespaces are one honking great idea — let’s do more of those!”

How this maps to “wireup” and “client” in a Go SDK

When someone uses a Go SDK, they usually want a client.

The wireup layer is where you assemble that client:

  • credentials
  • base URL (sometimes)
  • HTTP transport settings
  • retry/timeout policy
  • debugging toggles
  • headers/query params/feature flags

With functional options, you’re effectively saying:

“You, the user, decide which configuration functions I should invoke on your behalf during client construction.”

That makes the SDK ergonomic and maintainable because users only see relevant options, each option is clearly labeled, and users don’t have to memorize different parameters or their order. 

Smarty’s own Go SDK docs describe this builder-based approach, pointing users at the wireup package for client creation. 

The “breaking change” question every library/SDK author has to answer

Every public constructor parameter you add is a tax you collect forever.

Functional options reduce that tax by keeping your core constructor stable, letting new functionality ship as additive options. It won’t eliminate hard decisions, but it dramatically reduces how often you have to ask: “Is the value I’m adding worth the inconvenience to my users?”

Because most of the time, you can ship the value without the inconvenience.

A practical wrap-up

Functional options are one of Go’s most practical API design patterns.

They preserve a stable public constructor, keep internal configuration encapsulated, and make future enhancements additive instead of disruptive. 

In a wireup package—where the entire job involves composing a client—options provide a clean way to let users opt into behavior while keeping the SDK readable, searchable, and backward compatible.

Subscribe to our blog!
Learn more about RSS feeds here.
Read our recent posts
Functional options pattern in Go: Flexibility that won’t make future-you sigh loudly
Arrow Icon
SDK authors live in a permanent tug-of-war:Users want a simple constructor they can paste and ship. Maintainers want room to grow without breaking everybody’s build on the next release. That second part matters a lot right now, because a lot of people are still relatively early in their software careers. Approximately one in three developers has coded professionally for four years or less. That matters because unclear or fragile APIs disproportionately hurt newer developers—they don’t have scars yet.
Ambiguous address matches: What they are and why compliance teams should care
Arrow Icon
If you’ve ever run into an address that seems to exist in more than one place, congratulations—you’ve discovered the world of ambiguous address matches. They’re the Schrödinger’s cat of location data: valid, yet potentially two distinct locations. This blog will focus on a few key things: What are ambiguous address matches?Why ambiguous address matches matter for compliance and customer serviceHow to handle matches with address ambiguityWhy you should inform your customers of ambiguous address matchesOur final thoughts on ambiguous address matchesWhat are ambiguous address matches?An ambiguous address match occurs when an entered address resolves to two or more valid locations with slight but meaningful differences.
Smarty's January 2026 release adds parcel boundaries, provisional addresses, and smarter international geocoding
Arrow Icon
OREM, UT, Jan 27, 2026—Smarty®, an expert in address data intelligence, today announced a three-part release designed to help organizations turn messy, fast-changing location data into operational confidence. The January 2026 bundle introduces: 1) A brand-new parcel dataset, 2) Expands provisional address programs into core U. S. products, and 3) Upgrades Smarty’s International Geocoding engine—giving organizations more precision and more usable signals for automation at scale. “Address data is never ‘done.

Ready to get started?