🍔 Analogy
At a build-your-own-burger counter you start from a base and add what you want — cheese, bacon, hold the onions — while the kitchen assembles it step by step. You don’t recite a fixed 12-ingredient order; you specify only your changes, and defaults cover the rest.
The problem
An object with many fields — half of them optional — makes constructors miserable. NewServer("localhost", 8080, 30, true, false, nil) is unreadable, and “telescoping” constructors (NewServer, NewServerWithTLS, NewServerWithTLSAndTimeout…) multiply forever. Builder separates construction from the final object, letting callers set only what they care about.
Structure
graph LR
D["defaults<br/>port:8080, timeout:30s"] --> O1["WithPort(443)"]
O1 --> O2["WithTLS()"]
O2 --> F["final Server"]
Idiomatic Go — functional options
This is the Go community’s standard answer. NewServer applies defaults, then each Option closure tweaks the result. Edit and Run:
package main
import (
"fmt"
"time"
)
type Server struct {
host string
port int
timeout time.Duration
tls bool
}
// Option configures a Server.
type Option func(*Server)
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func WithTLS() Option { return func(s *Server) { s.tls = true } }
// NewServer applies sensible defaults, then each option in turn.
func NewServer(host string, opts ...Option) *Server {
s := &Server{host: host, port: 8080, timeout: 30 * time.Second} // defaults
for _, opt := range opts {
opt(s)
}
return s
}
func main() {
a := NewServer("localhost") // all defaults
b := NewServer("example.com", WithPort(443), WithTLS(), WithTimeout(5*time.Second))
fmt.Printf("%+v\n", *a)
fmt.Printf("%+v\n", *b)
}
The classic fluent builder
The GoF “method-chaining” builder is also valid Go — useful when construction is staged or needs a final Build() validation step:
type ServerBuilder struct{ s Server }
func NewServerBuilder(host string) *ServerBuilder {
return &ServerBuilder{Server{host: host, port: 8080}}
}
func (b *ServerBuilder) Port(p int) *ServerBuilder { b.s.port = p; return b }
func (b *ServerBuilder) TLS() *ServerBuilder { b.s.tls = true; return b }
func (b *ServerBuilder) Build() Server { return b.s } // validate here
// NewServerBuilder("x").Port(443).TLS().Build()
🐹 Functional options win for libraries
Because Go has no default or named parameters, functional options are the idiomatic builder — and they’re what gRPC, the AWS SDK, and countless libraries use. The killer feature: you can add WithKeepAlive(...) later and every existing caller still compiles. Reach for the chained builder when you genuinely need staged construction or a validating Build() step.
In the standard library
strings.Builder— accumulate a string without repeated allocations.bytes.Buffer— build up bytes incrementally.net/httpandtext/templatelean on option-style configuration.
Pitfalls
⚠️ Don't out-build a three-field struct
Functional options shine when there are many optional knobs. For a small struct, a named-field literal — Server{Host: "x", Port: 443} — is clearer than a pile of WithX functions. Add the machinery when the option count actually justifies it.
When to use it — and when not
✅ Reach for it when
- An object has many fields, several of them optional, and you want sensible defaults.
- You want a readable, extensible construction API — add an option without breaking callers.
- Construction should be validated or staged before the object is usable.
⛔ Think twice when
- The object has two or three required fields — a plain struct literal with field names is clearer.
- You'd add a builder purely for symmetry; don't pay for machinery you don't need.
Related patterns
Provide an interface for creating families of related objects without specifying their concrete types.
CREATIONAL Factory MethodDefine an interface for creating an object, but let the implementation decide which concrete type to instantiate.
CREATIONAL PrototypeCreate new objects by cloning an existing, configured instance instead of building one from scratch.
Check your understanding
Score: 0 / 31. What is the idiomatic Go form of Builder?
Go has no named/default parameters or constructor overloading, so the community settled on functional options: NewX(required, ...Option) where each Option is a closure that configures the value.
2. Why prefer functional options over one big constructor?
A long positional constructor is unreadable and brittle; options are self-documenting, order-independent, defaulted, and extensible without breaking existing calls.
3. Which standard-library type literally is a Builder?
strings.Builder builds a string piece by piece with Write/WriteString, then String() yields the result — Builder by name and by design.