The Functional Options Pattern in Go
It is particularly useful when you want to provide optional configuration and there may be more configuration options in the future. This pattern is great for libraries!
Let’s see an example. Here is a super basic server without functional options:
type Server struct {
host string
port int
protocol string
}
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
protocol: "http",
}
}
Over time, our requirements change, and we need to support more configuration options. Instead of changing the signature of the NewServer function, which can be problematic and not backward-compatible, we can use functional options.
Firstly, we define a functional option:
type ServerOption func(*Server)
and a function that satisfies the type:
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
Then, we modify our NewServer function:
func NewServer(host string, opts ...ServerOption) *Server {
server := &Server{
host: host,
port: 443, // default port set to 443
protocol: "https",
}
for _, opt := range opts {
opt(server)
}
return server
}
Now we can use it like this:
server1 := NewServer("localhost") // uses default port 443
server2 := NewServer("localhost", WithPort(8080))
As you can see, this allows us to offer customizations flexibly, while still maintaining readability and not exposing internal fields.
Pro Tip
Make sure that your functional options are truly optional. You’ll notice in my example that if I do not pass in WithPort()
, my NewServer
function returns a server with a sensible default configuration. This is good practice, and this pattern works best for things that are truly optional.
I hope you found this useful. This tip started life as a tweet (by me), and you can see the original here.