It has been about a year since I started developing some things in Go. At first, it was an unknown language to me. I only knew that it was a “typed” language, compiled and memory safe through a GC (Garbage collector), and nothing else. So I took a crash course to learn its types, syntax, concurrency and error handling, etc.
One of the things I learned is the Option Pattern, which is used to simplify the construction of a struct with optional parameters, because Go lacks overloading, that is, we can not have a function with the same name with different arguments as, for example, we can find in Java.
Problem Statement
We have a struct called server, which is private of its package, and has 4 properties that are also private (types or properties that start with a lowercase letter, in Go, are private).
// server struct. We want explicitly to keep properties private
type server struct {
host string
port uint16
timeout_ms uint64
encryption bool
}
Go, actually doesn’t support constructors, but constructor-like Factory functions are easily implemented. We will only be able to instance this struct this way, because server it’s private. The obvious solution would be to create a constructor with all parameters:
func NewServer(host string, port uint16, ms uint64, encryption bool) *server {
return &server{
host: host,
port: default_port,
timeout_ms: default_timeout,
encryption: default_encryption,
}
}
This is completely valid, but… we are requiring the user to provide each and every argument to the constructor. Moreover, this can make the code unreadable, because several parameters are numeric, and it is necessary to remember their order.
s := NewServer("localhost", 10000, 10000, true)
Solution Statement
Is it true that it is possible that we do not remember in which position the numerical parameters were? What argument is true?… What can we do to improve this? Let’s see some examples.
s := NewServer("localhost", WithEncryption())
s := NewServer("localhost", WithPort(10000), WithTimeoutMs(1000))
In this new constructor, we are only required to provide the host and optionally, a list of options. Let’s see how we can implement this pattern to create an elegant and user-friendly API.
Solution Implementation
Next, we will implement all the necessary elements to be able to use the syntax proposed in the solution approach.
Creation of the default values
const (
default_port = 8080
default_timeout = 60000
default_encryption = false
)
Creation of serverOpt type
type serverOpt func(*server)
Implementation of the constructor
func NewServer(host string, opts ...serverOpt) *server {
server := &server{
host,
default_port,
default_timeout,
default_encryption,
}
// Iterate options and apply them
for _, opt := range opts {
opt(server)
}
return server
}
Implementation of the options
// WithPort determines port for listen to
func WithPort(port uint16) serverOpt {
return func(s *server) {
s.port = port
}
}
// WithTimeoutMs determines millis to timeout
func WithTimeoutMs(ms uint64) serverOpt {
return func(s *server) {
s.timeout_ms = ms
}
}
// WithEncryption is to enable server encryption
func WithEncryption() serverOpt {
return func(s *server) {
s.encryption = true
}
}
// WithoutEncryption is to disable server encryption
func WithoutEncryption() serverOpt {
return func(s *server) {
s.encryption = false
}
}
You can see the full code here.