Options Pattern in Golang

30th June, 2024 (0 days ago)
#golang#designpattern

Whenever we are writing a real world software with a number of features, the core idea we all stick to is writing a modular code i.e. writing modules / libraries which can be seamlessly integrated and used, without too much of hassle. One of the design pattern which enables us to do this in Go is Options pattern.

Why is it needed?

Let’s say we are writing a http server with some configuration - id, max connection and tls. The first things that comes to mind is creating a constructor function and passing the config to it.

type Server struct {
id string
maxConn int
tls bool
}
func newServer(id string, maxConn int, tls bool) *Server {
return &Server{
id: id,
maxConn: maxConn,
tls: tls,
}
}

In this case, as the number of arguments increase the more tedious it becomes to maintain the code.

What can be the other solution? Let’s say we create a Options struct and pass it to the constructor which can be used to initialise the server.

type ServerOpts struct {
id string
maxConn int
tls bool
}
func newServer(opts *ServerOpts) *Server {
return &Server{
id: opts.id,
maxConn: opts.maxConn,
tls: opts.tls,
}
}

This seems to be working, but the issue is if I want to initialise the server with some default values, I can’t. Because I always have to pass the default values as the options because the server variable is dependent on it. Options pattern helps in making implementation more flexible.

How to implement it ?

Options Pattern 🚀🚀🚀

// Option defines a function type for server options
type Option func(*Server)
func getDefaultServerValues() *Server {
return &Server{
id: "default",
maxConn: 100,
tls: false,
}
}
func NewServer(options ...Option) *Server {
// Set default values
s := getDefaultServerValues()
// Apply the options
for _, option := range options {
option(s)
}
return s
}
// WithID sets the server ID
func WithID(id string) Option {
return func(s *Server) {
s.id = id
}
}
// WithMaxConn sets the maximum number of connections
func WithMaxConn(maxConn int) Option {
return func(s *Server) {
s.maxConn = maxConn
}
}
// WithTLS enables or disables TLS
func WithTLS(tls bool) Option {
return func(s *Server) {
s.tls = tls
}
}

For implementing options pattern:

  1. We’ve kept the Server struct, but removed the ServerOpts struct entirely.
  2. We’ve defined an Option type, which is a function that takes a pointer to a Server and modifies it.
  3. The NewServer function now takes a variadic parameter of Option functions. It creates a server with default values and then applies each option.
  4. We’ve created separate functions for each option (WithID, WithMaxConn, WithTLS). Each of these returns an Option function that modifies the specific field of the Server.

Now if I want to use a server with default values I can just use it like this:

package main
// import the server package
func main() {
serverObj := NewServer()
// use the server object
}

Or If I want to override the default values then I can use it like :

package main
// import the server package
func main() {
serverObj := NewServer(
WithMaxConn(10),
WithTLS(true),
)
// use the server object
}

This approach offers several advantages:

Thanks for reading this blog.