Golang Variadic Function: ... Syntax, Varargs, and Slice Examples

Variadic functions in Go use ...T for the last parameter (varargs), behave like []T inside the body, and accept slice arguments with slice.... Compare ... at define vs call, mix fixed parameters, ...any, and common mistakes.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

Golang Variadic Function: ... Syntax, Varargs, and Slice Examples

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 / min examples 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):

go
func sum(nums ...int) int

Inside sum, nums has type []int. Call with discrete values: sum(1, 2, 3) or with none: sum().

Expand a slice at the call site:

go
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.

go
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...))
}
Output

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.

go
func log(prefix string, messages ...string)

Invalid (compiler rejects):

go
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

go
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{}...)
}
Output

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
go
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...))
}
Output

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.

go
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...))
}
Output

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.

go
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)
}
Output

Forwarding variadic arguments

Wrappers pass the tail through with args...:

go
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)
}
Output

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).

go
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))
}
Output

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.


References

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …