Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
232: Builder Pattern in Go
I've spent a lot of my career cleaning up "constructor bloat." You know the pattern: you start with a simple NewClient function that takes two arguments. Then the project grows. Suddenly, you need a timeout. Then a retry policy. Then a custom logger, a proxy setting, and a trace ID. Before you know it, you're staring at a function signature with twelve arguments, half of which are nil or 0 because the caller doesn't actually need them.
The Nightmare of the Telescoping Constructor
Let's look at how most people start. Imagine we're building a DatabaseConnection object. It's a critical piece of infrastructure, so it needs a lot of configuration. The naive approach is to just keep adding parameters to the factory function:
type DatabaseConnection struct {
Host string
Port int
User string
Password string
Timeout time.Duration
SSL bool
}
func NewDatabaseConnection(host string, port int, user string, pass string, timeout time.Duration, ssl bool) *DatabaseConnection {
return &DatabaseConnection{
Host: host, Port: port, User: user, Password: pass, Timeout: timeout, SSL: ssl,
}
}
This is a maintenance disaster. If I want to add a MaxConnections field, I have to touch every single line of code in the entire repository that calls NewDatabaseConnection. Even worse, look at the call site. If I see NewDatabaseConnection("localhost", 5432, "admin", "secret", 30, true), I have no idea what 30 or true actually means without jumping back to the definition. I've definitely swapped two integer arguments in my sleep and spent an hour debugging why my port was set to 30 seconds.
Fluent Interfaces and the Builder's Logic
The Builder pattern fixes this by separating the construction of the object from the representation of the object. Instead of one massive function, we create a helper struct—the Builder—that collects the configuration step-by-step. The trick in Go is to return the builder itself from every setter method, creating a "fluent interface" that reads like a sentence.
type ConnectionBuilder struct {
conn *DatabaseConnection
}
func NewConnectionBuilder() *ConnectionBuilder {
// We set our sensible defaults right here
return &ConnectionBuilder{
conn: &DatabaseConnection{
Port: 5432,
Timeout: 30 * time.Second,
SSL: true,
},
}
}
func (b *ConnectionBuilder) WithHost(host string) *ConnectionBuilder {
b.conn.Host = host
return b
}
func (b *ConnectionBuilder) WithPort(port int) *ConnectionBuilder {
b.conn.Port = port
return b
}
func (b *ConnectionBuilder) WithTimeout(t time.Duration) *ConnectionBuilder {
b.conn.Timeout = t
return b
}
Now, the caller only specifies what they actually care about. The noise disappears. I can see exactly what's being configured, and if I add a new option to the builder, I don't break a single existing call site. It's a much more stable way to evolve an API.
When the Build Actually Happens
The most important part of this pattern isn't the setters; it's the Build() method. This is where we move from "gathering data" to "creating a valid object." If you just use a public struct and let users set fields manually, you can't guarantee the object is in a valid state. The Builder acts as a gatekeeper.
func (b *ConnectionBuilder) Build() (*DatabaseConnection, error) {
if b.conn.Host == "" {
return nil, errors.New("host is required to establish a connection")
}
if b.conn.Port <= 0 {
return nil, errors.New("invalid port number")
}
return b.conn, nil
}
I like this approach because it encapsulates the validation logic. The user doesn't have to wonder, "Did I remember to set the host?" They just call Build(), and the builder tells them exactly what they missed. You get a clean, immutable-feeling object at the end, and the messy logic of "how to put it together" is hidden away in the builder.
One quick word of caution: don't use this for everything. If your struct only has two or three fields that are always required, a simple New... function is better. The Builder pattern is a tool for complexity. If you use it for simple objects, you're just adding boilerplate for the sake of it, and your teammates will probably call you out on it during the PR.
📋 Practical Task
Exercise: Implementing a Custom HTTP Request Builder
You need to create a RequestBuilder to simplify the creation of complex HTTP requests. Instead of manually filling out a request struct, the user should be able to chain methods to configure the request.
Requirements:
- Create a
HTTPRequeststruct with fields forMethod(string),URL(string),Timeout(time.Duration), andHeaders(a map of strings). - Implement a
RequestBuilderthat provides:NewRequestBuilder(): Initializes a builder with a default method of "GET" and a default timeout of 10 seconds.WithURL(url string): Sets the target URL.WithMethod(method string): Overrides the default HTTP method.WithHeader(key, value string): Adds a key-value pair to the headers map.Build(): Returns the finalHTTPRequestand an error.
- The
Build()method must return an error if theURLhas not been set.
Test your implementation by building a "POST" request to "https://api.example.com/v1/data" with a "Content-Type: application/json" header and a custom timeout of 5 seconds.
There are no comments for now.