By Gopher Guides and Vinayak Baranwal
A variadic function is a function that accepts zero, one, or more values as a single argument. While variadic functions are not the common case, they can be used to make your code cleaner and more readable. By the end of this tutorial, you will be able to define variadic functions, pass slices with the spread operator, use the any type for type-flexible parameters, and choose between variadic and slice parameters in your own code.
Several functions in the Go standard library use variadic parameters, including fmt.Println, fmt.Printf, append, and log.Printf.
...T and must be the last parameter; the compiler enforces this at compile time.[]T; when zero arguments are passed, that parameter is nil, not an empty slice....) after a slice at the call site unpacks the slice into discrete arguments; you cannot mix spread and discrete arguments in the same call....any (or ...interface{}) allows a variadic function to accept values of mixed types at the cost of compile-time type safety; use type assertions inside the function when needed.nil, so check with len(v) == 0 for “no arguments” and be aware that v == nil is true only when zero arguments were passed.append function is variadic: you can pass discrete elements or spread a slice with append(s, others...).go version to confirm your installed version.If you do not have a module initialized, run the following command in your working directory before running the examples:
- go mod init example.com/variadic
A variadic function is one that accepts zero, one, or more values for a single parameter. In the function signature, that parameter is written with an ellipsis before the type: ...T. For example, fmt.Println is declared as:
func Println(a ...any) (n int, err error)
The any type is an alias for interface{} introduced in Go 1.18. In older codebases you may still see ...interface{}; the behavior is the same.
A function with a parameter preceded by ... is a variadic function. Go compiles variadic arguments into a slice of that type. Inside the function, the parameter is a []T. When the caller passes zero arguments, Go does not allocate an empty slice; the parameter is nil. Code that checks len(names) == 0 behaves the same for both “no arguments” and “empty slice,” but names == nil is true only when zero arguments were passed. This distinction can matter when you pass a pre-existing slice with the spread operator.
The following program calls fmt.Println with zero, one, or more arguments:
package main
import "fmt"
func main() {
fmt.Println()
fmt.Println("one")
fmt.Println("one", "two")
fmt.Println("one", "two", "three")
}
Run it:
- go run print.go
Output
one
one two
one two three
The first line is blank because no arguments were passed. The rest show how multiple arguments are printed.
Define a variadic function by putting an ellipsis (...) before the parameter type. The variadic parameter must be the last parameter in the function signature. Go allocates a backing array and wraps the supplied arguments into a slice before passing it to the function. When no arguments are passed for that parameter, the parameter is nil, not []string{}.
Example: a function that greets a list of names:
package main
import "fmt"
func main() {
sayHello()
sayHello("Sammy")
sayHello("Sammy", "Jessica", "Drew", "Jamie")
}
func sayHello(names ...string) {
for _, n := range names {
fmt.Printf("Hello %s\n", n)
}
}
The parameter names is variadic (...string). Inside the function it is a slice of strings. The first call passes no arguments, so names is nil and the loop runs zero times.
- go run hello.go
OutputHello Sammy
Hello Sammy
Hello Jessica
Hello Drew
Hello Jamie
Handling the zero-argument case explicitly:
package main
import "fmt"
func main() {
sayHello()
sayHello("Sammy")
sayHello("Sammy", "Jessica", "Drew", "Jamie")
}
func sayHello(names ...string) {
if len(names) == 0 {
fmt.Println("nobody to greet")
return
}
for _, n := range names {
fmt.Printf("Hello %s\n", n)
}
}
- go run hello.go
Outputnobody to greet
Hello Sammy
Hello Sammy
Hello Jessica
Hello Drew
Hello Jamie
Using a variadic parameter can improve readability at the call site. Compare a slice-based join with a variadic one. First, the version that takes a slice:
package main
import "fmt"
func main() {
var line string
line = join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"})
fmt.Println(line)
line = join(",", []string{"Sammy", "Jessica"})
fmt.Println(line)
line = join(",", []string{"Sammy"})
fmt.Println(line)
}
// Slice parameter: caller must wrap values in a []string literal
func join(del string, values []string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join.go
OutputSammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy
Callers must wrap every list in a slice. The variadic version allows discrete arguments:
package main
import "fmt"
func main() {
var line string
line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
fmt.Println(line)
line = join(",", "Sammy", "Jessica")
fmt.Println(line)
line = join(",", "Sammy")
fmt.Println(line)
}
// Variadic parameter: caller passes discrete string arguments
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join.go
OutputSammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy
Both versions behave the same; the variadic function signature makes the call site cleaner when you have discrete values.
A function may have only one variadic parameter, and it must be the last parameter. Any other order causes a compile error. This rule is enforced at compile time; there is no way to work around it with reflection or type tricks.
Example that does not compile:
package main
import "fmt"
func main() {
line := join(",", "Sammy", "Jessica")
fmt.Println(line)
}
// Error: variadic parameter is not last — this does not compile
func join(values ...string, del string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join_bad.go
Output./join_bad.go:11:28: syntax error: cannot use ... with non-final parameter values
When defining a variadic function, only the last parameter can be variadic.
A variadic parameter must always be the last parameter in a function signature.
Placing it in any other position causes a compilation error:
syntax error: cannot use ... with non-final parameter
You can pass zero, one, or more arguments to a variadic function. When you already have a slice, you must unpack it at the call site with the spread operator (...); otherwise the compiler expects discrete arguments of type T, not []T.
Without the spread operator, passing a slice is a type error:
package main
import "fmt"
func main() {
names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
line := join(",", names)
fmt.Println(line)
}
// Error: passing []string directly to a variadic parameter — this does not compile
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join_no_spread.go
Output./join_no_spread.go:10:14: cannot use names (type []string) as type string in argument to join
Suffix the slice with ... to turn it into discrete arguments:
package main
import "fmt"
func main() {
names := []string{"Sammy", "Jessica", "Drew", "Jamie"}
line := join(",", names...)
fmt.Println(line)
}
// Correct: spread operator unpacks the slice into discrete arguments
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join.go
OutputSammy,Jessica,Drew,Jamie
Passing a nil slice with the spread operator: When you spread a nil slice, the variadic parameter receives nil, not an empty slice. Both result in len(values) == 0, but the parameter will be nil:
package main
import "fmt"
func main() {
var names []string
show(names...)
}
func show(v ...string) {
fmt.Printf("len=%d, isNil=%v\n", len(v), v == nil)
}
- go run spread_nil.go
Outputlen=0, isNil=true
So when zero arguments are passed (or a nil slice is spread), the variadic parameter is nil.
You cannot mix spread and discrete arguments. The spread must be the only source of variadic arguments in that call. The following does not compile:
package main
import "fmt"
func main() {
names := []string{"Sammy", "Jessica"}
line := join(",", names..., "extra")
fmt.Println(line)
}
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join_mix.go
Output./join_mix.go:10:28: syntax error: cannot use ... with trailing arguments
You can combine different call styles across different calls: discrete args in one call, spread in another:
package main
import "fmt"
func main() {
line := join(",", []string{"Sammy", "Jessica", "Drew", "Jamie"}...)
fmt.Println(line)
line = join(",", "Sammy", "Jessica", "Drew", "Jamie")
fmt.Println(line)
line = join(",", "Sammy", "Jessica")
fmt.Println(line)
line = join(",", "Sammy")
fmt.Println(line)
}
// All valid call styles: spread literal, discrete args, and single arg
func join(del string, values ...string) string {
var line string
for i, v := range values {
line = line + v
if i != len(values)-1 {
line = line + del
}
}
return line
}
- go run join.go
OutputSammy,Jessica,Drew,Jamie
Sammy,Jessica,Drew,Jamie
Sammy,Jessica
Sammy
When you need a variadic function to accept values of different types, use ...any (or ...interface{} in older code). The compiler will accept any mix of types; inside the function you treat each value as any and use type assertions or type switches when you need to operate on concrete types. This trades compile-time type safety for flexibility, as with interfaces in Go.
Example: a simple logger that prints each value with its type:
package main
import "fmt"
func main() {
logValues("user", "alice", 42, true)
logValues("config", "env", "prod", 8080)
}
func logValues(prefix string, values ...any) {
fmt.Printf("[%s] ", prefix)
for _, v := range values {
fmt.Printf("%T: %v; ", v, v)
}
fmt.Println()
}
- go run log_values.go
Output[user] string: alice; int: 42; bool: true;
[config] string: env; string: prod; int: 8080;
When you need to act on the concrete type of each value, use a type switch:
package main
import "fmt"
func main() {
describe("count", 42, "hello", true, 3.14)
}
func describe(label string, values ...any) {
fmt.Printf("[%s]\n", label)
for _, v := range values {
switch val := v.(type) {
case string:
fmt.Printf(" string: %q\n", val)
case int:
fmt.Printf(" int: %d\n", val)
case bool:
fmt.Printf(" bool: %v\n", val)
default:
fmt.Printf(" other (%T): %v\n", val, val)
}
}
}
- go run type_switch.go
Output[count]
int: 42
string: "hello"
bool: true
other (float64): 3.14
The default case handles any type you have not explicitly listed.
Use this pattern any time you need different behavior per type rather than
just printing the value.
When you operate on individual values inside the function, you must use type assertions or type switches to get concrete types. Using any (or interface{}) gives flexibility but removes compile-time type safety. For more on types and interfaces, see How to Use Interfaces in Go.
The built-in append is a variadic function. Its signature is:
func append(slice []Type, elems ...Type) []Type
You can append discrete elements or spread another slice. Both patterns are valid:
package main
import "fmt"
func main() {
s := []int{10, 20}
s = append(s, 1, 2, 3)
fmt.Println("discrete:", s)
}
package main
import "fmt"
func main() {
s := []int{10, 20}
others := []int{1, 2, 3}
s = append(s, others...)
fmt.Println("spread:", s)
}
Combined in one runnable example:
package main
import "fmt"
func main() {
s := []int{10, 20}
s = append(s, 1, 2, 3)
fmt.Println("discrete:", s)
s = []int{10, 20}
others := []int{1, 2, 3}
s = append(s, others...)
fmt.Println("spread:", s)
}
- go run append_combined.go
Outputdiscrete: [10 20 1 2 3]
spread: [10 20 1 2 3]
Understanding variadic internals makes append predictable: when you spread a nil slice, no extra elements are appended, and the variadic parameter inside append receives nil.
Use a variadic parameter when callers pass discrete values at the call site. Use a slice parameter when callers already have a collection and will pass it directly, for example when the slice comes from another function’s return value or from unmarshaling JSON.
| Aspect | Variadic (...T) |
Slice ([]T) |
|---|---|---|
| Call-site syntax | Discrete values or one spread slice: f(1, 2, 3) or f(s...) |
Single slice: f(s) |
| Zero-value behavior | No args: parameter is nil. Spread nil slice: parameter is nil. |
Caller can pass nil or empty slice; both are explicit. |
| Spread existing slice | Yes: f(s...) |
N/A; slice is the argument. |
| Compile-time argument count | Flexible; zero or more. | Fixed: one slice argument. |
| Idiomatic use | Callers pass a few discrete values or one slice they unpack. | Callers already have a collection (e.g., from another API). |
A variadic sum that returns 0 when no arguments are passed:
package main
import "fmt"
func main() {
fmt.Println("sum() =", sum())
fmt.Println("sum(1,2,3) =", sum(1, 2, 3))
}
func sum(n ...int) int {
total := 0
for _, v := range n {
total += v
}
return total
}
- go run sum.go
Outputsum() = 0
sum(1,2,3) = 6
Construct a WHERE clause by joining filter strings (e.g., for building queries). Zero filters returns the base; multiple filters are joined with AND:
package main
import "fmt"
func main() {
fmt.Println(buildQuery("SELECT * FROM users"))
fmt.Println(buildQuery("SELECT * FROM users", "active = true"))
fmt.Println(buildQuery("SELECT * FROM users", "active = true", "role = 'admin'"))
}
func buildQuery(base string, filters ...string) string {
if len(filters) == 0 {
return base
}
q := base + " WHERE "
for i, f := range filters {
if i > 0 {
q += " AND "
}
q += f
}
return q
}
- go run build_query.go
OutputSELECT * FROM users
SELECT * FROM users WHERE active = true
SELECT * FROM users WHERE active = true AND role = 'admin'
Q1: What is a variadic function in Go?
A variadic function accepts zero, one, or more values for a single parameter. The parameter is declared with ...T and is implemented as a slice []T inside the function.
Q2: Can a Go function have more than one variadic parameter?
No. A function may have only one variadic parameter, and it must be the last parameter. The compiler reports an error otherwise.
Q3: What is the difference between passing a slice and using the spread operator?
Passing a slice s as f(s) passes one argument of type []T. Using the spread operator f(s...) unpacks the slice into individual T arguments, so the variadic parameter receives those elements. You cannot mix spread and other arguments in the same call.
Q4: What happens when you call a variadic function with no arguments?
The variadic parameter is nil. Its length is 0. Use len(p) == 0 to detect “no arguments”; p == nil is true only when zero arguments were passed (or a nil slice was spread).
Q5: Can you use variadic functions with generics in Go 1.18+?
Yes. You can define a generic variadic function, e.g. func F[T any](v ...T). The variadic parameter is still the last parameter and is implemented as a slice of the type parameter.
Q6: Why does fmt.Println use a variadic parameter?
fmt.Println must accept an arbitrary number of values of any type to print. A variadic parameter of type ...any allows multiple arguments of mixed types in a single call without the caller wrapping them in a slice.
Q7: Is there a performance difference between variadic functions and slice parameters?
In practice the difference is negligible. The compiler turns variadic arguments into a slice; passing a slice directly avoids that step but both are cheap. Choose based on call-site clarity and API design, not micro-optimization.
Q8: Can you append to the variadic parameter slice inside the function?
Yes. The parameter is a slice and you can append to it inside the function. The append affects only the local copy of the slice header; the caller’s original arguments are not changed. You can verify this:
package main
import "fmt"
func main() {
names := []string{"Sammy", "Jessica"}
addName(names...)
fmt.Println("caller slice after call:", names)
}
func addName(names ...string) {
names = append(names, "Drew")
fmt.Println("inside function:", names)
}
- go run append_local.go
Outputinside function: [Sammy Jessica Drew]
caller slice after call: [Sammy Jessica]
The caller’s slice is unchanged because append inside the function
operates on the local slice header. The only exception is if the backing
array has enough capacity to absorb the append without reallocation. In that case the new element is written
into the shared backing array, but the caller’s slice header still has the
original length and will not see the new element unless you explicitly
return the modified slice.
Variadic functions are the right tool when callers naturally pass a variable number of discrete values or when you want a single slice to be unpacked at the call site. Use the spread operator to pass an existing slice, and remember that zero arguments (or spreading a nil slice) make the variadic parameter nil. For mixed types, use ...any and handle type assertions inside the function; for a single type with a variable count, ...T keeps the API clear.
To go further, see How to Define and Call Functions in Go, Understanding Arrays and Slices in Go, Understanding Data Types in Go, How to Use Interfaces in Go, and How to Write Packages in Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Go (or GoLang) is a modern programming language originally developed by Google that uses high-level syntax similar to scripting languages. It is popular for its minimal syntax and innovative handling of concurrency, as well as for the tools it provides for building native binaries on foreign platforms.
Browse Series: 53 tutorials
Gopher Guides is a training and consulting company specializing in Go and Go related technologies. Co-founder:s Mark Bates & Cory LaNou.
Building future-ready infrastructure with Linux, Cloud, and DevOps. Full Stack Developer & System Administrator. Technical Writer @ DigitalOcean | GitHub Contributor | Passionate about Docker, PostgreSQL, and Open Source | Exploring NLP & AI-TensorFlow | Nailed over 50+ deployments across production environments.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.