Simple Enough Blog logo
  • Home 
  • Projects 
  • Tags 

  •  Language
    • English
    • Français
  1.   Blogs
  1. Home
  2. Blogs
  3. How to Handle Optional Parameters in Go

How to Handle Optional Parameters in Go

Posted on November 15, 2025 • 6 min read • 1,189 words
Golang   Thibault   Programming  
Golang   Thibault   Programming  
Share via
Simple Enough Blog
Link copied to clipboard

Go does not provide optional parameters or function overloading. Here are the idiomatic patterns, their advantages, and their limitations.

On this page
I. How to Handle Optional Parameters in Go   II. Why Is This an Important Topic?   III. Summary Table: How to Choose the Right Pattern?   1. Function Variants (for Simple Cases)   2. Options Struct: The Most Common Pattern   Why does this work well?   3. Functional Options (...Option)   Complete example:   4. Handling Tri-State Values with Pointers   5. The Traps of nil: Beware of Interfaces   Why an interface nil is not always nil   Key takeaway   IV. Final Table: How to Choose Quickly?   Conclusion   🔗 Useful Links  
How to Handle Optional Parameters in Go
Photo by Thibault Deheurles

I. How to Handle Optional Parameters in Go  

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.


II. Why Is This an Important Topic?  

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:

  • easy to read,
  • simple to extend,
  • stable and backward-compatible,
  • idiomatic Go.

III. Summary Table: How to Choose the Right Pattern?  

Here are the practical recommendations based on common design scenarios:

SituationRecommended Pattern
1–2 simple optional parametersFunction variants
Several options, standard APIOptions struct
Many options, public or evolving APIFunctional 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.


1. Function Variants (for Simple Cases)  

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.

2. Options Struct: The Most Common Pattern  

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,
})

Why does this work well?  

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.


3. Functional Options (...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.

Complete example:  

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(…)).


4. Handling Tri-State Values with Pointers  

Some options must distinguish three states:

  • not provided,
  • explicitly true,
  • explicitly false.

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.


5. The Traps of 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.

Why an interface nil is not always nil  

A Go interface contains:

  • a dynamic type
  • a dynamic value

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 = buf

Internally:

(dynamicType = *bytes.Buffer, dynamicValue = nil)

The interface is not nil because the dynamic type is present.

fmt.Println(w == nil) // false

Consequence: 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 dereference

Since 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

  • Always provide a safe default value when the interface parameter is not supplied:
func Do(w io.Writer) {
    if w == nil {
        w = io.Discard // safe no-op writer
    }
    w.Write([]byte("..."))
}

This avoids:

  • typed nil
  • misleading w == nil checks
  • panics

Key takeaway  

In Go:

  • nil for a pointer is allowed
  • nil for an interface may be non-nil and cause a panic
  • Always prefer a neutral value (io.Discard, default logger, etc.) rather than relying on nil checks with interfaces.

IV. Final Table: How to Choose Quickly?  

Number of OptionsZero-Value AmbiguityPublic API?Recommended Pattern
0–2 optionsnonoFunction variants
3–10nonoOptions struct
3–∞yesnoPointers inside struct
3–∞yes or noyesFunctional Options
Tri-stateyesyes/noPointer-based scalars

Conclusion  

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:

  • prefer options structs for simplicity,
  • use functional options for evolvable public APIs,
  • rely on pointers for tri-state semantics,
  • stay aware of interface nil pitfalls,
  • don’t hesitate to use function variants when the use case is simple.

By applying these principles, your code becomes more readable, stable, maintainable, and idiomatic — improving long-term collaboration and evolution.


🔗 Useful Links  

  • Effective Go (functions chapter)
  • Dave Cheney — Functional Options Pattern
  • Kubernetes codebase examples
  • Go Code Review Comments (zero-values)
 Karpenter: The Intelligent Autoscaler for EKS
Landing Pages: definition, types and best practices to boost your conversions 
  • I. How to Handle Optional Parameters in Go  
  • II. Why Is This an Important Topic?  
  • III. Summary Table: How to Choose the Right Pattern?  
  • 1. Function Variants (for Simple Cases)  
  • 2. Options Struct: The Most Common Pattern  
  • 3. Functional Options (...Option)  
  • 4. Handling Tri-State Values with Pointers  
  • 5. The Traps of nil: Beware of Interfaces  
  • IV. Final Table: How to Choose Quickly?  
  • Conclusion  
  • 🔗 Useful Links  
Follow us

We work with you!

   
Copyright © 2026 Simple Enough Blog All rights reserved. | Powered by Hinode.
Simple Enough Blog
Code copied to clipboard