Functional Options Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 79 of 107.
When creating structs with many optional configuration fields, constructor functions can become unwieldy. The Functional Options Pattern solves this elegantly by using functions to configure objects.
Consider a server struct with multiple optional settings. Instead of a constructor with many parameters, we define an option type and functions that return options:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}The constructor accepts a variadic list of options and applies each one:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // default
timeout: 30, // default
}
for _, opt := range opts {
opt(s)
}
return s
}Now creating servers becomes readable and flexible:
// Use defaults
s1 := NewServer("localhost")
// Customize specific options
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))This pattern shines because callers only specify what they need, defaults are clear in one place, and adding new options doesn't break existing code. It's widely used in Go libraries like grpc and zap.
Challenge
EasyLet's build a configurable database connection using the Functional Options Pattern! This pattern shines when you have many optional settings with sensible defaults—exactly what a database connection needs.
You'll organize your code across two files:
database.go: Define your database connection type and option functions.Create a
DBConnectionstruct with these unexported fields:host(string)port(int)username(string)password(string)maxConnections(int)timeout(int) - in seconds
Define an
Optiontype as a function that modifies a*DBConnection.Create these option functions that return an
Option:WithPort(port int)WithCredentials(username, password string)WithMaxConnections(max int)WithTimeout(seconds int)
Create a constructor
NewDBConnection(host string, opts ...Option)that returns a*DBConnection. Set these defaults: port5432, username"admin", password"secret", maxConnections10, timeout30. Apply all provided options after setting defaults.Add a
ConnectionString()method that returns the connection info in this format:[username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)main.go: Build database connections with various configurations.Read the host, then read a count of options to apply. For each option, read the option type and its value(s):
portfollowed by the port numbercredentialsfollowed by username and password (two lines)maxconnfollowed by the max connections numbertimeoutfollowed by the timeout in seconds
Create a
DBConnectionwith the specified options and print its connection string.
The following inputs will be provided:
- Line 1: Host name
- Line 2: Number of options
- Following lines: Option type and value(s)
For example, given:
localhost
0Your output should be:
admin:secret@localhost:5432 (max:10, timeout:30s)And given:
db.example.com
2
port
3306
credentials
root
mypasswordYour output should be:
root:mypassword@db.example.com:3306 (max:10, timeout:30s)And given:
production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60Your output should be:
dbuser:securepass123@production.server:5433 (max:50, timeout:60s)Notice how callers only specify the options they need—unspecified settings use sensible defaults. This is the elegance of the Functional Options Pattern!
Cheat sheet
The Functional Options Pattern provides an elegant way to configure structs with many optional fields by using functions instead of lengthy constructor parameters.
Define an Option type as a function that modifies a pointer to your struct:
type Server struct {
host string
port int
timeout int
}
type Option func(*Server)Create option functions that return Option closures:
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout int) Option {
return func(s *Server) {
s.timeout = timeout
}
}The constructor accepts a variadic list of options, sets defaults, then applies each option:
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // default
timeout: 30, // default
}
for _, opt := range opts {
opt(s)
}
return s
}Usage is clean and flexible—callers only specify what they need:
// Use defaults
s1 := NewServer("localhost")
// Customize specific options
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))Benefits: defaults are centralized, callers specify only what they need, and adding new options doesn't break existing code.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read host
host, _ := reader.ReadString('\n')
host = strings.TrimSpace(host)
// Read number of options
numOptionsStr, _ := reader.ReadString('\n')
numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))
// Collect options
var opts []Option
for i := 0; i < numOptions; i++ {
optionType, _ := reader.ReadString('\n')
optionType = strings.TrimSpace(optionType)
switch optionType {
case "port":
portStr, _ := reader.ReadString('\n')
port, _ := strconv.Atoi(strings.TrimSpace(portStr))
// TODO: Append WithPort option to opts
_ = port // Remove this line when implementing
case "credentials":
username, _ := reader.ReadString('\n')
username = strings.TrimSpace(username)
password, _ := reader.ReadString('\n')
password = strings.TrimSpace(password)
// TODO: Append WithCredentials option to opts
_, _ = username, password // Remove this line when implementing
case "maxconn":
maxStr, _ := reader.ReadString('\n')
maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
// TODO: Append WithMaxConnections option to opts
_ = maxConn // Remove this line when implementing
case "timeout":
timeoutStr, _ := reader.ReadString('\n')
timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
// TODO: Append WithTimeout option to opts
_ = timeout // Remove this line when implementing
}
}
// TODO: Create DBConnection using NewDBConnection with host and opts
// TODO: Print the connection string using ConnectionString() method
_ = opts // Remove this line when implementing
fmt.Println("TODO: Print connection string here")
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool12Advanced OOP Concepts
Functional Options PatternBuilder Pattern in GoMethod ChainingType Aliases vs DefinitionsReflection BasicsCode Generation Overview