Variadic Functions in Go
How Go Lets You Accept “Any Number of Arguments” Without Losing Type Safety
“Simplicity is prerequisite for reliability.” — Edsger Dijkstra
I remember the first time I really appreciated variadic functions in Go. I was writing a small logger for a side project, trying to handle messages with different numbers of arguments. My first approach was messy—manually building slices, checking lengths, adding cumbersome loops. Then I discovered that Go lets a function accept any number of arguments naturally. It felt like the language was finally listening to me.
Variadic functions are one of those little Go features that sneak into your code and make it cleaner, more readable, and more ergonomic.
At their core, variadic functions let you write flexible APIs while staying type-safe. They’re also surprisingly straightforward once you understand how Go treats them under the hood.
What a Variadic Function Actually Is
A variadic parameter is written using an ellipsis (...) before the type:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}This function can now be called with:
sum(1, 2, 3)
sum(10)
sum()Behind the scenes, nums becomes a slice of ints. That’s the key: variadic arguments are just syntactic sugar for “create a slice and pass it in.”
How the Compiler Treats Variadic Arguments
This call:
sum(1, 2, 3)is secretly transformed into:
sum([]int{1, 2, 3}...)Go automatically constructs the slice for you. When you pass an existing slice, you must expand it manually:
values := []int{1, 2, 3, 4}
sum(values...) // spread operator requiredThis explicitness is one of Go’s design strengths—it forces you to be clear when you’re spreading slices.
Building a Logger
Variadic functions shine when building “printf-style” APIs:
func Log(format string, vals ...any) {
msg := fmt.Sprintf(format, vals...)
fmt.Println(time.Now().Format(time.RFC3339), msg)
}Now calls feel natural:
Log(”user %s logged in”, username)
Log(”processing %d items”, count)
Log(”error: %v”, err)You get full flexibility without sacrificing structure.
func main() {
method := “POST”
path := “/api/checkout”
status := 201
duration := “150ms”
Log(”Request: %s %s returned %d in %s”, method, path, status, duration)
}
// Output:
// 2025-11-15T15:58:00 Request: POST /api/checkout returned 201 in 150msVariadic Functions with Mixed Parameters
Variadics must be the final parameter, but you can mix fixed and variable values:
func joinWith(sep string, parts ...string) string {
return strings.Join(parts, sep)
}
joinWith(”-”, “a”, “b”, “c”) // “a-b-c”Trying to place a parameter after the variadic one is illegal:
func bad(x ...int, y int) {} // compile errorThis rule keeps call sites unambiguous.
Using Variadics for Options Patterns
Go doesn’t have default parameters, but variadics can mimic optional behaviors. A common pattern is the “options” function:
type ServerOption func(*Server)
func WithPort(p int) ServerOption {
return func(s *Server) { s.Port = p }
}
func NewServer(opts ...ServerOption) *Server {
s := &Server{Port: 8080}
for _, opt := range opts {
opt(s)
}
return s
}Calling it:
srv := NewServer(WithPort(9090))Variadic options keep constructors ergonomic without resorting to many overloads.
Working with Interfaces and Type Safety
You’ll sometimes see variadics declared as ...any or ...interface{}:
func Debug(vals ...any) {
for _, v := range vals {
fmt.Printf(”%#v\n”, v)
}
}This allows mixed types:
Debug(”starting”, 42, true, user)Just note: the more generic your variadic param gets, the more type assertions you’ll eventually need.
Variadic Functions and Performance
Variadics are convenient, but they carry a cost:
Go must allocate a slice to hold the arguments.
For small argument lists, this is negligible. But for tight loops or high-performance code, this may matter.
Example:
for i := 0; i < 1_000_000; i++ {
sum(i) // still allocates a slice of length 1
}If performance matters, you can offer two APIs:
func sumSlice(nums []int) int { ... }
func sum(nums ...int) int { return sumSlice(nums) }So callers can pick the version that avoids allocation.
Variadics Are Not Just “Fancy Slices”
They’re intent.
What makes variadic functions powerful isn’t the syntax. It’s the signal they send to the caller:
zero or more values are allowed
order matters
grouping matters
the function is designed to be flexible
They allow APIs that feel natural while staying explicit. And because Go treats variadics as slices, the underlying behavior remains simple, predictable, and type-safe.
Final Thoughts
Variadic functions are one of those Go features you barely think about… until you realize how much they simplify everyday code. They power formatting functions, logging, error aggregation, command construction, and more. They give you flexibility without giving up clarity.
They’re not complex. They’re not flashy. But they let you shape APIs that feel ergonomic and “just right,” especially when dealing with open-ended input.
Enjoyed this post? Subscribe for more deep-dive Go tutorials, concurrency guides, and engineering insights.
I write weekly, no BS, no filler.



