Mastering Variadic Functions in Go with With() Functional Options
In Go, variadic functions are often introduced using fmt.Println() or sum() examples. But in real-world projects, one of the most powerful uses of variadic functions is the Functional Options Pattern — using With() style helpers for object creation.
This pattern makes your constructors:
- More readable
- Easier to extend
- Safer with built-in validation
What is a Variadic Function in Go?
A variadic function is one that accepts a variable number of arguments. You define it using an ellipsis (...) before the type of the last parameter.
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1,2)) // 3
In this case, numbers is a slice ([]int), allowing flexibility in how many arguments you pass.
Real-Life Use: Functional Options with With()
In the real world, Go projects rarely stay small. Over time, constructors tend to bloat with parameters — maybe it started as a small tool to call an API.
At first, creating your HTTP client was simple:
NewClient("https://api.example.com")
But as requirements pile up, your constructor begins to bloat:
- You need a base URL.
- You add a timeout.
- You pass an API token for authentication.
- You throw in retry counts for reliability.
- Maybe a custom HTTP transport for proxy support.
Now your once-clean call looks like this:
NewClient("https://api.example.com", 5*time.Second, "Bearer token", nil, true, 3)
At first glance… what do these parameters even mean?
- Is nil for headers or a custom transport?
- Is true enabling retries or debugging?
- What happens if you add one more parameter in the middle?
It’s like trying to assemble IKEA furniture without the instruction sheet — the pieces are all there, but you’re constantly flipping through the manual to remember where each one goes.
Solution
Now, imagine refactoring this using the With() functional options pattern.Your client setup suddenly reads like a sentence:
Recommended by LinkedIn
client := NewClient(
WithBaseURL("https://api.example.com"),
WithTimeout(5*time.Second),
WithHeader("Authorization", "Bearer token"),
)
No more guessing:
- Self-documenting — Every option tells you exactly what it’s doing.
- Order-independent — Mix and match without breaking anything.
- Future-proof — Add a new WithRetries(3) without touching existing code.
It’s not just about cleaner code — it’s about code that feels like a conversation, where you can read the setup and instantly understand the intent without hunting down the function signature.
🔗 Want to see a complete working example?
Advantages and Limitations of Variadic Function
Pros
- Cleaner & more readable constructors
- Supports optional arguments without multiple functions
- Easy to extend without breaking existing calls
- Allows per-option validation
Cons
- Slight overhead from extra slice allocation
- Can be misused if too many options are added without structure
Best Practices
- Keep the variadic parameter at the end of the constructor.
- Make each With() self-contained & validated.
- Avoid dumping all logic in New... — let each option handle its own field.
- Document available With() options clearly for other developers.
Conclusion
Variadic functions in Go are much more than a beginner’s trick. When paired with the With() functional options pattern, they become a powerful design tool for building maintainable, scalable, and clean Go APIs.
🔗 Want to see a complete working example? I’ve created a GitHub repository demonstrating:
- HTTP client with retries
- TLS certificate handling
- GET & POST requests with custom headers
💬 Have you used With() in your Go projects? Share your experience in the comments!