Функция — это именованный кусок логики, который принимает входные данные,
что-то делает и (обычно) возвращает результат. Звучит банально, но в Go у
функций есть несколько черт, которые сразу отличают язык от Python или Java:
несколько возвращаемых значений, идиома value, err, переменное число
аргументов и тот факт, что функция — это полноценное значение, которое
можно положить в переменную, передать в аргумент и вернуть из другой функции.
В этой главе разберём всё по порядку: от обычного объявления до замыканий,
которые «помнят» своё состояние. Это фундамент — почти весь Go про то, как
устроены функции и как они друг другу передаются.
Читается слева направо: ключевое слово func, имя, список параметров в
скобках, затем тип результата, затем тело. Тип параметра пишется после
имени — это общее правило Go (a int, а не int a).
Если у нескольких параметров подряд один тип, его можно указать один раз —
в самом конце группы:
func add(a, b int) int { // a и b оба int return a + b}
Функция без результата просто не указывает тип после скобок:
Аргументы в Go всегда передаются по значению — функция получает копию. Если
передать число или строку и изменить параметр внутри, снаружи ничего не
изменится. (Для срезов, карт и указателей копируется сам «дескриптор» или адрес,
поэтому через них можно менять данные — но это тема других глав.)
package mainimport "fmt"func double(x int) int { return x * 2}func main() { n := 21 fmt.Println("вход:", n) fmt.Println("результат:", double(n)) fmt.Println("n не изменился:", n)}
Главная фишка: функция может вернуть сразу несколько значений. Типы
результатов перечисляются в скобках после параметров.
func divmod(a, b int) (int, int) { return a / b, a % b}
Это не кортеж и не специальная структура — просто несколько значений, которые
вызывающий код разбирает по переменным:
q, r := divmod(17, 5) // q == 3, r == 2
Самое частое применение — пара «результат и ошибка». В Go нет исключений: вместо
того чтобы «бросать» ошибку, функция возвращает её последним значением. Если всё
хорошо — ошибка nil. Это и есть идиома value, err, которую вы будете видеть
буквально везде:
package mainimport ( "errors" "fmt")func safeDiv(a, b int) (int, error) { if b == 0 { return 0, errors.New("деление на ноль") } return a / b, nil}func main() { if res, err := safeDiv(10, 2); err == nil { fmt.Println("10 / 2 =", res) } if _, err := safeDiv(10, 0); err != nil { fmt.Println("ошибка:", err) }}
Вывод:
10 / 2 = 5ошибка: деление на ноль
Обратите внимание на два приёма. Первый — if res, err := ...; err == nil:
объявляем переменные прямо в if и тут же проверяем ошибку, область видимости
ограничена телом if. Второй — _ (пустой идентификатор): так мы явно
говорим «это значение мне не нужно». Игнорировать возвращённую ошибку молча —
плохая привычка; либо обрабатывайте её, либо осознанно отбрасывайте через _.
Результатам можно дать имена прямо в сигнатуре. Тогда они работают как
заранее объявленные переменные, а return без аргументов («голый return»)
вернёт их текущие значения.
func parseRange(total, parts int) (size, remainder int) { size = total / parts remainder = total % parts return // вернёт size и remainder}
Иногда это удобно: имена документируют, что именно возвращается, и хорошо
сочетаются с defer, которому нужно подправить результат перед выходом.
Но злоупотреблять не стоит. В длинной функции «голый return» заставляет
читателя прокручивать код вверх, чтобы понять, что же вернулось. Поэтому
практическое правило простое: именованные возвраты — для коротких функций,
где всё видно с одного экрана. В длинных пишите значения в return явно.
package mainimport "fmt"// minMax возвращает минимум и максимум из трёх чисел.func minMax(a, b, c int) (lo, hi int) { lo, hi = a, a for _, v := range []int{b, c} { if v < lo { lo = v } if v > hi { hi = v } } return // голый return: вернёт lo и hi}func main() { lo, hi := minMax(7, 2, 9) fmt.Println("min =", lo, "max =", hi)}
Иногда заранее неизвестно, сколько аргументов придёт. Тогда последний параметр
объявляют с многоточием — ...T. Внутри функции он виден как обычный срез
[]T, а вызвать можно с любым числом аргументов, включая ноль.
package mainimport "fmt"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 fmt.Println(sum()) // 0 — ноль аргументов это нормально}
Вывод:
6300
А если у вас уже есть готовый срез и его нужно «разложить» в variadic-функцию?
Для этого при вызове ставят ... после среза — это раскрывает его в отдельные
аргументы:
Кстати, знакомый fmt.Println — тоже variadic-функция: её сигнатура
принимает ...interface{}, поэтому ей можно передать сколько угодно значений
любых типов.
В Go функция — это значение первого класса. Её можно присвоить переменной,
у которой будет тип-функция, и потом вызвать через эту переменную:
package mainimport "fmt"func main() { var op func(int, int) int // переменная типа «функция (int,int) int» op = func(a, b int) int { return a + b } fmt.Println("сумма:", op(3, 4)) op = func(a, b int) int { return a * b } fmt.Println("произведение:", op(3, 4))}
Вывод:
сумма: 7произведение: 12
Раз функция — это значение, её можно передавать в другую функцию
аргументом. Это основа множества приёмов: колбэки, стратегии, функции
сортировки. Функцию, которая принимает или возвращает другую функцию, называют
функцией высшего порядка.
package mainimport "fmt"// apply применяет переданную функцию ко всем элементам и печатает результат.func apply(nums []int, f func(int) int) { for _, n := range nums { fmt.Print(f(n), " ") } fmt.Println()}func main() { data := []int{1, 2, 3, 4} apply(data, func(x int) int { return x * x }) // квадраты apply(data, func(x int) int { return x + 10 }) // +10}
Замыкание (closure) — это функция, объявленная внутри другой функции,
которая «захватывает» переменные из окружающего контекста и продолжает иметь к
ним доступ даже после того, как внешняя функция завершилась. Захватываются не
копии значений, а сами переменные — поэтому замыкание может их менять, и эти
изменения сохраняются между вызовами.
Классический пример — счётчик. Внешняя функция объявляет count и возвращает
маленькую функцию, которая при каждом вызове увеличивает его:
package mainimport "fmt"func makeCounter() func() int { count := 0 return func() int { count++ // меняем переменную из внешней области return count }}func main() { next := makeCounter() fmt.Println(next()) // 1 fmt.Println(next()) // 2 fmt.Println(next()) // 3 other := makeCounter() fmt.Println(other()) // 1 — у каждого счётчика своё состояние}
Вывод:
1231
Ключевой момент в последних строках: каждый вызов makeCounter() создаёт
новую переменную count и новое замыкание над ней. Поэтому next и other
независимы — у каждого свой счётчик. Это и делает замыкания удобным способом
«упаковать» состояние без объявления отдельной структуры.
Игнорирование ошибки.res, _ := safeDiv(a, b) молча выбрасывает ошибку,
и если она была — res содержит мусорное нулевое значение. Отбрасывайте
ошибку через _ только когда точно уверены, что она здесь невозможна.
«Голый return» в длинной функции. Технически работает, но читать тяжело:
непонятно, что вернулось. Именованные возвраты — для коротких функций.
Захват переменной цикла в замыкании (до Go 1.22). Если внутри for
создавать замыкания, они захватывают одну и ту же переменную цикла. В
старых версиях Go все замыкания видели её финальное значение. Начиная с
Go 1.22 каждая итерация получает свежую переменную, и проблема ушла — но в
чужом коде вы ещё встретите обходной приём v := v внутри цикла.
Путаница ... в объявлении и при вызове. В сигнатуре nums ...int делает
параметр variadic. При вызове sum(s...) раскрывает готовый срез s в
аргументы. Это две разные роли одного синтаксиса.
Вы научились объявлять функции, возвращать несколько значений, работать с
идиомой value, err, variadic-параметрами и замыканиями. Дальше функции будут
встречаться постоянно: как методы у типов, как обработчики, как горутины.
Идиома value, err — это только начало разговора об ошибках; полноценная их
обработка (оборачивание, errors.Is, собственные типы ошибок) — отдельная
большая тема, ошибки как значения. А замыкания,
запущенные через go, — прямой мостик к
горутинам, где захват переменных из
контекста становится особенно важным (и опасным) приёмом.