14. Variadic functions

📖 Reading · 7 min
💡 Every code box below is live — edit it and hit Run.

You've been calling one since lesson one. fmt.Println("a", "b", "c") takes as many arguments as you feel like giving it. Functions that accept a variable number of arguments are called variadic, and you can write your own.

Writing one

Put ... before the type of the last parameter:

package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))
    fmt.Println(sum(10, 20))
    fmt.Println(sum())
}

Inside the function, nums is an ordinary slice of int — you range over it exactly as you would any slice. Go builds that slice for you from whatever arguments the caller passed.

Called with no arguments at all, nums is an empty slice and sum() returns 0. No special case needed.

Mixing fixed and variadic parameters

The variadic parameter must come last, but it can follow normal ones:

package main

import "fmt"

func join(sep string, parts ...string) string {
    out := ""
    for i, p := range parts {
        if i > 0 {
            out += sep
        }
        out += p
    }
    return out
}

func main() {
    fmt.Println(join("-", "go", "is", "fun"))
    fmt.Println(join(", ", "solo"))
}

Only one variadic parameter is allowed, and it has to be at the end — the compiler needs to know where the fixed arguments stop.

Spreading a slice with ...

What if you already have a slice and want to pass it to a variadic function? Add ... after the argument:

package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    values := []int{4, 8, 15, 16}

    fmt.Println(sum(values...))
    fmt.Println(sum(values[:2]...))
}

sum(values...) means "use this slice as the argument list". Without the dots you'd get a type error — Go would think you're passing one []int where it expected ints.

So ... appears in two places with mirrored meanings: in a declaration it packs arguments into a slice; at a call site it unpacks a slice into arguments.

The slice is shared, not copied

A subtle detail worth knowing before it bites you: when you spread a slice, the function receives the same underlying array, not a copy. Modifying it inside the function modifies the caller's slice:

package main

import "fmt"

func zeroFirst(nums ...int) {
    if len(nums) > 0 {
        nums[0] = 0
    }
}

func main() {
    values := []int{7, 8, 9}
    zeroFirst(values...)
    fmt.Println(values)
}

values is now [0 8 9]. This is slice behaviour, not variadic magic — the Collections module explains exactly why. Passing arguments individually (zeroFirst(7, 8, 9)) builds a fresh slice, so there's nothing of yours to modify.

Where you already use it

The standard library is full of variadic functions:

  • fmt.Println(a ...any) — any number of values, of any type
  • fmt.Printf(format string, a ...any) — fixed format, then the values
  • append(slice, elems ...T) — the most-used one of all
  • strings.Join is not variadic, and takes a slice — because it wants precisely one list

The any in those signatures is Go's name for "a value of any type at all". You'll meet it properly in the Interfaces module.

Your turn

Write longest, which takes any number of strings and returns the longest one (the first, if several tie). Return "" for no arguments. Print the result so the program outputs exactly:

elephant
package main

import "fmt"

// write longest here

func main() {
    fmt.Println(longest("cat", "elephant", "horse"))
}
package main

import "fmt"

func longest(words ...string) string {
    best := ""
    for _, w := range words {
        if len(w) > len(best) {
            best = w
        }
    }
    return best
}

func main() {
    fmt.Println(longest("cat", "elephant", "horse"))
}

Next: functions stop being things you call and start being things you can store, pass around and return.