skip to content

Variadic Functions and Argument Passing

A variadic parameter is just a slice built at the call site, which is why forwarding with s... shares the caller's backing array instead of copying it. Interviewers use this to check that you see through the syntax to the slice underneath.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In Go, what does `func sum(xs ...int)` receive as `xs`, and how do you pass an existing `[]int`?

level: juniorimportance: must knowfreq 78%

answer

  1. sugar at the call site
  2. the body sees one ordinary value
  3. a slice, never an array
  4. three dots after the value, not before
  5. sum(nums...) versus sum(nums)

basics

~10 s

Inside the function, xs is an ordinary []int holding the arguments. Writing sum(1, 2, 3) makes Go build that slice for you; to pass a slice you already hold, spread it with sum(nums...).

solid answer

~40 s

A variadic parameter is a slice with sugar at the call site. `func sum(xs ...int)` lets callers pass any number of `int` arguments, and inside the body `xs` is a plain `[]int` you can index, `range` over and measure with `len`. When you write `sum(1, 2, 3)` the compiler materialises a `[]int{1, 2, 3}` and hands it over. When you already hold a `[]int`, you spread it with `sum(nums...)`, and that form passes the slice itself rather than a copy. Calling `sum()` gives a nil slice: `len(xs)` is 0 and ranging over it does nothing. Only the final parameter may be variadic, and the two call forms cannot be mixed — `sum(1, nums...)` is a compile error. `append` has exactly this shape: `append(dst, elems...)`.

code

go · 12 lines
go
func sum(xs ...int) int {
	total := 0
	for _, x := range xs {
		total += x
	}
	return total
}

nums := []int{1, 2, 3}
sum(1, 2, 3) // 6 - the compiler builds a []int for this call
sum(nums...) // 6 - passes nums itself, no new slice
sum() // 0 - xs is a nil slice here

go deeper

for a junior

Be ready to state that the parameter is a plain slice inside the body, and to show both call forms: loose arguments, and an existing slice spread with three trailing dots.

for a middle

Explain what the compiler does at each call site — builds a fresh slice for loose arguments, hands over the caller's own slice for the spread form — and why only the final parameter may be variadic.

for a senior

Show where the two forms diverge in production: spreading shares the caller's data, so a callee writing through the parameter changes what the caller holds. Say when you would take an explicit slice parameter instead.

for a principal

Own the API question. A variadic signature is easy to call but weakly typed and impossible to extend, since nothing can ever follow it. Argue for when an options struct or an explicit slice is the better exported shape.

