Variadic Functions

Understand variadic functions in Go – how to declare and call functions that accept a variable number of arguments, using the ... syntax, spreading slices, and avoiding common pitfalls.

A typical Go function expects a fixed list of parameters. If you want fmt.Println to work with any number of values, or an append that can add multiple elements at once, you need a mechanism that lets the caller supply zero, one, or many arguments of the same type. That mechanism is the variadic parameter.

What a Variadic Parameter Is

A variadic parameter is the final parameter in a function signature, declared by placing three dots (...) before the type. It allows the function to accept any number of arguments – including none – for that last position. Inside the function body, the parameter behaves exactly like a slice of the specified element type.

The most common way to picture it is this: Go collects all the trailing arguments into a slice and hands you that slice inside the function. The caller writes individual arguments, but the implementation sees a slice.

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

Inside the function, it's a slice:

The type of nums inside sum is []int. You can call len(nums), range over it, or pass it to any function that accepts a []int. The only special thing is how the caller supplies the values.

This is not merely syntactic sugar – the conversion from individual arguments into a slice happens at the call site, not inside the function body. The compiler does the work so the function author works with a familiar slice type.

Why Variadic Functions Exist

If Go only supported slices as parameters, callers would need to create a slice manually every time they want to pass a list of values. That adds ceremony:

sum([]int{1, 2, 3})
// vs.
sum(1, 2, 3)

Variadic functions remove that friction. They make calls read naturally, especially when the number of arguments is small or varies from call to call. Many standard library functions (fmt.Println, append, strings.Join after the separator) rely on this pattern precisely because it keeps the calling code clean.

Calling a Variadic Function

There are two ways to supply arguments to a variadic parameter, and they cannot be mixed in the same call.

Passing Individual Arguments

Write the arguments separated by commas, just like any other function call. The compiler packs them into a freshly allocated slice.

sum(5, 10, 15)   // nums becomes []int{5, 10, 15}
sum()             // nums is nil

Calling sum() with no trailing arguments is valid. In that case the variadic parameter is nil – not an empty but non‑nil slice. This matters if the function checks for nil versus length zero, though in most cases ranging over a nil slice works fine and produces zero iterations.

Zero arguments are perfectly fine:

A variadic function handles the empty case naturally without forcing the caller to pass an empty slice literal. This is one reason functions like fmt.Println() can be called with no arguments without errors.

Spreading an Existing Slice

When you already have a slice and want to pass its elements as separate arguments, use the ... suffix after the slice variable. This passes the slice directly – without allocating a new one.

numbers := []int{7, 8, 9}
sum(numbers...)

Behind the scenes, the function receives the very same slice value that was passed. No copy of the underlying array is made. This has an important side effect we'll examine later.

You cannot mix individual arguments and a spread slice:

A call like sum(1, numbers...) does not compile. Go does not allow combining manual arguments with a spread slice for the same variadic parameter. Decide on one style per call.

The Rule: Only the Last Parameter Can Be Variadic

A function may have at most one variadic parameter, and it must appear last in the parameter list.

func greet(prefix string, names ...string) { /* valid */ }
func invalid(names ...string, prefix string) { /* compile error */ }

The reason is mechanical: if the variadic parameter were not last, the compiler would have no way to know where the variadic arguments end and the next parameter begins. Placing it at the end removes any ambiguity.

Variadic Parameter vs. Slice Parameter

At first glance, a variadic parameter looks equivalent to a slice parameter. Compare:

func sumSlice(nums []int) int { ... }
func sumVariadic(nums ...int) int { ... }

The implementation can be identical. The difference is entirely at the call site:

sumSlice([]int{1, 2, 3})   // caller must construct a slice
sumVariadic(1, 2, 3)       // caller passes separate values

With a variadic function the compiler allocates the slice for you when you pass individual arguments. When you spread a pre‑existing slice, no allocation happens and the slice is handed straight through.

Use a slice parameter when the function is part of an API that already works with slices and you want the caller to own the allocation decision. Use a variadic parameter when the natural way to call the function is with a fluid list of values.

