Переменное число аргументов
fmt.Println принимает одно значение, три или ни одного, а функция с параметрами a, b int всегда требует ровно два аргумента. Сумму или максимум тоже удобно считать для любого числа значений.
Функция, которая принимает любое число аргументов одного типа, называется вариативной. Перед типом её последнего параметра стоят три точки:
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), sum(5), sum()) // 6 5 0
}Представьте камеру хранения, где под вашу кладь выделяют ряд шкафчиков нужной длины: две сумки — два шкафчика, пять сумок — пять. Go поступает так же. Он раскладывает аргументы вызова по шкафчикам нового ряда и отдаёт функции окно на этот ряд, то есть срез.
Параметр — это срез
Внутри функции параметр nums ...int — обычный срез []int. Аргументы вызова попадают в него по порядку, а len(nums) возвращает их число. Давайте разберём три вызова из примера:
sum(1, 2, 3)— вnumsлежит[1 2 3], цикл складывает три числа, и вызов возвращает6.sum(5)— вnumsодин элемент, и вызов возвращает5.sum()— срез пустой, циклrangeне выполняется ни разу, иtotalостаётся0.
Со срезом nums работает всё, что работает с любым срезом: индексы, len, range и append. Здесь аналогия с камерой хранения заканчивается: при вызове без аргументов ряд не строится вовсе, и nums равен nil.
Так устроена и fmt.Println: её единственный параметр принимает любое число значений любого типа. Поэтому вызов fmt.Println() без аргументов допустим и печатает пустую строку.
Обычные параметры перед ...
Go разрешает три точки только у последнего параметра. Перед ним могут стоять обычные: func scale(k int, nums ...int) принимает первое число в k, а остальные в nums. Посмотрим на два вызова:
scale(10, 1, 2, 3)передаёт10вk, а срез[1 2 3]вnums;scale()без аргументов не скомпилируется:kнужен всегда.
Два способа вызова
Вариативную функцию вызывают двумя способами.
Отдельные значения
Вызов sum(1, 2, 3) перечисляет аргументы через запятую. Go строит для них новый ряд, и функция получает окно на него. В этот ряд Go копирует каждый аргумент, поэтому запись в nums не меняет переменные вызывающего кода.
Готовый срез с тремя точками
Если числа уже лежат в срезе, их передают, поставив три точки после имени среза:
s := []int{4, 5, 6}
fmt.Println(sum(s...)) // 15Вызов sum(s...) складывает 4, 5 и 6, и программа печатает 15. Запись s... не копирует элементы: Go не строит новый ряд, а отдаёт функции ваше окно. Параметр nums получает тот же срез s, поэтому запись nums[0] = 0 внутри функции изменила бы и s.
На собеседовании любят спрашивать именно об этом: видит ли вызывающий код изменения, сделанные через вариативный параметр. При передаче s... видит.
У этого способа два правила:
- срез может быть пустым или
nil: тогдаnumsтоже пуст, иsumвозвращает0; - к срезу с тремя точками нельзя добавить отдельные значения:
sum(1, s...)не скомпилируется, аscale(10, s...)компилируется, потому что10попадает вk.
Частая ошибка
Срез передают без трёх точек:
s := []int{4, 5, 6}
fmt.Println(sum(s)) // ✗cannot use s (variable of type []int) as int value in argument to sumКаждый аргумент вариативной функции должен иметь тип int, а s — срез. Компилятор не раскладывает срез на элементы сам. Пишите sum(s...).
Главное
- Параметр
nums ...intвнутри функции — обычный срез[]int; три точки ставят только у последнего параметра. - Готовый срез передают как
sum(s...), и функция получает тот же срез, без копии элементов. - Без трёх точек срез вместо отдельных
intне компилируется.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.