Go is not a fully object-oriented language, and some object-oriented patterns are not well-suited for it. However, over the years, Go has developed its own set of patterns. Today, I would like to introduce a common pattern: the Functional Options Pattern.
This article is first published in the medium MPP plan. If you are a medium user, please follow me in medium. Thank you very much.
What is the Functional Options Pattern?
Go does not have constructors like other languages. Instead, it typically uses a New
function to act as a constructor. However, when a structure has many fields that need to be initialized, there are multiple ways to do so. One preferred way is to use the Functional Options Pattern.
The Functional Options Pattern is a pattern for constructing structs in Go. It involves designing a set of expressive and flexible APIs to help configure and initialize the struct.
The Go Language Specification by Uber mentions this pattern:
Functional options are a pattern in which you declare an opaque
Option
type that records information in some internal structure. You accept these variable numbers of options and operate on the complete information recorded by the options on the internal structure.Use this pattern for optional parameters in constructors and other public APIs where you expect these parameters to be extended, especially when there are already three or more parameters on these functions.
An Example
To better understand this pattern, let’s walk through an example.
Let’s define a Server
struct:
|
|
How do we use it?
|
|
But what if we want to extend the configuration options for the Server
? There are generally three approaches:
- Declare a new constructor function for each different configuration option.
- Define a new
Config
struct to store the configuration information. - Use the Functional Options Pattern.
Approach 1: Declare a new constructor function for each different configuration option
This approach involves defining dedicated constructor functions for different options. Let’s say we added two fields to the Server
struct:
|
|
Typically, host
and port
are required fields, while timeout
and maxConn
are optional. We can keep the original constructor function and assign default values to these two fields:
|
|
Then, we can provide two additional constructor functions for timeout
and maxConn
:
|
|
This approach works well for configurations that are unlikely to change frequently. Otherwise, you would need to create new constructor functions every time you need to add a new configuration. This approach is used in the Go standard library, such as the Dial
and DialTimeout
functions in the net
package:
|
|
Approach 2: Use a dedicated configuration struct
This approach is also common, especially when there are many configuration options. Typically, you create a Config
struct that contains all the configuration options for the Server
. This approach allows for easy extension without breaking the API of the Server
, even when adding more configuration options in the future.
|
|
When using this approach, you need to construct a Config
instance first, which brings us back to the original problem of configuring the Server
. If you modify the fields in Config
, you may need to define a constructor function for Config
if the fields are changed to private.
Approach 3: Use the Functional Options Pattern
A better solution is to use the Functional Options Pattern.
In this pattern, we define an Option
function type:
|
|
The Option
type is a function type that takes a *Server
parameter. Then, the constructor function for Server
accepts a variable number of Option
types as parameters:
|
|
How do the options work? We need to define a series of related functions that return Option
:
|
|
To use this pattern, the client code would look like this:
|
|
Adding new options in the future only requires adding corresponding WithXXX
functions.
This pattern is widely used in third-party libraries, such as github.com/gocolly/colly
:
|
|
However, when Uber’s Go Programming Style Guide mentions this pattern, it suggests defining an Option
interface instead of an Option
function type. This Option
interface has an unexported method, and the options are recorded in an unexported options
struct.
Can you understand Uber’s example?
|
|
Summary
In real-world projects, when dealing with a large number of options or options from different sources (e.g., from files or environment variables), consider using the Functional Options Pattern.
Note that in actual work, we should not rigidly apply the pattern as described above. For example, in Uber’s example, the Open
function does not only accept a variable number of Option
parameters because the addr
parameter is required. Therefore, the Functional Options Pattern is more suitable for cases with many configurations and optional parameters.