Перейти к содержимому
Теория · Массивы и срезы · урок 8 из 10

Переменное число аргументов

fmt.Println принимает одно значение, три или ни одного, а функция с параметрами a, b int всегда требует ровно два аргумента. Сумму или максимум тоже удобно считать для любого числа значений.

Функция, которая принимает любое число аргументов одного типа, называется вариативной. Перед типом её последнего параметра стоят три точки:

GO
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) возвращает их число. Давайте разберём три вызова из примера:

  1. sum(1, 2, 3) — в nums лежит [1 2 3], цикл складывает три числа, и вызов возвращает 6.
  2. sum(5) — в nums один элемент, и вызов возвращает 5.
  3. 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 не меняет переменные вызывающего кода.

Готовый срез с тремя точками

Если числа уже лежат в срезе, их передают, поставив три точки после имени среза:

GO
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.

Частая ошибка

Срез передают без трёх точек:

GO
s := []int{4, 5, 6}
fmt.Println(sum(s)) // ✗
TEXT
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 и подпишись на канал — откроются все уроки.

Войти другим способом