Go Constructor, Functional Option And Builder Patterns

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

image.png|300

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:

  1. 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.
  2. Enforcing Business Rules (Invariants): The factory function acts as an unbypassable entry point to execute validation logic, protecting the type’s invariants.
  3. 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.
  4. 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.
  5. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type User struct {
Name string
Age int
}

// Example 1: Factory function with validation and error return
func NewUser(name string, age int) (*User, error) {
if age < 18 {
return nil, errors.New("user must be at least 18 years old")
}
return &User{
Name: name,
Age: age,
}, nil
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"time"
)

type Server struct {
port string
timeout time.Duration
maxConnections int
}

// 1. Define the Option Type: a function that modifies the Server struct
type ServerOptions func(*Server)

// 2. Define Option Constructors: functions that return an Option
func WithPort(port string) ServerOptions {
return func(s *Server) {
s.port = port
}
}

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

func WithMaxConnections(maxConnections int) ServerOptions {
return func(s *Server) {
s.maxConnections = maxConnections
}
}

// 3. The Factory Function applies the options
func NewServer(options ...ServerOptions) *Server {
// Set reasonable defaults before applying custom options
server := &Server{
port: ":8080",
timeout: time.Second * 10,
maxConnections: 100,
}

// Apply all passed options
for _, option := range options {
option(server)
}
return server
}

func main() {
myServer := NewServer(
WithMaxConnections(500),
WithTimeout(time.Second*30),
// WithPort is optional, default is :8080
)
fmt.Printf("Server config: %+v\n", myServer)
}

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 the Server object 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Example: Fluent Setters (often mislabeled as Builder)
type Server struct {
port string
timeout time.Duration
maxConnections int
}

func (s *Server) WithPort(port string) *Server {
s.port = port
return s
}

// ... other With methods ...

func NewServer() *Server {
return &Server{}
}

func main() {
server := NewServer().
WithMaxConnections(500).
WithTimeout(time.Second * 30).
WithPort(":8080")
// fmt.Println(server)
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Example: True Builder Pattern
type server struct { // unexported to enforce use of builder
port string
timeout time.Duration
}

// Private constructor
func newServer(p string, t time.Duration) *server {
return &server{port: p, timeout: t}
}

type ServerBuilder struct {
Port string
Timeout time.Duration
// MaxConnections int // Builder fields are usually exported for easy literal assignment
}

// Build method creates and returns the final, configured server
func (sb ServerBuilder) Build() (*server, error) {
if sb.Port == "" {
return nil, errors.New("port is required")
}
// Apply configuration, potentially running validation
return newServer(sb.Port, sb.Timeout), nil
}

func main() {
// Configuration via struct literal, followed by mandatory Build()
srv, err := ServerBuilder{
Port: ":8080",
Timeout: time.Second * 30,
}.Build()
// fmt.Println(srv, err)
}

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:

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

Go Language Evolution:Simplicity,Complexity,and Stability The Liability of Package Managers and Dependencies

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×