How Variadic Arguments Become a Slice

When the caller passes individual arguments, Go creates a slice literal with those values and passes it to the function. The underlying array is new; the function receives a slice value with its own length and capacity exactly matching the argument count.

When the caller spreads a slice with s..., no new slice header or array is created. The function parameter receives the same slice that was passed. This means any mutation of the slice's elements inside the function is visible to the caller.

func upper(names ...string) {
    for i := range names {
        names[i] = strings.ToUpper(names[i])
    }
}
func main() {
    list := []string{"alice", "bob"}
    upper(list...)
    fmt.Println(list) // [ALICE BOB]
}

Spreading a slice shares the underlying array:

If you spread a slice and the function modifies its elements, the original slice reflects those changes. This is not a copy. The behavior is consistent with passing any slice to a function – the slice header is copied, but the array is shared.

When you pass individual arguments, a new array is allocated, so the function cannot inadvertently mutate the caller's data.

Variadic Functions of Any Type

To accept arguments of any type, declare the variadic parameter with ...interface{}. The function receives a []interface{} slice, and you can use type assertions or reflection to inspect the values.

func printAll(vals ...interface{}) {
    for _, v := range vals {
        fmt.Printf("%v (type: %T)\n", v, v)
    }
}
printAll(42, "hello", true)
// Output:
// 42 (type: int)
// hello (type: string)
// true (type: bool)

fmt.Println itself is declared as func Println(a ...interface{}) (n int, err error). The pattern is fundamental to the fmt package.

Built-in Functions That Use Variadic Parameters

Several key standard library functions are variadic. Knowing their signatures helps internalize the pattern:

  • func append(slice []Type, elems ...Type) []Type
  • func Println(a ...interface{}) (n int, err error)
  • func Printf(format string, a ...interface{}) (n int, err error)

append demonstrates an important design: the first parameter is a regular parameter (the slice to append to), and the second is variadic, so you can append multiple elements in one call.

Common Mistakes

Even a simple feature like variadic parameters has edges that trip people up.

Expecting a Copy When Spreading a Slice

As shown above, spreading a slice does not make a defensive copy. If the function mutates the elements, the caller is affected. When that's undesirable, copy the slice before passing it:

copied := make([]string, len(list))
copy(copied, list)
upper(copied...)

Assuming a Non-Nil Slice When No Arguments Are Passed

func process(values ...int) {
    if values == nil {
        fmt.Println("no values")
    }
}
process() // prints "no values"

A variadic parameter with zero arguments is nil, not an empty slice. In most range-based code this makes no difference, but explicit nil checks will trigger.

Trying to Mix Call Styles

s := []int{1, 2}
sum(3, s...) // compile error: too many arguments

The compiler does not allow a mix of individual values and a spread slice in the same variadic position. Convert everything to a slice and spread it, or pass individual arguments only.

When to Use Variadic Functions

Reach for a variadic parameter when the number of inputs naturally varies and providing them as a simple list feels right for the caller. Common scenarios:

  • Summation, multiplication, or any accumulation over a set of values.
  • Loggers or formatters that accept multiple items to print.
  • Functions that build collections (like append).
  • Configuration functions where you supply a list of options.

If the function will always be called with a slice that is built gradually somewhere else, a slice parameter may be clearer because it makes the caller's data structure explicit.

Performance is rarely the deciding factor:

The allocation cost of creating a slice from individual variadic arguments is usually negligible compared to I/O or algorithmic work. Avoid micro‑optimizing away variadic calls unless profiling shows a genuine bottleneck.

Summary

Variadic functions give Go callers a clean syntax for passing a dynamic number of arguments without sacrificing type safety. The final parameter, marked with ..., turns into a slice inside the function, so the implementation uses familiar slice operations. Callers can pass arguments individually or spread an existing slice, and the zero‑argument case is handled by a nil slice.

The only hard restrictions are that the variadic parameter must be the last parameter and you cannot have more than one. These rules keep the language unambiguous and the compiler's job simple.