Option Pattern in Golang

Introduction

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.

This is all! If you found this post interesting, we encourage you to visit the Software category to see all the related posts and to share it on social networks. See you soon!
Default image
Cristòfol Torrens
Articles: 8