Introduction#
Hello everyone! 👋
In this article I’ll present you the options pattern in Golang. The pattern is useful when you want to create a function
that takes different parameters as an option.
Code Study: Building a rocket#
The following code defines a Rocket struct with 3 fields and a NewRocket function which builds an instance
of the Rocket.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Rocket models a rocket.
type Rocket struct {
name string
nose Nose
fuelCapacity int
}
// NewRocket returns a new rocket instance.
func NewRocket(name string, nose Nose, fuelCapacity int) Rocket {
return Rocket{
name: name,
nose: nose,
fuelCapacity: fuelCapacity,
}
}
|
To use the NewRocket function one would have to write something like this:
1
2
3
4
5
| func main() {
rocket := NewRocket("Techno", PointyNose, 100)
println(rocket.String())
}
|
This is straightforward, but what if we’d want the name of the rocket, nose and fuel capacity to be optional parameters?
We could rewrite the NewRocket
function to take pointers instead of literals and if we do so we could build a
default rocket with:
1
2
3
4
5
| func main() {
rocket := NewRocket(nil, nil, nil)
println(rocket.String())
}
|
This is of course not very readable, my eyes start watering only by looking at this code.
When we want to modify the Rocket struct and add a new field, that is also instantiated we’d need to remember the
position in our code and modify all the NewRocket function usages.
So far not using the options pattern for instantiating a Rocket has the following disadvantages:
- Ugly code.
- Hard to modify code.
- Need to refactor every usage of the function.
The Options Pattern#
The Options pattern is straightforward to implement and there are many variations, in this article we’ll explore
one simple variation:
First, define the RocketOptions struct that takes the same configurable fields as the Rocket struct and defines
functions for instantiating them.
The fields are defined as pointers instead of literals because we want to treat the nil
value as not user specified.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| func NewRocketOptions() *RocketOptions {
return &RocketOptions{
name: nil,
nose: nil,
fuelCapacity: nil,
}
}
func (o *RocketOptions) WithName(name string) *RocketOptions {
o.name = &name
return o
}
func (o *RocketOptions) WithNose(nose Nose) *RocketOptions {
o.nose = &nose
return o
}
func (o *RocketOptions) WithFuelCapacity(capacity int) *RocketOptions {
o.fuelCapacity = &capacity
return o
}
|
Next, modify the NewRocket function to use the RocketOptions struct like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // NewRocket returns a new rocket instance.
func NewRocket(options *RocketOptions) Rocket {
var name string = "Unknown"
if options.name != nil {
name = *options.name
}
var nose Nose = PointyNose
if options.nose != nil {
nose = *options.nose
}
var fuelCapacity int = 100
if options.fuelCapacity != nil {
fuelCapacity = *options.fuelCapacity
}
return Rocket{
name: name,
nose: nose,
fuelCapacity: fuelCapacity,
}
}
|
The NewRocket now provides default values for the nil
options and when they are specified it uses the user
provided option value.
Other Variation#
Another variation can be implemented like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| type Nose string
const PointyNose Nose = "POINTY"
const RoundNose Nose = "ROUND"
type rocketOptions struct {
name string
nose Nose
fuelCapacity int
}
type RocketOption func(options *rocketOptions)
// Rocket models a rocket.
type Rocket struct {
options rocketOptions
}
func (r *Rocket) String() string {
return fmt.Sprintf("%s rocket with %s nose and fuel capacity of %d", r.options.name, r.options.nose, r.options.fuelCapacity)
}
// NewRocket returns a new rocket instance.
func NewRocket(options ...RocketOption) Rocket {
rocketOptions := rocketOptions{
name: "Unknown",
nose: PointyNose,
fuelCapacity: 100,
}
for _, option := range options {
option(&rocketOptions)
}
return Rocket{
options: rocketOptions,
}
}
func NameOption(name string) RocketOption {
return func(options *rocketOptions) {
options.name = name
}
}
func main() {
rocket := NewRocket()
println(rocket.String())
otherRocket := NewRocket(NameOption("Cool"))
println(otherRocket.String())
}
|
Conclusion#
We’ve explored how the Options pattern can significantly enhance the design and usability of functions in Golang,
particularly when dealing with multiple optional parameters.
Thank you for reading!