## What the declaration means A parameter written with a leading `...` is **variadic**: the caller may supply zero, one or many arguments for it, and the function body sees them as a single slice. ```go func sum(xs ...int) int { total := 0 for _, x := range xs { total += x } return total } ``` The type of `xs` inside `sum` is `[]int` — not an array, not a pointer, not some special argument object. Everything you can do with a slice you can do with `xs`: `len(xs)`, `cap(xs)`, `xs[0]`, `xs[1:]`, `for i, x := range xs`. There is no separate variadic machinery at run time; the `...` exists only so the **call site** can be written without building a slice by hand. ## The two call forms **Loose arguments.** `sum(1, 2, 3)` is the form the sugar exists for. The compiler creates a backing array holding `1, 2, 3`, forms a slice over it, and passes that slice as `xs`. That slice belongs to this call and nobody else holds a reference to it. **Spread.** When the values are already in a `[]int`, you append three dots to the argument: `sum(nums...)`. This is not a conversion and not a copy — the parameter receives the slice header you passed, so `xs` has the same pointer, length and capacity as `nums`. This form is the reason a wrapper function can forward its own variadic parameter onward unchanged. The forms cannot be combined. `sum(1, nums...)` does not compile, and neither does `sum(nums)` — a `[]int` is not an `int`, so it is not a valid loose argument for a `...int` parameter. If you need both, build one slice first and spread that. ## Zero arguments `sum()` is legal and gives a nil slice: `xs == nil` is true, `len(xs)` is 0, and `range` runs zero times. Nothing is allocated. Note the consequence for API design: a callee cannot distinguish "the caller passed nothing" from "the caller spread an empty slice" by length — both are 0 — and distinguishing them by a nil check is fragile, because a caller who spreads an empty-but-non-nil slice and a caller who spreads a nil one behave differently for reasons they will never guess. Do not build meaning on that difference. ## Placement rules Only the **last** parameter may be variadic. `func f(xs ...int, sep string)` is a compile error; the fixed parameters go first, as in `func f(sep string, xs ...int)`. This is why every stdlib function of this shape puts its fixed arguments first — `fmt.Sprintf(format string, a ...any)`, `strings.Join` taking an explicit slice instead, `append(dst []T, elems ...T)`. It also has a design consequence worth knowing early: **you can never add a parameter after a variadic one**, so a variadic signature is a commitment. Once a package exports `func New(opts ...Option)`, every future knob must arrive as another `Option`, not as a new argument. ## `append` is the canonical example The builtin is declared, conceptually, as `append(slice []T, elems ...T) []T`. That explains both spellings you see everywhere: `append(dst, a, b)` supplies loose elements, and `append(dst, src...)` spreads an existing slice. There is one extra special case in the spec: when the destination is a `[]byte` you may spread a `string`, as in `append(b, "hello"...)`, because Go defines that conversion for this position. ## When not to reach for variadic Variadic parameters read well when the argument count is genuinely open and each argument is the same kind of thing: `max(a, b, c)`, a formatting call, a list of options. They read badly when they hide arity that actually matters — a function taking `...string` where exactly two strings are required has thrown away a compile-time check that a two-parameter signature would have given you for free. And a `...any` parameter throws away the element type entirely; the compiler stops helping and mistakes surface at run time. Prefer an explicit `[]T` parameter when the caller always has a slice anyway: it costs the caller three characters and it tells the reader the truth about what is being shared.

  • What is the variadic parameter when the caller passes no arguments at all?
    It is a nil slice: `len(xs)` is 0, `range` over it runs zero times, and `xs == nil` is true. Nothing is allocated for it. The practical consequence is that a callee cannot tell `sum()` apart from a caller spreading an empty slice by length alone, and separating them by a nil check is a fragile thing to build an API on.
  • Can a variadic parameter appear before other parameters?
    No — only the last parameter may be variadic, so `func f(xs ...int, sep string)` does not compile. Put the fixed parameters first: `func f(sep string, xs ...int)`. It is also why you can never add a parameter after a variadic one, which makes a variadic signature a long-term commitment in an exported API.
  • Is `append` a variadic function, and what are its spread forms?
    Yes, its shape is `append(dst []T, elems ...T) []T`. You can pass loose elements, `append(dst, a, b)`, or spread another slice, `append(dst, src...)`. The spec adds one special case: when the destination is a `[]byte` you may spread a string, as in `append(b, "hello"...)`.

The three dots are a packing box. Loose arguments get packed into one box for you; nums... says the items are already boxed, so ship that very box rather than repacking it.

saying these in an interview costs you the question

  • Says the parameter is an array rather than a slice
  • Thinks the spread form copies the slice before the call
  • Writes a manual loop where a spread would do
  • Puts the variadic parameter before other parameters
  • Expects a non-nil empty slice when no arguments are passed
  • Mixes loose and spread arguments in one call
open as a page

In Go, when you forward a slice with `f(s...)`, can the callee's writes to that parameter change the caller's slice?

level: middleimportance: should knowfreq 52%

basics

~20 s

Yes. The three-dot spread passes the caller's slice itself rather than a copy, so both sides index one backing array. Any element the callee writes through the variadic parameter is visible to the caller afterwards.

open as a page

Why can't a Go `[]string` be spread into a `...any` parameter, and what does the workaround cost?

level: middleimportance: should knowfreq 44%

basics

~20 s

The spread requires a slice already assignable to []any, and []string is not: Go's slice types are invariant. You must build a []any and convert element by element, costing one allocation plus one conversion per element.

open as a page

When does a call to a Go `...any` variadic helper allocate, and how would you keep that off a hot path?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Each call site listing loose arguments builds a fresh slice for the variadic parameter, which lands on the heap only if it escapes. Forwarding with args... builds nothing extra; a fixed-arity variant avoids the slice altogether.

open as a page