Contents

Golang Functional Options Pattern

In Go, we often use structs to encapsulate and organize data. However, this approach can become complex and difficult to maintain when we need to provide optional, customizable behavior. This is where the Functional Options Pattern comes into play. It’s a design pattern that allows us to create objects with optional parameters and customizable behavior.

The Basics

A Functional Option is a function that takes an object that needs to be customized and modifies it as needed. Here’s a basic example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Option func(*MyStruct)

func NewMyStruct(options ...Option) *MyStruct {
  obj := &MyStruct{}

  for _, opt := range options {
    opt(obj)
  }

  return obj
}

In this example, the NewMyStruct function accepts any number of Option functions. It creates a new MyStruct object and iterates through each option function, passing the new object as an argument.

How to Use Functional Options

Let’s create a concrete example to better understand how to use this pattern. Suppose we have a Server struct that has some configurable properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Server struct {
  Address string
  Port int
}

func NewServer(options ...func(*Server)) *Server {
  s := &Server{
    Address: "0.0.0.0",
    Port: 8080,
  }

  for _, option := range options {
    option(s)
  }

  return s
}

We can define some functions to modify the default values of the Server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func WithAddress(address string) func(*Server) {
  return func(s *Server) {
    s.Address = address
  }
}

func WithPort(port int) func(*Server) {
  return func(s *Server) {
    s.Port = port
  }
}

We can then use these functions to create new Server objects:

1
2
3
4
s := NewServer(
  WithAddress("127.0.0.1"),
  WithPort(8081),
)

Benefits of Functional Options

  • Conciseness: Each configuration option becomes a standalone function, which takes the struct it needs to modify. This eliminates the need for long argument lists, improving code readability.
  • Flexibility: Adding new options simply requires creating new functions, without modifying the existing code. This keeps your project extensible for future requirements.
  • Testability: It’s easy to isolate and test each configuration option, ensuring the robustness of your configuration.
  • Readability: Function names can be self-documenting, enhancing comprehension and maintainability of the code.

Specifically, the Golang Functional Options pattern can help you solve the following problems:

  • Verbose Configuration Code: Traditional constructor methods result in lengthy, hard-to-maintain code. The Golang Functional Options pattern allows you to break down configuration code into several independent functions, making it cleaner, more readable.
  • Inflexible Configuration: If you want to add new configuration options, you need to modify the existing code. The Golang Functional Options pattern solves this by letting you add new functions without modifying the existing code.
  • Difficult to Test Configuration: Traditional constructor methods mix configuration code with business logic code, which increases the complexity of testing. The Golang Functional Options pattern separates configuration code from business logic code, making it easier to test.

When to Use Functional Options

Consider using the functional options pattern when your project involves:

  • A struct with numerous configuration options.
  • An expectation of flexibility and extensibility in configuration.
  • A preference for concise, readable, and maintainable code.

Conclusion

The Functional Options Pattern is a powerful way to create objects with configurable options in Go. By using this pattern, we can create code that is concise, easy to extend, and easy to read.