Report this

What is the reason for this report?

How To Use Variadic Functions in Go

Updated on March 18, 2026
English
How To Use Variadic Functions in Go

Introduction

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.

Key Takeaways

  • In a Go function signature, the variadic parameter uses the syntax ...T and must be the last parameter; the compiler enforces this at compile time.
  • Go compiles variadic arguments into a slice []T; when zero arguments are passed, that parameter is nil, not an empty slice.
  • The spread operator (...) after a slice at the call site unpacks the slice into discrete arguments; you cannot mix spread and discrete arguments in the same call.
  • Using ...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.
  • Prefer a variadic parameter when callers will pass discrete values at the call site; prefer a slice parameter when callers will pass an existing collection.
  • Calling a variadic function with no arguments is valid; the variadic parameter receives nil, so check with len(v) == 0 for “no arguments” and be aware that v == nil is true only when zero arguments were passed.
  • The built-in append function is variadic: you can pass discrete elements or spread a slice with append(s, others...).

Prerequisites

If you do not have a module initialized, run the following command in your working directory before running the examples:

  1. go mod init example.com/variadic

What Is a Variadic Function in Go

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:

print.go
package main

import "fmt"

func main() {
    fmt.Println()
    fmt.Println("one")
    fmt.Println("one", "two")
    fmt.Println("one", "two", "three")
}

Run it:

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

Defining a Variadic Function

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:

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

  1. go run hello.go
Output
Hello Sammy Hello Sammy Hello Jessica Hello Drew Hello Jamie

Handling the zero-argument case explicitly:

hello.go
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)
    }
}
  1. go run hello.go
Output
nobody 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:

join.go
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
}
  1. go run join.go
Output
Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy

Callers must wrap every list in a slice. The variadic version allows discrete arguments:

join.go
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
}
  1. go run join.go
Output
Sammy,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.

Variadic Argument Order

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:

join_bad.go
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
}
  1. 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

Using the Spread Operator to Pass a Slice

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:

join_no_spread.go
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
}
  1. 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:

join.go
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
}
  1. go run join.go
Output
Sammy,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:

spread_nil.go
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)
}
  1. go run spread_nil.go
Output
len=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:

join_mix.go
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
}
  1. 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:

join.go
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
}
  1. go run join.go
Output
Sammy,Jessica,Drew,Jamie Sammy,Jessica,Drew,Jamie Sammy,Jessica Sammy

Variadic Functions with the any Type

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:

log_values.go
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()
}
  1. 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:

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

How the append Function Uses Variadic Arguments

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:

Pattern (a): append discrete elements

append_discrete.go
package main

import "fmt"

func main() {
    s := []int{10, 20}
    s = append(s, 1, 2, 3)
    fmt.Println("discrete:", s)
}

Pattern (b): append a slice with spread

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

append_combined.go
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)
}
  1. go run append_combined.go
Output
discrete: [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.

Variadic Functions vs Slice Parameters

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

Practical Patterns and Real-World Examples

Example (a): Safe zero-argument sum

A variadic sum that returns 0 when no arguments are passed:

sum.go
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
}
  1. go run sum.go
Output
sum() = 0 sum(1,2,3) = 6

Example (b): buildQuery for a SQL WHERE clause

Construct a WHERE clause by joining filter strings (e.g., for building queries). Zero filters returns the base; multiple filters are joined with AND:

build_query.go
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
}
  1. go run build_query.go
Output
SELECT * FROM users SELECT * FROM users WHERE active = true SELECT * FROM users WHERE active = true AND role = 'admin'

Frequently Asked Questions About Variadic Functions in Go

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:

append_local.go
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)
}
  1. go run append_local.go
Output
inside 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.

Conclusion

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.

Learn more about our products

Tutorial Series: How To Code in Go

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.

About the author(s)

Gopher Guides
Gopher Guides
Author
See author profile

Gopher Guides is a training and consulting company specializing in Go and Go related technologies. Co-founder:s Mark Bates & Cory LaNou.

Vinayak Baranwal
Vinayak Baranwal
Editor
Technical Writer II
See author profile

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.

Category:

Still looking for an answer?

Was this helpful?


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!

Creative CommonsThis work is licensed under a Creative Commons Attribution-NonCommercial- ShareAlike 4.0 International License.
Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Start building today

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.