JavaRush /Courses /Go SELF /Variadic functions in Go: ...T and scores...

Variadic functions in Go: ...T and scores...

Go SELF
Level 9, Lesson 4
Available

1. Introduction

Sometimes we do not want to fix “exactly two numbers” or “exactly three strings” ahead of time. We want to write a function like “compute the sum of all numbers you were given”—and have it work equally well for sum(10, 20), sum(1, 2, 3, 4, 5) and even sum() (yes, sometimes that happens too).

Variadic functions in Go are like an “elastic pocket” in a backpack: you can put one item, three, or ten in there—the pocket will not mind. The key thing is that all items must have the same type, because Go likes order and cannot stand a “salad” of types without an explicit indication.

Plus, this is not some exotic feature: you have used variadic functions even before you knew the word “variadic”. For example, formatted output fmt.Printf takes a format and a variable number of arguments after it—this is clearly visible in the signature func Printf(format string, a ...interface{}) (n int, err error).

Syntax ...T and declaration rules

A variadic parameter looks like this: name ...T, where T is the element type. The three dots ... read roughly as “and any number of values of this type”.

The most important rule (and it is as simple as it gets): a variadic parameter is always last in the parameter list. This is not a whim, but a necessity: otherwise the compiler cannot understand where the “normal part” of arguments ends and the “elastic” part begins.

Let us compare a regular function and a variadic function in a small table:

What we want What the signature looks like
Exactly two numbers
func add(a, b int) int
“Any number of numbers”
func sum(nums ...int) int
First a required parameter, then a “tail”
func log(prefix string, values ...int)

Let us start with the most classic sum function. This will be the first building block in our small learning app: a “score calculator” (points for tasks + bonuses). Nothing serious, but it is convenient to show variadic without unnecessary magic on this example.

package main

import "fmt"

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

func main() {
	fmt.Println(sum(1, 2, 3)) // 6
}

Note: inside sum we can calmly do for _, v := range nums. This is a normal pattern: a variadic parameter is convenient to iterate with a range loop.

2. Calling variadic functions

Calling a variadic function looks the same as calling a regular one, except you can list a different number of arguments. At this point beginners often think: “So Go lets me break the rules?” No. Go is simply saying: “OK, this parameter may repeat many times.”

It is important to think through (and lock into your head) what a call with no arguments means. For example, “the sum of an empty set” is 0. This is logical and convenient.

Let us expand the example and look at different calls:

package main

import "fmt"

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

func main() {
	fmt.Println(sum())           // 0
	fmt.Println(sum(10))         // 10
	fmt.Println(sum(10, 20, 30)) // 60
}

There is no “special overloading” here. It is the same call to the same function.

Now let us take the next step toward our mini app “score calculator”. Suppose we have a “base” (for example, the main score), and bonuses are optional.

package main

import "fmt"

func totalScore(base int, bonuses ...int) int {
	return base + sum(bonuses...)
}

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

func main() {
	fmt.Println(totalScore(100))           // 100
	fmt.Println(totalScore(100, 5))        // 105
	fmt.Println(totalScore(100, 5, 10))    // 115
	fmt.Println(totalScore(100, 5, 10, 2)) // 117
}

Here we did an important thing for the first time: we forwarded the variadic arguments further via bonuses.... Do not worry—we will cover this in detail in a separate section, but you can already catch the idea: “pass all bonuses as separate arguments”.

4. Variadic inside functions

len, range and empty input

Inside a function, a variadic parameter behaves like a “set of values”: you can get its length via len, iterate with range, check “did anything arrive at all?”. This is very convenient, because variadic functions almost always require explicit behavior for edge cases: 0 values, 1 value, and so on.

Let us write a function average that computes an average. The average for an empty set of numbers is a philosophical thing (you could argue until morning), so we will agree: if there are no numbers, it is an error. This is also a great moment to reinforce the (T, error) contract together with variadic.

package main

import (
	"errors"
	"fmt"
)

func average(nums ...int) (float64, error) {
	if len(nums) == 0 {
		return 0, errors.New("no numbers to average")
	}
	total := sum(nums...)
	return float64(total) / float64(len(nums)), nil
}

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

func main() {
	avg, err := average(10, 20, 30)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(avg) // 20
}

Pay attention to the style: right after the call we check err, and only then use the result.

One more point that is useful to “feel with your hands”: a variadic parameter can be processed like a list, but it does not have to be a “real list”. At a beginner level, it is enough to understand it like this: “the function received N numbers, and I iterate over them with a loop”.

Required parameters and the “tail”

A very common scenario: a function has a few required parameters, and then an optional part. Variadic is perfect for such a “tail”.

For example, we want to print a report for a student: the name is required, and grades/points are however many were provided.

Let us make a function printReport. It will print the name, the number of values, and the average. And to make it look like a “piece of a real app”, let it return an error if there are no scores (otherwise the report is meaningless).

package main

import (
	"errors"
	"fmt"
)

func printReport(name string, scores ...int) error {
	if len(scores) == 0 {
		return errors.New("no scores for report")
	}
	avg, _ := average(scores...) // safe here: we already checked len(scores) > 0
	fmt.Printf("%s: count=%d avg=%.2f\n", name, len(scores), avg)
	return nil
}

func average(nums ...int) (float64, error) {
	if len(nums) == 0 {
		return 0, errors.New("no numbers to average")
	}
	return float64(sum(nums...)) / float64(len(nums)), nil
}

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

