Back to blog

Sunday, August 16, 2026

Variadic Functions in Go: Common Mistakes and Gotchas

Title card for 'Variadic Functions in Go: Common Mistakes and Gotchas' with a short code example

The last parameter names ...string is a []string inside the function. Passing a, b, c builds a new slice. Passing s... hands over the same slice header. Mix those styles in one call and it does not compile. Forget ... on []any and fmt.Println prints one slice, not the elements.

The failure

This does not compile:

package main

func greet(who ...string) {}

func main() {
    kids := []string{"James", "Jasmine"}
    greet("Mom", kids...) // too many arguments
}

This compiles and mutates the caller's data:

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]
}

And this logs the wrong shape:

kids := []any{"James", "Jasmine"}
fmt.Println(kids...) // James Jasmine
fmt.Println(kids)    // [James Jasmine]

Calling process() with no arguments, where process takes values ...int, gives values == nil, not a non-nil empty slice.

Why this happens

The spec allows either a list of T or one []T followed by .... Not both. There is no place to splice extra values onto a spread.

s... does not copy the array. The function receives the same header. Element writes are visible to the caller. Individual arguments allocate a fresh array, so those writes stay local.

Zero trailing arguments leave the parameter nil. range over nil is fine. But process([]int{}...) passes a non-nil empty slice, so values == nil is false there. If your code cares about "no arguments", check len(values) == 0, which is true in both cases.

A ...any call without ... boxes the slice as one any. Loggers and slog hit this constantly.

The fix

Spread only a complete slice. To prepend values, append first:

greet(append([]string{"Mom", "Dad"}, kids...)...)

Copy if the callee must not mutate:

copied := append([]string(nil), list...)
upper(copied...)

Use s... when you mean elements. Pass s only when you want one argument. Treat nil vs empty explicitly if your API cares. Prefer a []T parameter when callers already own a slice and you do not want the two call styles.