Go: Функции

Теория: Variadic-функции

В реальных программах редко бывает так, что мы всегда знаем точное количество входных данных. Иногда это одно значение, иногда пять, иногда ни одного. Если делать отдельные функции под каждый случай — код быстро превратится в кашу.

Чтобы этого избежать, в Go есть variadic-функции — функции с переменным числом аргументов. Они позволяют передавать сколько угодно значений одного типа. Под капотом это всё тот же срез ([]T), просто собранный компилятором.

Базовый пример: сумма чисел

package main

import "fmt"

// Sum принимает любое количество чисел и считает их сумму.
// nums внутри — это обычный []int.
func Sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}

	return total
}

func main() {
	fmt.Println(Sum(1, 2, 3))        // 6
	fmt.Println(Sum(10, 20, 30, 40)) // 100
	fmt.Println(Sum())               // 0 — пустой срез
}

Если не передать ни одного аргумента, nums станет пустым срезом ([]int{}), и цикл просто не выполнится.

Передача готового среза

Часто у нас уже есть []int, и мы хотим передать его в variadic-функцию. В этом случае нужен ... - оператор распаковки среза.

numbers := []int{5, 15, 25}

fmt.Println(Sum(numbers...)) // 45

Если забыть ..., компилятор сообщит об ошибке: cannot use numbers (type []int) as type int in argument to Sum.

Логирование

В логах мы часто пишем сообщение и дополнительные детали, которых может быть 0, 1 или больше. Variadic-функции подходят идеально.

// Log выводит сообщение и список деталей.
// details — []any, значит сюда можно передать значения любых типов.
func Log(message string, details ...any) {
	fmt.Printf("[LOG] %s | details: %v\n", message, details)
}

func main() {
	Log("Ошибка при подключении к БД")
	Log("Ошибка при запросе", 500, "timeout", "retrying...")
}

// [LOG] Ошибка при подключении к БД | details: []
// [LOG] Ошибка при запросе | details: [500 timeout retrying...]

SQL-запрос с фильтрами

Допустим, у нас есть функция, которая собирает условие WHERE из произвольного количества фильтров.

import "strings"

// BuildWhere объединяет фильтры через AND.
// Если фильтров нет — возвращает пустую строку.
func BuildWhere(conditions ...string) string {
	if len(conditions) == 0 {
		return ""
	}

	return "WHERE " + strings.Join(conditions, " AND ")
}

func main() {
	fmt.Println(BuildWhere("age > 18", "country = 'RU'"))
	// WHERE age > 18 AND country = 'RU'
	fmt.Println(BuildWhere())
	// ""
}

Здесь мы не ограничиваем разработчика: он может передать хоть один фильтр, хоть двадцать.

Несколько аргументов и variadic

Variadic-параметр всегда стоит последним в списке аргументов. До него можно указывать обычные параметры, а уже затем вариативные.

// SendEmail принимает обязательный адрес и список получателей в копии.
func SendEmail(to string, cc ...string) {
	fmt.Printf("Письмо: %s (копии: %v)\n", to, cc)
}

func main() {
	SendEmail("user@example.com")
	SendEmail("user@example.com", "boss@example.com", "hr@example.com")
}

// Письмо: user@example.com (копии: [])
// Письмо: user@example.com (копии: [boss@example.com hr@example.com])

Variadic в стандартной библиотеке

Многие знакомые функции устроены именно так. fmt.Println(a ...any) печатает любое количество значений разных типов. А append(slice, elems ...T) добавляет к срезу сразу несколько элементов.

slice := []int{1, 2}
slice = append(slice, 3, 4, 5) // добавляем три элемента сразу
fmt.Println(slice)             // [1 2 3 4 5]

Под капотом: что делает компилятор

Когда мы вызываем:

fmt.Println(Sum(1, 2, 3))

Go превращает это в:

fmt.Println(Sum([]int{1, 2, 3}))

То есть variadic-параметр — это всегда срез. В этот момент создаётся новый срез длиной три, в него копируются все значения, а в функцию передаётся структура среза (pointer, length, capacity). Если у нас уже есть срез и мы пишем f(slice...), то ничего не копируется — передаётся тот же самый срез.

Пустые вызовы

fmt.Println(Sum())

Компилятор подставит:

fmt.Println(Sum([]int{}))

Это значит, что nums будет пустым срезом ([]int{}), а не nil.

Подводные камни

Внутри функции variadic-параметр всегда превращается в срез, поэтому работать с ним нужно как с []T. При передаче готового среза обязательно использовать оператор ..., иначе компилятор выдаст ошибку. И помнить, что в списке аргументов может быть только один variadic-параметр, и он всегда должен быть последним.

Итоги

Variadic-функции в Go — это способ передавать переменное количество аргументов. Они упрощают API функций, когда заранее неизвестно количество данных, и позволяют не писать десятки перегрузок. Такой подход активно используется в стандартной библиотеке — например, в функциях fmt и в append. По сути это обычный срез, собранный автоматически компилятором. Главное — помнить, что variadic-параметр всегда идёт последним и обрабатывается как []T.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845