Comparing Patterns: FOP vs. Builder - Which to Choose?

Note: The core content was generated by an LLM, with human fact-checking and structural refinement.
Go’s design philosophy prioritizes simplicity and directness. While simple struct literals are sufficient for basic data structures, complex systems require a more controlled initialization process to enforce rules and manage dependencies. This necessity drives the use of powerful initialization patterns, including the idiomatic factory function and the popular Functional Options Pattern (FOP).
Go Constructor Pattern (Idiomatic Factory Function)
In Go, there is no built-in syntax for a “Constructor” like in many object-oriented languages. Instead, developers adopt an idiomatic design pattern: the Factory Function, conventionally named New or NewXXX.
The core responsibility of this factory function is to encapsulate creation logic and return a specific, immediately usable instance.
The Power of the Factory Function
Factory functions are essential when simple struct literal initialization is insufficient due to complexity. They provide several critical benefits:
- Guarding Against Invalid Zero Values: Although Go’s zero value mechanism ensures variables have a known initial state, that zero value might be logically invalid (e.g., an empty string for a required name). The factory function ensures the initial state is valid and deliberate.
- Enforcing Business Rules (Invariants): The factory function acts as an unbypassable entry point to execute validation logic, protecting the type’s invariants.
- Encapsulating Complex Initialization: If creating a struct requires injecting dependencies, initializing internal structures (like maps or channels), or performing non-trivial setup, the factory function hides this complexity from the caller.
- Designing Stable APIs: By returning an instance via a function, you decouple the structure’s internal implementation from the client code. The internal data structure can evolve freely as long as the factory function’s signature remains stable, preventing breaking changes.
- Managing Dependencies and Testability: The factory function is the ideal stage for Dependency Injection, allowing the function to receive interfaces (abstractions) and return a concrete structure. This practice makes unit testing easier, as mock implementations can be passed in.
Code Example: Idiomatic Factory Function
The following example demonstrates validation and clear failure paths using a factory function:
1 | type User struct { |
Functional Options Pattern (FOP)
The Functional Options Pattern (FOP) is a widely used Go idiom, popularized partly by influential blog posts, designed to handle initialization of objects with a potentially large or evolving number of optional parameters.
Defining the Functional Options Pattern
FOP relies on passing a variadic list of functions (options) into the constructor. Each option function usually takes a pointer to the structure being created and modifies a specific field.
1 | package main |
Advantages of FOP
- Flexibility and Non-breaking Changes: FOP is a huge advantage for library maintainers because they can easily deprecate options, combine existing options, or add new options without breaking existing client code.
- Encapsulation: Options are not part of the
Server’s API; they do not add methods or change the methods theServerobject implements. - Guaranteed Construction: The consuming code receives a fully constructed type after the constructor function is invoked.
Limitations and Workarounds
Despite its benefits, FOP has practical downsides:
- Naming Pollution: Option functions are namespaced to the package, meaning if you want to create the same functional option for two different objects, you must name them differently (e.g.,
WithTimeoutForServer,WithTimeoutForClient), potentially polluting the namespace. One workaround is using an empty struct type to namespace options within a package. - Type Restrictions: A function can only have one variadic set of parameters of the same type. This makes it annoying if you need multiple types, forcing all functional options to share a single type signature.
- Discoverability: It can be harder for users to discover which functional options are available when calling a function compared to explicit fields in an options struct. However, some argue that IDE autocomplete and Go documentation generally list options nicely by type.
- Complexity: Some developers find the pattern overly complicated, viewing it as a large workaround for Go’s lack of default parameters.
A common workaround for type restrictions and testability is defining the option as an interface instead of a function. For instance, Uber’s zap logging library defines an option as type Option interface { apply(*Logger) }. This allows several types to implement the interface, all being valid Options.
Comparison to Builder Pattern
The Functional Options Pattern and the Builder Pattern both aim to solve the problem of initializing objects with many optional parameters, but they achieve this differently.
The Builder Pattern (and Fluent Setters)
The pattern often referred to as the Builder Pattern in Go is sometimes simply fluent setters or method chaining. This approach involves methods that mutate the object and return the object’s pointer to allow chaining calls.
1 | // Example: Fluent Setters (often mislabeled as Builder) |
Critique of Fluent Setters in Go:
This method chaining approach is generally considered less idiomatic in Go. A significant drawback is that it allows the user to mutate the type at any time after creation, which makes reasoning about the data difficult, especially for services or servers expected to be immutable after initialization. Additionally, it is hard to return errors or mock objects with this fluent style.
The True Builder Pattern
The true Builder Pattern separates the builder object from the constructed object. The builder collects all configurations, and the final object is created only when a dedicated Build() method is called, often with an error return. This approach prevents attributes from being mutable after the object is successfully constructed.
1 | // Example: True Builder Pattern |
FOP vs. Builder: Which to Choose?
The choice often depends on the required constraints:
| Feature | Functional Options Pattern (FOP) | Builder Pattern (True) |
|---|---|---|
| Initialization Logic | Superior: The constructor function controls when options are applied, allowing for required initialization logic immediately after options are set, ensuring a valid object state upon return. | Requires a separate Build() method; complex validation or required configuration may need to be enforced separately or within Build(). |
| Required Parameters | Required parameters must be regular arguments in the New function. |
Required parameters can be placed in the Builder‘s constructor or enforced during Build(). Required configs slightly favor the builder/config struct approach (sometimes called the “Dysfunctional Options Pattern”). |
| Ergonomics/Usage | Highly ergonomic, especially if users are not expected to pass many options and none are required. | Can be verbose (boilerplate), but useful when configuration involves conditionals (if conf.SetBar { … }). |
| Extensibility | Easy for consumers of a package to implement custom option functions. | Generally closed for extension by external users; options are tightly bound to the builder type. |
| Go Idiomacy | Considered more prevalent and idiomatic in the Go ecosystem. | Method chaining (fluent setters) is often considered non-idiomatic. The true Builder pattern is often used for operations like SQL query construction where sequential steps are required (e.g., db.Select().From().Where()). |
For APIs that are designed from the ground up and typically called without many options, or where parameters are required, a plain options struct passed into the factory function might be the most idiomatic choice. However, FOP remains a preferred tool for managing optional configuration gracefully and enabling future extensibility.
Quoted Article Links
The sources contain references to several external articles and guides related to these patterns:
- Dave Cheney’s Post on Functional Options:
https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis - Uber Go Style Guide on Functional Options:
https://github.com/uber-go/guide/blob/master/style.md#functional-options - Dysfunctional Options Pattern in Go:
https://rednafi.com/go/dysfunctional_options_pattern/ - Tony Bai’s Go Constructor Pattern Guide:
https://tonybai.com/2025/09/12/go-constructor-pattern-guide - Different ways to initialize Go structs (asankov.dev):
https://asankov.dev/blog/2022/01/29/different-ways-to-initialize-go-structs/
More
Recent Articles:
- Go Error Handling Evolution and AsA Proposals on Medium on Website
- Go Pointers: Best Practices and the New Initialization Proposal on Medium on Website
Random Article:
More Series Articles about You Should Know In Golang:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a
And I’m Wesley, delighted to share knowledge from the world of programming.
Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. It would be a great help to me.
Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether I continue to post this type of article.
See you in the next article. 👋
中文文章: https://programmerscareer.com/zh-cn/go-function-option-patterns/
Author: Medium,LinkedIn,Twitter
Note: Originally written at https://programmerscareer.com/go-function-option-patterns/ at 2025-09-21 16:01.
Copyright: BY-NC-ND 3.0
Comments