Variadic Functions

Write a function that takes any number of arguments with ...T, and spread an existing list at the call site with s...

Syntaxfunc sum(nums ...int) int // call as sum(1, 2) or sum(scores...)

Some functions should accept any number of values. fmt.Println is one: fmt.Println("a") and fmt.Println("a", 1, true) both work. A function written that way is variadic. The type of its last parameter is prefixed with three dots.

func sum(nums ...int) int

Callers pass as many ints as they like, or none at all. Inside the function, nums is a slice: a numbered list of values of one type, which range can walk over and len can measure. Slices get a stage of their own later. Called with no arguments, that list has length 0.

Spreading at the call site

If you already hold a list, three dots after it pass its elements as separate arguments:

sum(1, 2, 3)   // three arguments
sum(scores...) // the elements of scores, spread

Without the dots the compiler rejects the call, because a list is not an int. append behaves the same way: append(a, b...) adds the elements of b, where append(a, b) would try to add b itself as one element.

What trips people up

Only the final parameter may be variadic, so func f(a ...int, b string) does not compile. Passing a list where ...any is expected, as in fmt.Println(words), prints one value, the whole list, rather than its elements. And spreading copies nothing: f(s...) hands over your own list, so a function that writes to its variadic parameter is writing to your data.

Example

Example · go
package main

import "fmt"

// nums is variadic: the caller may pass any number of ints.
// Inside the function nums is a slice, a numbered list of ints,
// and len(nums) says how many arrived.
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}

func main() {
	fmt.Println(sum())        // no arguments: nums has length 0
	fmt.Println(sum(1, 2, 3)) // three arguments

	scores := []int{10, 20, 12}
	// scores... spreads the list into separate arguments.
	// Without the three dots this would not compile.
	fmt.Println(sum(scores...))
}

// Output:
// 0
// 6
// 42

When to use it

  • Write a sum or max helper that takes two numbers or twenty.
  • Forward formatting arguments from your own log function through to fmt.Printf.
  • Join one list onto another with append(dst, src...).
  • Accept zero or more search terms from the command line without checking how many there are.

More examples

append spreads, and spreading shares

The first half is the form you will type most: joining one list onto another. The second half is the surprise behind real bug reports. A variadic parameter built from separate arguments is a fresh list, but one built by spreading is the caller's list, so writing to it is visible outside. Copy first if the function needs to modify its input.

Example · go
package main

import "fmt"

// zero sets every value it is handed to 0.
func zero(xs ...int) {
	for i := range xs {
		xs[i] = 0
	}
}

func main() {
	a := []int{1, 2}
	b := []int{3, 4}

	// append is itself variadic. b... spreads b's elements,
	// so this adds 3 and 4 rather than one list.
	a = append(a, b...)
	fmt.Println(a)

	// Spreading hands over the list itself, not a copy,
	// so zero writes through to b.
	zero(b...)
	fmt.Println(b)
}

// Output:
// [1 2 3 4]
// [0 0]

Forwarding variadic arguments

Wrapping a formatting function is the usual reason to write a variadic parameter of your own: take the values and pass them straight on with args... Drop the three dots in the forwarding call and the whole list arrives as a single argument, which prints as [8080] instead of 8080. The two declarations are identical, so recognise both spellings of any.

Example · go
package main

import "fmt"

// args is variadic. "any" is an alias for "interface{}", added in
// Go 1.18: the same type, two spellings.
func logf(level, format string, args ...any) {
	// args... forwards the values on, still as separate arguments.
	fmt.Printf("[%s] %s\n", level, fmt.Sprintf(format, args...))
}

// The same signature written the long way.
func logfLong(level, format string, args ...interface{}) {
	fmt.Printf("[%s] %s\n", level, fmt.Sprintf(format, args...))
}

func main() {
	logf("info", "listening on port %d", 8080)
	logfLong("warn", "retry %d of %d", 2, 5)
}

// Output:
// [info] listening on port 8080
// [warn] retry 2 of 5

Discussion

  • Be the first to comment on this lesson.