JavaRush /Курсы /Go SELF /Variadic‑функции в Go: ...T и scores...

Variadic‑функции в Go: ...T и scores...

Go SELF
9 уровень , 4 лекция
Открыта

1. Введение

Иногда мы заранее не хотим фиксировать «ровно два числа» или «ровно три строки». Нам хочется написать функцию «посчитай сумму всех чисел, которые тебе передали» — и чтобы она работала одинаково хорошо для sum(10, 20), sum(1, 2, 3, 4, 5) и даже sum() (да, иногда и так бывает).

Variadic‑функции в Go — это как «резиновый карман» в рюкзаке: вы можете положить туда один предмет, три или десять — карман не обидится. Главное — чтобы все предметы были одного типа, потому что Go любит порядок и терпеть не может «салат» из типов без явного указания.

Плюс это не какая-то экзотика: вы пользовались variadic ещё до того, как узнали слово «variadic». Например, форматированный вывод fmt.Printf принимает формат и переменное число аргументов после него — это прямо видно в сигнатуре func Printf(format string, a ...interface{}) (n int, err error).

Синтаксис ...T и правила объявления

Variadic‑параметр выглядит так: name ...T, где T — тип элементов. Три точки ... читаются примерно как «и ещё сколько угодно значений этого типа».

Самое важное правило (и оно простое, как табуретка): variadic‑параметр всегда последний в списке параметров. Это не каприз, а необходимость: иначе компилятор не сможет понять, где заканчивается «обычная часть» аргументов и начинается «резиновая».

Сравним обычную функцию и variadic‑функцию в маленькой табличке:

Что хотим Как выглядит сигнатура
Ровно два числа
func add(a, b int) int
«Сколько угодно чисел»
func sum(nums ...int) int
Сначала обязательный параметр, потом «хвост»
func log(prefix string, values ...int)

Давайте начнём с самой классической функции суммы. Это будет первый кирпичик в нашем маленьком учебном приложении: «калькулятор очков» (баллы за задания + бонусы). Ничего серьёзного, но зато на его примере удобно показать variadic без лишней магии.

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
}

Обратите внимание: внутри sum мы спокойно делаем for _, v := range nums. Это нормальный паттерн: variadic‑параметр удобно обходить циклом range.

2. Вызов variadic‑функций

Вызов variadic‑функции выглядит так же, как обычной, только вы можете перечислить разное количество аргументов. В этом месте у новичков часто появляется мысль: «То есть Go позволяет мне нарушать правила?» Нет. Go просто говорит: «Ок, этот параметр может повторяться много раз».

Важно заранее продумать (и зафиксировать в голове), что означает вызов без аргументов. Например, «сумма пустого набора» — это 0. Это логично и удобно.

Давайте расширим пример и посмотрим на разные вызовы:

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
}

Здесь нет никакой «особой перегрузки». Это всё один и тот же вызов одной функции.

Теперь сделаем следующий шаг к нашему мини‑приложению «калькулятор очков». Пусть у нас есть «база» (например, основной балл), а бонусы — опциональны.

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
}

Тут мы впервые сделали важную штуку: прокинули variadic дальше через bonuses.... Не пугайтесь — мы подробно разберём это в отдельном разделе, но идею можно поймать уже сейчас: «передай все бонусы как отдельные аргументы».

4. Variadic внутри функций

len, range и пустой ввод

Внутри функции variadic‑параметр ведёт себя как «набор значений»: у него можно узнать длину через len, можно пройтись range, можно проверить «а вообще хоть что-то пришло?». Это очень удобно, потому что почти всегда variadic‑функции требуют явного поведения для крайних случаев: 0 значений, 1 значение и так далее.

Давайте напишем функцию average, которая считает среднее. Среднее для пустого набора чисел — штука философская (можно спорить до утра), поэтому мы договоримся: если чисел нет, это ошибка. И это отличный момент, чтобы закрепить контракт (T, error) в связке с 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
}

Обратите внимание на стиль: сразу после вызова мы проверяем err, и только потом используем результат.

Ещё один момент, который полезно «прочувствовать руками»: variadic‑параметр можно обрабатывать как список, но он не обязан быть «настоящим списком». На уровне новичка достаточно понимать так: «функция получила N чисел, и я иду по ним циклом».

Обязательные параметры и «хвост»

Очень частый сценарий: у функции есть несколько обязательных параметров, а потом — опциональная часть. Variadic идеально подходит для такого «хвоста».

Например, мы хотим печатать отчёт по студенту: имя — обязательно, оценки/баллы — сколько передали.

Сделаем функцию printReport. Она будет печатать имя, количество значений и среднее. А чтобы было похоже на «кусочек реального приложения», пусть она возвращает error, если баллов нет (иначе отчёт бессмысленный).

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...) // тут безопасно, мы уже проверили 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
}

Здесь сразу несколько полезных наблюдений.

Во-первых, scores ...int обязан стоять последним, иначе компилятор не поймёт границы «хвоста».

Во-вторых, fmt.Printf мы используем как «принтер» с форматированием. И это не случайная магия: у него как раз variadic‑аргументы после строки формата.

