Writing Better Code in Go with the Option Pattern

dsysd dev
4 min readApr 26

--

Photo by Clay Banks on Unsplash

As developers, we are always looking for better ways to write code that is maintainable, testable, and scalable.

In Go, we often see structs with many fields, which can make initialization a pain, especially when some fields are optional.

It becomes even more challenging when there are many possible configuration combinations.

One way to solve this problem is to use the option pattern, which is widely used in many Go libraries, such as net/http, sqlx, and gorm.

Backstory

The option pattern originated from functional programming, where functions accept optional arguments.

Instead of using function overloading, which can make the codebase more complex, developers use functional options to provide a flexible interface that can be extended without breaking the existing API.

In Go, the option pattern is widely used to simplify struct initialization. Instead of defining a large number of constructors with different parameters, we can define a single constructor that accepts a variadic number of functional options.

Advantages

Using the option pattern has many advantages:

Flexibility

Using functional options makes the API more flexible because we can extend it without breaking the existing API. We can add new options without modifying the existing code, and we can also provide default values for optional fields.

Readability

Using functional options improves code readability because it makes the initialization code more concise and easier to read. Instead of having many parameters, we have a clear list of options that the struct accepts.

Maintainability

Using functional options makes the code easier to maintain because we have a clear separation between the initialization code and the business logic. We can also easily add new options and modify existing ones without affecting the rest of the code.

Testability

Using functional options makes the code easier to test because we can easily create different configuration combinations for testing. We can create mocks of the options and test how the struct behaves with different configurations.

Examples

Here is an example of using the option pattern to initialize a struct:

type Server struct {
host string
port int
timeout time.Duration
}

type ServerOption func(*Server)
func WithHost(host string) ServerOption {
return func(s *Server) {
s.host = host
}
}
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) ServerOption {
return func(s *Server) {
s.timeout = timeout
}
}
func NewServer(opts ...ServerOption) *Server {
s := &Server{
host: "localhost",
port: 8080,
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}

In this example, we define a Server struct with three fields: host, port, and timeout. We define ServerOption as a functional option that accepts a pointer to the Server struct.

We also define three options: WithHost, WithPort, and WithTimeout, which return a function that modifies the corresponding field in the Server struct.

In the NewServer constructor, we initialize the struct with default values and then apply the functional options to modify the struct.

Here is an example of using the NewServer constructor with options:

s := NewServer(
WithHost("localhost"),
WithPort(8080),
WithTimeout(30 * time.Second),
)

In this example, we use the NewServer constructor with three options to create a new Server.

You can observe the extensibility of the code, let’s say at a later point of time we need to add another field, like a logger, then using this pattern we can extend this code to add the logger easily as compared to using a NewServer with fixed set of parameters, which would need to be modified again.

The option pattern is an excellent way to make your code more flexible and extensible.

It allows users to create new objects with a custom configuration without having to add more parameters to the constructor.

To summarize, the option pattern is a powerful tool for making code more flexible and extensible.

It allows developers to add new configuration options to a constructor without cluttering it up with a ton of parameters.

It also provides a clean and concise way to initialize structs with multiple fields.

Now that you have a good understanding of the option pattern, try using it in your next project.

It will help you create more flexible and maintainable code.

I hope you found this post helpful. Thank you for reading!

Subscribe to my Youtube channel

Subscribe to my youtube channel if you are on the lookout for more such awesome content in video format.

https://www.youtube.com/@dsysd-dev

Subscribe to my Newsletter

If you like my content, then consider subscribing to my free newsletter, to get exclusive, educational, technical, interesting and career related content directly delivered to your inbox

https://dsysd.beehiiv.com/subscribe

Important Links

Thanks for reading the post, be sure to follow the links below for even more awesome content in the future.

twitter: https://twitter.com/dsysd_dev
youtube: https://www.youtube.com/@dsysd-dev
github: https://github.com/dsysd-dev
medium: https://medium.com/@dsysd-dev
email: dsysd.mail@gmail.com
linkedin: https://www.linkedin.com/in/dsysd-dev/
newsletter: https://dsysd.beehiiv.com/subscribe
gumroad: https://dsysd.gumroad.com/

--

--

dsysd dev

Helping you become an 11x developer. I write on distributed systems, system design, blockchain, and go. https://twitter.com/dsysd_dev