- Базовый пример: сумма чисел
- Передача готового среза
- Логирование
- SQL-запрос с фильтрами
- Несколько аргументов и variadic
- 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 — []interface{}, значит сюда можно передать значения любых типов.
func Log(message string, details ...interface{}) {
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 ...interface{})
печатает любое количество значений разных типов. А 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
.
Самостоятельная работа
Попрактикуемся в создании функций с переменным числом Аргументов.
Реализуйте функцию JoinStrings(parts ...string) string
, которая объединяет переданные строки через пробел. Если строк нет, функция должна возвращать пустую строку.
Примеры вызова:
fmt.Println(JoinStrings("Go", "это", "круто")) // Go это круто
fmt.Println(JoinStrings()) // ""
fmt.Println(JoinStrings("Привет")) // Привет
Показать решение
package main
import "fmt"
func JoinStrings(parts ...string) string {
if len(parts) == 0 {
return ""
}
res := parts[0]
for _, p := range parts[1:] {
res += " " + p
}
return res
}
func main() {
fmt.Println(JoinStrings("Go", "это", "круто")) // Go это круто
fmt.Println(JoinStrings()) // ""
fmt.Println(JoinStrings("Привет")) // Go это круто
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.