Skip to content

Commit 0bad35a

Browse files
committed
add tutorial post on handling optional parameters in Go with functional options
1 parent f1976a1 commit 0bad35a

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
layout: post
3+
title: "Handling Optional Parameters in Go with Functional Options"
4+
date: 2025-01-14
5+
tags: [go, functional, patterns]
6+
---
7+
8+
Go is a powerful language that values simplicity and clarity, but newcomers might find its lack of support for optional parameters a limitation. For instance, if you're used to languages like Python or JavaScript, creating functions with default values for parameters might seem straightforward. However, with a few idiomatic techniques, Go provides robust alternatives for handling optional parameters.
9+
10+
In this post, we'll explore how to implement optional parameters in Go using the functional options pattern.
11+
12+
## The Problem: Optional Parameters in Go
13+
14+
Consider a scenario where you want to write a JSON response in an HTTP handler. A function for this might look like:
15+
16+
```go
17+
func WriteJSON(w http.ResponseWriter, data any, status int, header http.Header) {
18+
w.Header().Set("Content-Type", "application/json")
19+
for key, values := range header {
20+
for _, value := range values {
21+
w.Header().Add(key, value)
22+
}
23+
}
24+
25+
js, err := json.Marshal(data)
26+
if err != nil {
27+
http.Error(w, "failed to marshal JSON", http.StatusInternalServerError)
28+
return
29+
}
30+
31+
w.WriteHeader(status)
32+
w.Write(js)
33+
}
34+
```
35+
36+
Here, the `status` and `header` parameters can be optional:
37+
- If `status` is not set, it should default to `200`.
38+
- If `header` is not set, it should be ignored.
39+
40+
While you can overload functions in other languages, Go doesn't support this. Fortunately, we can use the **functional options pattern** to address this.
41+
42+
## The Solution: Functional Options Pattern
43+
44+
### Step 1: Define an Options Struct
45+
46+
First, define a struct to hold optional parameters:
47+
48+
```go
49+
type options struct {
50+
status *int
51+
header http.Header
52+
}
53+
```
54+
55+
Here:
56+
- The `status` field is a pointer to an integer, allowing us to detect when it hasn’t been set.
57+
- The `header` field is an `http.Header` to store response headers.
58+
59+
### Step 2: Define Option Type and Implementations
60+
61+
Next, define an `Option` type as a function that modifies the `options` struct:
62+
63+
```go
64+
type Option func(*options) error
65+
66+
func WithStatus(status int) Option {
67+
return func(o *options) error {
68+
if status < 100 || status > 599 {
69+
return fmt.Errorf("invalid status code: %d", status)
70+
}
71+
o.status = &status
72+
return nil
73+
}
74+
}
75+
76+
func WithHeader(header http.Header) Option {
77+
return func(o *options) error {
78+
o.header = header
79+
return nil
80+
}
81+
}
82+
```
83+
84+
These functions allow you to specify the status code or headers in a clean, modular way.
85+
86+
### Step 3: Update the WriteJSON Function
87+
88+
Modify `WriteJSON` to accept a variadic slice of `Option` arguments:
89+
90+
```go
91+
func WriteJSON(w http.ResponseWriter, data any, opts ...Option) {
92+
// Evaluate the options
93+
var options options
94+
for _, opt := range opts {
95+
if err := opt(&options); err != nil {
96+
http.Error(w, err.Error(), http.StatusInternalServerError)
97+
return
98+
}
99+
}
100+
101+
// Set the status code to 200 OK if not specified
102+
status := http.StatusOK
103+
if options.status != nil {
104+
status = *options.status
105+
}
106+
107+
// Set the response headers
108+
w.Header().Set("Content-Type", "application/json")
109+
for k, v := range options.header {
110+
w.Header()[k] = v
111+
}
112+
113+
// Marshal the data to JSON
114+
js, err := json.Marshal(data)
115+
if err != nil {
116+
http.Error(w, "failed to marshal JSON", http.StatusInternalServerError)
117+
return
118+
}
119+
120+
w.WriteHeader(status)
121+
w.Write(js)
122+
}
123+
```
124+
125+
### Step 4: Use the Function
126+
127+
Here’s how you can use the updated `WriteJSON` function:
128+
129+
```go
130+
func handler(w http.ResponseWriter, r *http.Request) {
131+
data := map[string]string{"message": "Hello, world!"}
132+
133+
// Default status and no headers
134+
WriteJSON(w, data)
135+
136+
// Custom status
137+
WriteJSON(w, data, WithStatus(http.StatusCreated))
138+
139+
// Custom status and headers
140+
headers := http.Header{"X-Custom-Header": []string{"value"}}
141+
WriteJSON(w, data, WithStatus(http.StatusAccepted), WithHeader(headers))
142+
}
143+
```
144+
145+
### Advantages of Functional Options
146+
147+
1. **Flexibility**: Easily add new optional parameters without breaking existing function signatures.
148+
2. **Readability**: Clear and descriptive usage with named option constructors.
149+
3. **Error Handling**: Validate inputs when constructing options.
150+
151+
### Considerations
152+
153+
- **Complexity**: The pattern introduces additional code for simple cases.
154+
- **Overhead**: Parsing options adds minor runtime overhead.
155+
156+
## Conclusion
157+
158+
While Go doesn’t support optional parameters natively, the functional options pattern provides an idiomatic and flexible solution. It enables you to write clean, extensible functions that handle optional parameters gracefully.
159+
160+
Give it a try in your next Go project and enjoy the power of functional options!

0 commit comments

Comments
 (0)