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.
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.
Options Pattern 🚀🚀🚀
// Option defines a function type for server optionstype 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 IDfunc WithID(id string) Option { return func(s *Server) { s.id = id }}
// WithMaxConn sets the maximum number of connectionsfunc WithMaxConn(maxConn int) Option { return func(s *Server) { s.maxConn = maxConn }}
// WithTLS enables or disables TLSfunc WithTLS(tls bool) Option { return func(s *Server) { s.tls = tls }}
For implementing options pattern:
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.