How to Handle Optional Parameters in Go
Posted on November 15, 2025 • 6 min read • 1,189 wordsGo does not provide optional parameters or function overloading. Here are the idiomatic patterns, their advantages, and their limitations.

In most modern languages, you can define default values for function parameters or overload functions to cover various use cases.
Go, however, offers no optional parameters, no overloading, and no default values in function signatures.
Yet the needs remain the same: creating readable, stable APIs that can evolve without breaking users.
This article presents all idiomatic approaches to handling optional parameters in Go, when to use them, how to avoid pitfalls, and how to design a clean API — with decision tables and practical examples inspired by DevOps, Kubernetes, and AWS projects.
Without native optional parameters, naïve Go code quickly becomes unreadable:
NewAPIClient("https://service", token, 0, 0, nil, false)This type of call forces developers to memorize the meaning of each positional argument, creates ambiguity, and makes API evolution difficult.
This is why the Go ecosystem converged toward a few simple, effective patterns to represent options clearly.
The goal is not just to have functional code, but an API that is:
Here are the practical recommendations based on common design scenarios:
| Situation | Recommended Pattern |
|---|---|
| 1–2 simple optional parameters | Function variants |
| Several options, standard API | Options struct |
| Many options, public or evolving API | Functional Options (...Option) |
Ambiguous zero-value (0, false, "") | Functional Options or pointers |
| Need for tri-state (default / true / false) | Pointer to scalar (*bool, *int) |
| Optional resource (e.g., TLS) | Nil-accepted pointer |
These patterns help you avoid combinatorial explosion while keeping APIs clean.
When there’s only one optional parameter or two clearly distinct use cases, the simplest approach is to define two functions.
func NewServer(addr string) (*Server, error) { /* ... */ }
func NewServerTLS(addr string, tlsCfg *tls.Config) (*Server, error) { /* ... */ }This is extremely readable and requires no additional abstraction. However, it becomes limited as options multiply: adding several variants creates unnecessary complexity.
The most idiomatic method in Go is to group all optional parameters into a struct passed into the function. Each field uses its zero-value as default, which keeps the API simple and evolvable.
Example:
type ClientOptions struct {
Timeout time.Duration // 0 = default
Retries int // 0 = default
Logger *log.Logger // nil = log.Default()
InsecureTLS bool // false = default
}
func NewAPIClient(baseURL, token string, opts ClientOptions) (*APIClient, error) {
if opts.Timeout == 0 {
opts.Timeout = 5 * time.Second
}
if opts.Retries == 0 {
opts.Retries = 3
}
if opts.Logger == nil {
opts.Logger = log.Default()
}
return &APIClient{/* ... */}, nil
}Usage:
client, _ := NewAPIClient("https://api.service", token, ClientOptions{
Timeout: 2 * time.Second,
InsecureTLS: true,
})This approach is readable, intuitive, supports auto-completion, and is API-friendly.
You can add fields to the struct without breaking existing calls.
The limitation arises when the zero-value has business meaning (e.g., Timeout = 0 means “no timeout”).
In that case, another pattern is needed.
...Option)
This pattern is used by Kubernetes, Docker, Prometheus, Terraform, and many other major Go projects.
It turns options into configuration functions, allowing for a clean, highly extensible API.
type Option func(*config)
type config struct {
timeout time.Duration
retries int
logger *log.Logger
insecureTLS bool
}
func WithTimeout(d time.Duration) Option {
return func(c *config) { c.timeout = d }
}
func WithRetries(n int) Option {
return func(c *config) { c.retries = n }
}
func WithLogger(l *log.Logger) Option {
return func(c *config) { c.logger = l }
}
func WithInsecureTLS() Option {
return func(c *config) { c.insecureTLS = true }
}
func NewAPIClient(baseURL, token string, opts ...Option) (*APIClient, error) {
cfg := config{
timeout: 5 * time.Second,
retries: 3,
logger: log.Default(),
}
for _, o := range opts {
o(&cfg)
}
return &APIClient{/* ... */}, nil
}Usage
client, _ := NewAPIClient(
"https://api.xyz",
token,
WithTimeout(1*time.Second),
WithRetries(4),
WithInsecureTLS(),
)This pattern is ideal for public, exposed APIs that are meant to last. It avoids ambiguities while remaining elegant at the call site (WithTimeout(…), WithLogger(…)).
Some options must distinguish three states:
In this case, option structs or functional options must rely on pointers to scalar values.
type ClientOptions struct {
EnableCache *bool // nil = default, true/false = explicit
}
func boolPtr(b bool) *bool { return &b }Usage
NewAPIClient("https://api", token, ClientOptions{
EnableCache: boolPtr(false),
})Common in DevOps, Kubernetes, and AWS scenarios where boolean flags often have deeper semantics.
nil: Beware of Interfaces
Accepting nil for pointers is natural in Go.
But accepting nil for interfaces can lead to subtle and dangerous behavior due to typed nils.
nil is not always nil
A Go interface contains:
It is only truly nil if both are nil:
dynamicType = nil, dynamicValue = nil
The trap occurs when a nil pointer is stored inside an interface, for example:
var buf *bytes.Buffer = nil
var w io.Writer = bufInternally:
(dynamicType = *bytes.Buffer, dynamicValue = nil)The interface is not nil because the dynamic type is present.
fmt.Println(w == nil) // falseConsequence: panic risk
Since the interface is not considered nil, code like this :
if w != nil {
w.Write([]byte("hello"))
}Will cause a panic:
panic: runtime error: invalid memory address or nil pointer dereferenceSince Go attempts to call a method on a nil value inside a non-nil interface.
What not to do
Using nil as a business signal for interface parameters
Checking w == nil to detect whether a writer/logger/output exists
Best practice
func Do(w io.Writer) {
if w == nil {
w = io.Discard // safe no-op writer
}
w.Write([]byte("..."))
}This avoids:
w == nil checksIn Go:
nil for a pointer is allowednil for an interface may be non-nil and cause a panicio.Discard, default logger, etc.) rather than relying on nil checks with interfaces.| Number of Options | Zero-Value Ambiguity | Public API? | Recommended Pattern |
|---|---|---|---|
| 0–2 options | no | no | Function variants |
| 3–10 | no | no | Options struct |
| 3–∞ | yes | no | Pointers inside struct |
| 3–∞ | yes or no | yes | Functional Options |
| Tri-state | yes | yes/no | Pointer-based scalars |
Even though Go does not provide optional parameters, it offers several elegant and effective patterns to model flexible, robust APIs.
The key is choosing the right tool for the situation:
By applying these principles, your code becomes more readable, stable, maintainable, and idiomatic — improving long-term collaboration and evolution.