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
98: The Functional Options Pattern
I was working on a networking library the other day and ran into a classic Go dilemma: how do I let the user configure a service without making the constructor a nightmare to maintain? I want to start with a few sensible defaults, but I want to give power users the ability to tweak everything.
The Constructor Explosion
My first instinct was the simplest one. I'll just put the configuration in the NewServer function. It starts off easy enough:
type Server struct {
Addr string
Timeout time.Duration
}
func NewServer(addr string, timeout time.Duration) *Server {
return &Server{Addr: addr, Timeout: timeout}
}
But then the requirements changed. I needed to add MaxConnections. Then I needed a ReadTimeout. Suddenly, my function signature looks like this: NewServer(addr string, timeout time.Duration, maxConn int, readTimeout time.Duration, writeTimeout time.Duration). It's hideous. If I want to add a sixth option, I break every single piece of code that calls this function across the entire project. That's a non-starter.
The "Zero Value" Trap
Okay, so I'll use a config struct. That's a common pattern, right? I'll pass a Config object and the constructor will just use it.
type Config struct {
Addr string
Timeout time.Duration
MaxConn int
}
func NewServer(cfg Config) *Server {
return &Server{
Addr: cfg.Addr,
Timeout: cfg.Timeout,
MaxConn: cfg.MaxConn,
}
}
Here is where I hit a wall. What happens if the user wants the default MaxConn, but they want to set a custom Timeout? They'll pass a Config struct where MaxConn is 0. But wait—is 0 their intended value, or is it just the default integer zero value? If I want the default to be 100, I can't tell if the user explicitly asked for 0 or if they just didn't provide a value.
I could use pointers in the struct (e.g., *int), but then the caller has to do this annoying dance of creating a variable just to take its address: MaxConn: &someIntVar. It's clunky and feels un-idiomatic.
Flipping the Script with Functions
I realized I didn't need the user to give me a data structure; I needed them to give me instructions on how to modify the server. This is where the Functional Options pattern comes in. Instead of a config struct, I'll define a function type that knows how to update a Server.
type Option func(*Server)
func WithTimeout(t time.Duration) Option {
return func(s *Server) {
s.Timeout = t
}
}
func WithMaxConn(n int) Option {
return func(s *Server) {
s.MaxConn = n
}
}
Now, look at how the constructor changes. I can use a variadic parameter to accept any number of these options. I set my defaults first, and then I let the options overwrite them:
func NewServer(addr string, opts ...Option) *Server {
// 1. Set the sensible defaults
srv := &Server{
Addr: addr,
Timeout: 30 * time.Second,
MaxConn: 100,
}
// 2. Apply each option provided by the user
for _, opt := range opts {
opt(srv)
}
return srv
}
This is a massive win. If I need to add a new configuration field next week, I just write a new WithSomething function. The NewServer signature never changes, nothing breaks, and the API is incredibly readable. You can see exactly what is being configured at a glance:
// Only change the timeout, keep other defaults
s1 := NewServer("localhost:8080", WithTimeout(10 * time.Second))
// Change everything
s2 := NewServer("localhost:8080", WithTimeout(5*time.Second), WithMaxConn(500))
I've found this to be the "gold standard" for Go libraries. It balances flexibility with a clean, stable API. It's a bit more boilerplate up front to write the With... functions, but your future self (and anyone using your code) will thank you for it.
📋 Practical Task
Implementing a Customizable Database Client
You are building a database client library. The client needs a ConnectionURL (which is mandatory), but it should have optional settings for RetryAttempts (default: 3) and EnableLogging (default: false).
Your Task:
- Define a
Clientstruct with fields forConnectionURL,RetryAttempts, andEnableLogging. - Create an
Optionfunction type. - Implement two option functions:
WithRetryAttempts(int)andWithLogging(bool). - Implement a
NewClient(url string, opts ...Option) *Clientconstructor that applies the defaults and then processes the options. - In your
mainfunction, instantiate two clients: one using only the defaults, and one with 5 retry attempts and logging enabled.
There are no comments for now.