A variadic function takes zero or more trailing arguments of one type. In Go you declare that with ...T on the last parameter only; inside the function it is a []T. At the call site, ... after a slice expands elements into that variadic slot—different from the definition form. For ordinary parameters and return values first, see Golang function syntax and parameters. For append and slice growth patterns you often combine with variadic calls, see Golang append to slice.
Tested on: Go 1.22, 64-bit Linux. Built-in
max/minexamples need Go 1.21+. Signature-only snippets use{run=false}so the in-page Run control stays off for incomplete code.
Quick answer: two different uses of ...
Define a variadic parameter (varargs):
func sum(nums ...int) intInside sum, nums has type []int. Call with discrete values: sum(1, 2, 3) or with none: sum().
Expand a slice at the call site:
data := []int{1, 2, 3}
sum(data...)Without the dots, sum(data) is a compile-time error: a []int is not the same as three int arguments.
package main
import "fmt"
func sum(nums ...int) int {
t := 0
for _, n := range nums {
t += n
}
return t
}
func main() {
fmt.Println(sum(1, 2, 3))
fmt.Println(sum())
data := []int{2, 4, 6}
fmt.Println(sum(data...))
}You should see 6, 0, and 12 on three lines.
What is a variadic function in Go?
Variadic function syntax
The pattern is func name(fixed types..., values ...T) with at most one variadic parameter and it must be last.
func log(prefix string, messages ...string)Invalid (compiler rejects):
package main
// func bad(values ...string, prefix string) {}
// func bad2(a ...int, b ...string) {}
func main() {}Why variadic parameters must be last
The language packs trailing arguments into one slice; only the tail can grow unboundedly, so nothing may follow it in the parameter list.
Basic variadic function example
Pass zero, one, or many arguments
package main
import "fmt"
func inspect(label string, xs ...int) {
fmt.Printf("%s: len=%d nil=%v\n", label, len(xs), xs == nil)
}
func main() {
inspect("no args")
var nilSlice []int
inspect("nil slice...", nilSlice...)
inspect("empty literal...", []int{}...)
}With no variadic arguments, the parameter is a nil []int (length 0). With []int{}..., the slice is non-nil but still length 0—use len(xs) == 0 when you do not care which empty form you got.
How variadic arguments behave like a slice
You can range, pass to append (see append to a slice), or read len / cap like any []T.
Pass a slice to a variadic function
Use slice... to expand a slice
| Call | Meaning |
|---|---|
sum(1, 2, 3) |
Discrete values; compiler builds a slice for the callee |
sum(nums...) |
One slice supplies all variadic elements |
sum(nums) |
Invalid if sum is ...int—types do not match |
package main
import "fmt"
func sum(xs ...int) int {
t := 0
for _, n := range xs {
t += n
}
return t
}
func main() {
data := []int{2, 4, 5, 7, 8}
fmt.Println(sum(data...))
}You should see 26.
Difference between slice and slice...
A parameter typed []int takes one slice value. A parameter typed ...int takes zero or more int values; data... forwards the elements of data in that shape.
You cannot mix discrete values and s... for the same variadic parameter (sum(1, s...) is illegal). Build one slice first, then sum(append([]int{1}, s...)...) or change the API (e.g. first int, rest ...int).
Variadic function with regular parameters
Fixed parameters come first; the variadic tail is optional at the call site.
package main
import (
"fmt"
"strings"
)
func join(sep string, parts ...string) string {
return strings.Join(parts, sep)
}
func main() {
fmt.Println(join(", ", "a", "b", "c"))
words := []string{"x", "y"}
fmt.Println(join("|", words...))
}You should see a, b, c and x|y style output from Join.
Variadic function with any and fmt
fmt.Println, fmt.Printf, and friends use ...any so each operand can be a different static type. Prefer ...any over ...interface{} in new code; behavior is the same.
Use ...any only when mixed types are really needed. If every value is an int, use ...int so the compiler checks call sites.
package main
import "fmt"
func describe(items ...any) {
for i, v := range items {
fmt.Printf("%d: %T = %v\n", i, v, v)
}
}
func main() {
describe(42, "answer", true)
}Forwarding variadic arguments
Wrappers pass the tail through with args...:
package main
import "fmt"
func inner(prefix string, xs ...int) {
fmt.Println(prefix, xs)
}
func outer(prefix string, xs ...int) {
inner(prefix+":", xs...)
}
func main() {
outer("msg", 1, 2, 3)
}You should see a line like msg: [1 2 3] (slice formatting may vary slightly).
Standard library examples
append — variadic element values or another slice with ...: append(s, 1, 2) or append(s, more...).
max / min (Go 1.21+) — variadic over ordered types: max(3, 1, 4).
package main
import "fmt"
func main() {
s := []int{1, 2}
s = append(s, 3, 4, 5)
more := []int{6, 7}
s = append(s, more...)
fmt.Println(s, max(3, 1, 4))
}You should see the full appended slice and 4.
Mutability when using slice...
If the caller passes data..., the callee’s []T may share the same underlying array as data. Mutating elements in the callee can affect the caller. Discrete arguments still get a new slice header, but elements of reference types are shared as usual—copy if you need isolation.
Variadic function vs slice parameter
| Situation | Prefer |
|---|---|
Callers usually pass literals: max(1,2,3) |
...T |
Callers already hold []User as the main input |
[]T parameter |
| Optional tail of strings | ...string |
| “Must pass a collection” as one value | []T |
Variadic is for convenience at the call site; a slice parameter is often clearer when the value is always a collection.
Generics (short note)
A generic function may be variadic on a type parameter: func first[T any](values ...T) T. The “variadic last” rule still applies. For generics in general, see Golang generics; for spec wording when combining type parameters with ..., see function declarations.
Common mistakes with variadic functions
Putting the variadic parameter before regular parameters
Only func(prefix string, xs ...int) is valid.
Passing a slice without ...
Use data..., not data, when the function expects ...T.
Mixing slice... with extra trailing values
Not allowed for one variadic slot—append into a slice first or change the signature.
Using ...any when a concrete ...T is enough
Keeps type checking at compile time.
Forgetting that zero variadic args yield a nil slice
Prefer len(xs) == 0 unless you truly distinguish nil vs empty.
Go variadic function cheat sheet
| Goal | Pattern |
|---|---|
| Declare varargs | func f(xs ...T) |
| Call with values | f(1, 2, 3) |
| Call with none | f() |
| Pass slice | f(s...) |
| Fixed + variadic | func f(prefix string, xs ...int) |
| Mixed types | func log(v ...any) |
| Forward to another variadic | inner(xs...) |
| Append elements | append(s, a, b) or append(s, t...) |
Which pattern should you use?
| Need | Use |
|---|---|
| Nice literal calls | ...T |
| One collection in, one collection out | Often []T |
| Printf-style | ...any + format string first |
Summary
...T on the last parameter declares a variadic (varargs) function; inside the body it is []T. Callers either pass discrete values (packed into a new slice, or a nil slice when none) or pass s... to reuse one slice as the whole variadic argument—never mix those forms in one call. ... in the signature and ... after a slice at the call site are different jobs. Prefer concrete ...T over ...any when types are uniform; use len for emptiness; watch slice aliasing when forwarding slices. Rules: Passing arguments to ... parameters. Related: Golang function, return multiple values, Golang generics, Golang append to slice.

