10. Функции. Аргументы, return, multiple return values
Функции в компьютерных науках — это фундаментальный механизм обуздания сложности. В языке Go функции являются полноправными объектами первого класса (First-Class Citizens). Вы можете свободно присваивать их переменным, упаковывать в структуры данных, передавать как аргументы в другие функции и возвращать в качестве результатов.
Однако разработчиков, переходящих в Go из C++, Java или C#, поначалу поражает то, чего в языке намеренно нет:
- Нет перегрузки функций (Overloading): Вы принципиально не можете объявить две функции с одинаковым именем в одном пакете, например
Print(int)иPrint(string). - Нет аргументов по умолчанию (Default parameters): Нельзя написать сигнатуру вроде
func process(timeout int = 10).
Это не случайное упущение создателей языка, а результат многолетнего опыта инженеров Bell Labs и Google. В гигантских монорепозиториях перегрузка функций и неявные значения по умолчанию приводят к тому, что при беглом чтении исходного кода (Code Review) человек без помощи IDE не способен однозначно определить, какая именно версия функции будет вызвана и с какими скрытыми параметрами.
Go требует абсолютной инженерной честности: если поведение различается — дайте функциям явные, говорящие имена (PrintInt, PrintString) или спроектируйте лаконичный интерфейс.
Анатомия функции и Multiple Return Values
В классических языках вроде C или Java функция возвращает строго одно значение. Если инженеру требовалось вернуть полезную нагрузку и одновременно статус ошибки, приходилось прибегать к костылям: выбрасывать исключения (Exception), возвращать магические константы или null, либо передавать во входные аргументы указатели ("out-параметры"), загрязняющие сигнатуру.
В Go множественные возвращаемые значения (Multiple Return Values) встроены в ядро системы типов. Это архитектурный базис идиоматичной модели обработки ошибок, делающий поток выполнения кристально чистым:
package main
import "fmt"
// Функция принимает два int и возвращает int и error
func divide(a, b int) (int, error) {
if b == 0 {
// Ошибка - обычное значение, реализующее интерфейс error
return 0, fmt.Errorf("деление на ноль")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Результат:", result)
}
Никаких тяжелых блоков try-catch: ошибка — это обычное возвращаемое значение, которое проверяется сразу же на месте.
flowchart LR
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Caller["Вызывающая функция"]:::process -->|"a в RAX<br>b в RBX"| Func["func divide"]
Func -->|"int в RAX<br>error в RBX+RCX"| Caller
Передача аргументов: Всегда по значению
Один из самых популярных вопросов на техническом собеседовании звучит так: «Как в Go передаются аргументы — по значению или по ссылке?»
Ответ однозначен: в Go абсолютно все аргументы передаются исключительно по значению (Pass by value). В языке физически нет концепции передачи по ссылке (Pass by reference) в том виде, в каком она реализована в C++ (через синтаксис Type&).
Когда вы вызываете функцию и передаете ей аргумент, создается полная побитовая копия этого значения (код копирования генерирует компилятор), и она помещается и помещает ее в целевой регистр процессора или на стековый фрейм вызываемой функции.
func modify(x int) {
x = 100 // Изменяем КОПИЮ, оригинал не пострадает
}
Передача указателей
Если логика программы требует изменить значение переменной в вызывающем коде, вы передаете указатель на нее. Но даже в этом случае механика непреклонна: сам указатель передается по значению:
func modifyPtr(x *int) {
*x = 100 // Разыменовываем копию указателя, меняем оригинал
}
Функция modifyPtr получает копию адреса в памяти (8 байт на 64-битной архитектуре). Эта копия указывает на ту же ячейку, что и оригинальный указатель. Мы копируем записку с адресом дома, а не сам дом.
(Глубокое исследование работы с памятью, указателей и Escape Analysis мы проведем в главе 14. Указатели в Go).
Вариативные аргументы (Variadic parameters)
В ситуациях, когда функция должна принимать произвольное количество параметров (подобно fmt.Println), Go предлагает синтаксис вариативных аргументов с троеточием: ...type. По правилам синтаксиса такой параметр обязан стоять строго последним в сигнатуре:
func sum(prefix string, nums ...int) int {
total := 0
// Внутри функции nums - это обычный срез (slice) типа []int
for _, n := range nums {
total += n
}
return total
}
Внутри тела функции переменная nums ведет себя как стандартный срез []int.
Распаковка слайса (Unpacking)
Если у вас уже сформирован готовый слайс, передать его напрямую в вариативную функцию нельзя — возникнет конфликт типов. Вы должны явно «распаковать» его с помощью суффикса ...:
mySlice := []int{1, 2, 3, 4}
// sum("test", mySlice) // Ошибка компиляции! Ожидается int, а не []int
result := sum("test", mySlice...) // Правильно
Функции как параметры (Колбэки и замыкания)
Статус функций как объектов первого класса раскрывается в паттернах внедрения зависимостей, фильтрации коллекций и паттерне «Стратегия».
В идиоматичном Go принято объявлять тип сигнатуры функции через конструкцию type, повышая выразительность кода:
// Определяем тип функции, которая принимает int и возвращает bool
type FilterFunc func(int) bool
// Функция принимает слайс и функцию-фильтр
func filter(data []int, f FilterFunc) []int {
var result []int
for _, val := range data {
if f(val) {
result = append(result, val)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Передаем анонимную функцию (лямбду) в качестве аргумента
evens := filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4]
}
Передаваемая функция может являться замыканием (closure): она сохраняет доступ к переменным окружающего лексического контекста (например, области видимости функции main), даже когда физически выполняется во фрейме функции filter. Если такие захваченные переменные переживают возврат из функции, компилятор Go (escape analysis) заранее размещает их не на стеке, а в куче (Heap).
Итог
- Никакой магии: Отсутствие перегрузки функций и параметров по умолчанию делает Go-код предельно прямолинейным и прозрачным для чтения без IDE.
- Multiple Return Values: Возврат нескольких значений избавляет систему от громоздких конструкций
try-catchи делает ошибку полноправным значением первого класса. - Register ABI: Начиная с Go 1.17 (на amd64; с Go 1.18 — и на arm64, ppc64), передача параметров и возврат результатов выполняются через регистры CPU (
RAX,RBX,RCX…), обеспечивая максимальную аппаратную производительность. - Pass by value: В Go всё передается строго по значению. Указатель — это копия адреса. Слайс — это копия 24-байтового заголовка
SliceHeader. - Вариативные параметры (
...T): Внутри функции представлены срезом. Анализ побега памяти (Escape Analysis) позволяет компилятору размещать их прямо на стеке.
Мы изучили базовую механику работы с функциями. Но в арсенале Go есть еще два критически важных инструмента управления жизненным циклом функций и освобождением ресурсов: именованные возвращаемые переменные и отложенные вызовы defer. Их внутренней работе посвящена следующая статья: 11. Именованные возвращаемые значения и defer.