В-третьих, мы показали один аккуратный трюк: если мы уже проверили условие, то иногда допустимо дальше вызвать функцию и игнорировать ошибку осознанно (но только когда вы железно уверены, что ошибки не будет). Это не «лайфхак», а просто дисциплина: вы должны уметь объяснить, почему это безопасно.

5. Прокидывание variadic‑аргументов: scores...

Очень часто variadic‑функции строятся слоями. Одна функция принимает «хвост», делает часть работы и передаёт этот хвост дальше в другую функцию. Тогда появляется синтаксис something... при вызове.

Это выглядит так: если у вас есть variadic‑параметр scores ...int, то внутри функции scores — это уже «собранный набор», и чтобы передать его как набор отдельных аргументов в другую variadic‑функцию, вы пишете scores....

Давайте подчеркнём это простым примером на «сумме с подписью».

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
}

Тут printSum не обязан пересчитывать сумму вручную. Он просто делегирует работу sum, прокидывая весь «хвост».

Почему вообще нужен этот ... при прокидывании? Потому что без него вы передаёте «одну штуку» (внутреннее представление набора), а с ... — «разверни набор в отдельные аргументы». На этом этапе достаточно запомнить не теорию, а практику: если принимаешь ...int, то чтобы передать дальше в ...int, скорее всего, напишешь nums....

Теперь соберём мини‑версию нашего приложения целиком. Мы сделаем main, который читает имя, три обязательных балла и количество бонусов (0, 1 или 2). Да, это слегка искусственно (потому что без более продвинутых тем сложнее читать «произвольное количество чисел»), но как учебный пример — отлично: мы покажем вызов variadic с 0/1/2 аргументами.

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)) // без бонусов
		return
	}
	if bonusCount == 1 {
		var b1 int
		fmt.Scan(&b1)
		fmt.Println(name, totalScore(base, b1))
		return
	}

	// bonusCount >= 2 (для простоты считаем, что максимум 2)
	var b1, b2 int
	fmt.Scan(&b1, &b2)
	fmt.Println(name, totalScore(base, b1, b2))
}

Здесь мы аккуратно держим стиль «ранний выход». Обратите внимание, как удобно читается totalScore(base, b1, b2) — как будто мы вызвали обычную функцию, просто аргументов чуть больше.

6. Типичные ошибки при работе с variadic‑функциями

Ошибка №1: попытка поставить variadic‑параметр не последним.
Новичок иногда пишет что-то вроде func f(nums ...int, label string) и удивляется, почему компилятор ругается. Логика у Go здесь очень прагматичная: если ...int не последний, компилятор не сможет понять, где закончились числа и началась строка. Лечится просто: variadic — всегда хвост, а всё «обязательное» ставим до него.

Ошибка №2: не определить поведение для «пустого вызова».
Если у вас sum(nums ...int) — отлично, пустой вызов sum() естественно возвращает 0. Но если у вас «среднее» или «максимум», пустой вызов становится проблемой: чему равен максимум пустого набора? Часто правильный ответ — «это ошибка», и тогда сигнатура должна стать (T, error), а внутри появится проверка if len(nums) == 0 { ... }. Самое неприятное, когда поведение не определено и в одном месте вы считаете, что «0 — ок», а в другом — что «0 — это уже готовый ответ».

Ошибка №3: забыть ... при прокидывании аргументов дальше.
Ситуация типичная: вы написали func printSum(nums ...int), внутри хотите вызвать sum(nums) — и ловите ошибку компиляции. В таких местах нужно помнить простое правило: чтобы «развернуть» variadic‑набор и передать его как отдельные аргументы в другую variadic‑функцию, пишется nums.... Без трёх точек Go воспринимает это как «передаю один аргумент», а не «передаю много».

Ошибка №4: игнорировать err там, где variadic‑функция может вернуть ошибку.
Как только вы сделали функцию вроде average(nums ...int) (float64, error), вам придётся дисциплинированно проверять ошибку. Очень соблазнительно написать avg, _ := average(...) «чтобы не мешало», особенно когда кажется, что вход всегда хороший. Но если вход однажды окажется пустым (или вы позже переиспользуете функцию в другом месте), программа начнёт вести себя странно: вы будете печатать 0.00 и думать, что «математика сломалась». На практике сломалась не математика, а обработка ошибок.

Ошибка №5: использовать variadic там, где контракт на самом деле фиксированный.
Иногда пишут func add(nums ...int) int, а потом всегда вызывают add(a, b) и никогда не передают больше или меньше. Формально это работает, но читаемость страдает: по имени и по смыслу кажется, что функция может принимать много чисел, хотя бизнес‑логика ожидает ровно два. Лучше выбирать variadic только там, где «переменное количество» реально часть контракта, иначе вы усложняете чтение кода будущему себе (а будущий вы, как известно, человек мстительный и без кофе).

1
Задача
Go SELF, 9 уровень, 4 лекция
Недоступна
Базовый счёт
Базовый счёт
1
Задача
Go SELF, 9 уровень, 4 лекция
Недоступна
Быстрый отчёт
Быстрый отчёт
1
Задача
Go SELF, 9 уровень, 4 лекция
Недоступна
Честное среднее
Честное среднее
1
Задача
Go SELF, 9 уровень, 4 лекция
Недоступна
Статистика партии
Статистика партии
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