Variadic functions

You have been calling a variadic function since the very first lesson. fmt.Println accepts any number of arguments: one, five, or zero. That flexibility is not magic, it is a feature of Go's function declaration syntax called variadic parameters.

Declaring one

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

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

The ...int says "zero or more int values, packed together at this position". Inside the function, nums is a slice (the same collection type you met briefly in the for-range lesson: an ordered sequence you can iterate with range). Slices get a full treatment in the next chapter; for now, you can see exactly what nums looks like inside the function by printing it:

func dumpArgs(nums ...int) {
    fmt.Println(nums)
}

func main() {
    dumpArgs(1, 2, 3)    // [1 2 3]
    dumpArgs(42)         // [42]
    dumpArgs()           // []
}

The caller passes individual int values; inside the function those values arrive as one slice, printed in Go's default slice format (square brackets around space-separated elements). Zero arguments produces an empty slice, not a crash, which is why fmt.Println() with no arguments still compiles and just prints a bare newline.

With that shape in mind, sum works over nums using the range loop you already know. Call it with any number of arguments:

fmt.Println(sum(1, 2, 3))         // 6
fmt.Println(sum(1, 2, 3, 4, 5))   // 15
fmt.Println(sum())                // 0

The variadic parameter must be last

A function can have many ordinary parameters before the variadic one, but at most one variadic parameter, and it has to be last:

func logf(prefix string, values ...int) { ... }   // fine: prefix first, variadic last
func logf(values ...int, prefix string) { ... }   // compile error: variadic must be last

The rule exists so the compiler can tell where the regular parameters end and the variable-length list begins.

Spreading a slice with ...

Sometimes you already have the values in a slice and want to pass them to a variadic function. You spread them with a trailing ... at the call site:

numbers := []int{1, 2, 3, 4, 5}

fmt.Println(sum(numbers...))   // 15

The trailing ... tells Go to unpack the slice and pass each element as a separate argument. Without it, the compiler complains that you are passing a slice where individual int values are expected.

numbers := []int{1, 2, 3}

fmt.Println(sum(numbers))    // compile error: cannot use numbers (variable of type []int) as int value in argument to sum

The ... in the declaration (nums ...int) and the ... at the call site (numbers...) are two sides of the same mechanism: one packs incoming arguments into a slice, the other unpacks a slice back into arguments.

Variadic, or just a slice?

The two options are func f(xs ...int) and func f(xs []int). The right one depends on the shape of a typical call. Reach for variadic when callers most often pass values they wrote directly at the call site, like sum(1, 2, 3) or max(4, 1, 7, 3). Reach for a plain slice parameter when callers usually already have the values collected in a slice, like nums := []int{1, 2, 3} followed by f(nums). If both shapes are common, variadic is often the nicer default because slice-holding callers can still write f(nums...).

Task
  • Write a variadic function called max that takes any number of int arguments and returns the largest one. You can assume at least one argument is always passed.

  • Call it from main in three ways and print each result on its own line:

  • max(4, 1, 7, 3) should print 7

  • max(42) should print 42

  • Declare a slice nums := []int{8, 2, 10, 5} and call max(nums...). It should print 10.

Expected output
7
42
10