Skip to content

Commit 3724e45

Browse files
committed
publishes post: better options in go
1 parent 9c8ecc7 commit 3724e45

File tree

1 file changed

+92
-0
lines changed

1 file changed

+92
-0
lines changed

content/posts/better-options-in-go.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
---
2+
title: Better options in Go
3+
date : '2025-06-06T15:45:12+09:00'
4+
---
5+
6+
Recently I was updating [fuego](https://github.com/go-fuego/fuego) in a go project and came across a change fuego had made to how you configure a server and I think the pattern provides a compelling argument to stop using structs to define configuration of objects.
7+
8+
Allow me to start from the beginning.
9+
10+
Say there was a library that defined the object `Farmer` that was designed to collect `Apples`, and to create a new instance of a farmer the library provided the idiomatic go function `func NewFarmer() *Farmer`.
11+
12+
Down the line let's suppose there's a new release of the farming library and `Farmer` objects can now collect `Oranges`, but just to make sure the older projects that consume the farming library don't go looking for `Oranges` where there aren't any the library wants to introduce an option `OnlyCollectsApples`. Previously to me adopting this pattern my approach to building this would be by defining the `Farmer` instantiation function as `NewFarmer(opts FarmerOptions) *Farmer` and define `FarmerOptions` like below.
13+
14+
```go
15+
type FarmerOptions struct {
16+
OnlyCollectsApples bool
17+
}
18+
```
19+
20+
The intended use would be for the older projects that don't want to collect `Oranges` to update their calls instantiating `Farmer`s to `farmer := NewFarmer(FarmerOptions{OnlyCollectsApples:true})`.
21+
22+
Now suppose these older projects want to see just what the `OnlyCollectsApples` option actually does. The best shot is to open all references of this option, and trawl through everything to get an idea of *what* actually changes. This new pattern changes all of this.
23+
24+
If we instead define options like below then we can change this for the better, and reduce the cognitive load required to understand *what* any given option does.
25+
26+
```go
27+
func DisableOrangeCollection() func(*FarmerOptions) {
28+
return func(opts *FarmerOptions) {
29+
opts.OnlyCollectApples = true
30+
}
31+
}
32+
```
33+
34+
Now we need to change the `NewFarmer` signature, and introduce the bit that makes all of this work.
35+
36+
```go
37+
func NewFarmer(...opts func(*FarmerOptions)) *Farmer {
38+
config := FarmerOptions{}
39+
for _, option := range opts {
40+
option(&config)
41+
}
42+
return Farmer{
43+
FarmerOptions: config
44+
}
45+
}
46+
```
47+
48+
Now creating a farmer that doesn't collect oranges looks like this `farmer := NewFarmer(DisableOrangeCollection())`.
49+
50+
But, this is where the simplicity of the example starts to belie the value of this new pattern, and to be fair the pattern is overkill for the logic required by this option. To get a better understanding of the pattern's strengths let's look at a real example from fuego - `OptionAddResponse`.
51+
52+
`OptionAddResponse` allows for the user to add a response to a route by http status code. The implementation is below.
53+
54+
```go
55+
func OptionAddResponse(code int, description string, response Response) func(*BaseRoute) {
56+
return func(r *BaseRoute) {
57+
if r.Operation.Responses == nil {
58+
r.Operation.Responses = openapi3.NewResponses()
59+
}
60+
r.Operation.Responses.Set(
61+
strconv.Itoa(code), &openapi3.ResponseRef{
62+
Value: r.OpenAPI.buildOpenapi3Response(description, response),
63+
},
64+
)
65+
}
66+
}
67+
```
68+
69+
The beauty of the new pattern is that when we see `OptionAddResponse(200,"Perfect", nil)` as an option, if we want to understand it, going to definition will drop us right at the logic above, and we can see just how this option is manipulating the `BaseRoute` object `r`.
70+
71+
I've started implementing this pattern in my new pet project altar and am coming across some free wins like being able to nest options [like below](https://github.com/t-monaghan/altar/blob/f2c797771c306d5acf94e0caadf05776dfe1093e/broker/config.go#L17-L31).
72+
73+
```go
74+
func DisableAllDefaultApps() func(*AwtrixConfig) {
75+
return func(cfg *AwtrixConfig) {
76+
defaultApps := []func(*AwtrixConfig){
77+
DisableDefaultTimeApp(),
78+
DisableDefaultWeekdayApp(),
79+
DisableDefaultDateApp(),
80+
DisableDefaultHumidityApp(),
81+
DisableDefaultTempApp(),
82+
DisableDefaultBatteryApp(),
83+
}
84+
for _, fn := range defaultApps {
85+
fn(cfg)
86+
}
87+
}
88+
}
89+
```
90+
91+
So far I'm enjoying the pattern, and looking forward to seeing how it pans out for me as altar becomes more complex, but I'm keen to hear what you think, please let me know in the comments of my post here:
92+

0 commit comments

Comments
 (0)