func main() {
	_ = printReport("Ann", 10, 20, 30) // Ann: count=3 avg=20.00
}

There are several useful observations here.

First, scores ...int must be last, otherwise the compiler will not understand the boundaries of the “tail”.

Second, we use fmt.Printf as a formatter “printer”. And this is not accidental magic: it has variadic arguments after the format string.

Third, we showed one neat trick: if we have already checked a condition, sometimes it is acceptable to call a function and intentionally ignore the error (but only when you are absolutely sure there will be no error). This is not a “lifehack”, it is simply discipline: you must be able to explain why it is safe.

5. Forwarding variadic arguments: scores...

Very often, variadic functions are built in layers. One function takes a “tail”, does part of the work, and passes that tail further into another function. Then the syntax something... appears in a call.

It looks like this: if you have a variadic parameter scores ...int, then inside the function scores is already a “collected set”, and to pass it as a set of separate arguments into another variadic function, you write scores....

Let us emphasize this with a simple example of a “sum with a label”.

package main

import "fmt"

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

func printSum(label string, nums ...int) {
	fmt.Println(label, sum(nums...))
}

func main() {
	printSum("points:", 5, 10, 3) // points: 18
}

Here printSum does not need to recompute the sum manually. It simply delegates the work to sum, forwarding the entire “tail”.

Why do we even need these ... when forwarding? Because without them you pass “one thing” (the internal representation of the set), and with ... you “expand the set into separate arguments”. At this stage it is enough to remember the practice, not the theory: if you accept ...int, then to pass further into ...int, you will most likely write nums....

Now let us assemble a mini version of our app end-to-end. We will make main that reads a name, three required scores, and the number of bonuses (0, 1 or 2). Yes, this is slightly artificial (because without more advanced topics it is harder to read “an arbitrary number of integers”), but as a learning example it is great: we show calling a variadic function with 0/1/2 arguments.

package main

import (
	"fmt"
)

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

func totalScore(base int, bonuses ...int) int {
	return base + sum(bonuses...)
}

func main() {
	var name string
	var s1, s2, s3 int
	var bonusCount int

	fmt.Scan(&name, &s1, &s2, &s3, &bonusCount)

	base := sum(s1, s2, s3)

	if bonusCount == 0 {
		fmt.Println(name, totalScore(base)) // no bonuses
		return
	}
	if bonusCount == 1 {
		var b1 int
		fmt.Scan(&b1)
		fmt.Println(name, totalScore(base, b1))
		return
	}

	// bonusCount >= 2 (for simplicity we assume the maximum is 2)
	var b1, b2 int
	fmt.Scan(&b1, &b2)
	fmt.Println(name, totalScore(base, b1, b2))
}

Here we carefully keep the “early exit” style. Notice how nicely this reads: totalScore(base, b1, b2) —as if we called a regular function, just with a few more arguments.

6. Common mistakes when working with variadic functions

Mistake #1: trying to put the variadic parameter not last.
A beginner sometimes writes something like func f(nums ...int, label string) and wonders why the compiler complains. Go’s logic here is pragmatic: if ...int is not last, the compiler cannot understand where the numbers ended and the string started. The fix is simple: variadic is always the tail, and everything “required” goes before it.

Mistake #2: not defining behavior for an “empty call”.
If you have sum(nums ...int)—great, an empty call sum() naturally returns 0. But if you have an “average” or a “maximum”, an empty call becomes a problem: what is the maximum of an empty set? Often the correct answer is “this is an error”, and then the signature should become (T, error), and inside you add a check if len(nums) == 0 { ... }. The worst case is when behavior is undefined and in one place you treat “0 is OK”, while in another you treat “0 is already a ready-made answer”.

Mistake #3: forgetting ... when forwarding arguments further.
A typical situation: you wrote func printSum(nums ...int), and inside you want to call sum(nums)—and you hit a compilation error. In those places, remember a simple rule: to “expand” a variadic set and pass it as separate arguments into another variadic function, write nums.... Without the three dots, Go reads it as “I am passing one argument”, not “I am passing many”.

Mistake #4: ignoring err where a variadic function may return an error.
As soon as you make a function like average(nums ...int) (float64, error), you must check the error in a disciplined way. It is very tempting to write avg, _ := average(...) “so it does not get in the way”, especially when it feels like the input is always good. But if the input is ever empty (or you later reuse the function somewhere else), the program will start behaving oddly: you will print 0.00 and think “math is broken”. In practice, it is not math that broke—it is error handling.

Mistake #5: using variadic where the contract is actually fixed.
Sometimes people write func add(nums ...int) int, and then always call add(a, b) and never pass more or less. Formally it works, but readability suffers: by name and by meaning it looks like the function can take many numbers, even though the business logic expects exactly two. It is better to choose variadic only where “a variable count” is truly part of the contract; otherwise you make the code harder to read for your future self (and future you, as we know, can be vindictive and coffee-deprived).

1
Task
Go SELF, level 9, lesson 4
Locked
Base score
Base score
1
Task
Go SELF, level 9, lesson 4
Locked
Quick report
Quick report
1
Task
Go SELF, level 9, lesson 4
Locked
Fair Average
Fair Average
1
Task
Go SELF, level 9, lesson 4
Locked
Game session statistics
Game session statistics
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION