Представьте, что сдачу из главы о переменных нужно посчитать в трёх местах
программы. Можно трижды скопировать формулу, но тогда исправлять ошибку в ней
тоже придётся трижды. Функция решает это: кусок логики получает имя, принимает
входные данные и возвращает результат. Вы уже вызывали чужие функции —
fmt.Println, strconv.Atoi. Теперь будем писать свои.
У функций в Go есть черты, которых нет в Python или Java: функция может вернуть
несколько значений (отсюда идиома value, err), принимать переменное число
аргументов и сама является значением — её можно положить в переменную,
передать аргументом и вернуть из другой функции. Глава идёт от простого
объявления к замыканиям, которые «помнят» своё состояние; замыкания понадобятся
в последней главе, при запуске горутин.
Читается слева направо: ключевое слово 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. Второй — _ (пустой идентификатор): так мы явно
говорим «это значение мне не нужно». Игнорировать возвращённую ошибку молча —
плохая привычка; либо обрабатывайте её, либо осознанно отбрасывайте через _.
В следующих примерах встречается []int{2, 4} — список целых чисел, который в Go называют срезом. Квадратные скобки без числа задают тип, в фигурных перечислены значения. Индексы начинаются с нуля: nums[0] — первый элемент. for i, v := range nums перебирает элементы: i получает индекс, v — значение. Если индекс не нужен, вместо имени пишут _. Подробно срезы разобраны в главе «Срезы, карты, строки»; здесь хватит этих правил.
Результатам можно дать имена прямо в сигнатуре. Тогда они работают как
заранее объявленные переменные, а 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-функция. Её параметр объявлен как
...any, где any означает «значение любого типа» (подробнее в главе об
интерфейсах). Поэтому ей можно передать сколько
угодно значений разных типов.
В 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
Справа от op = стоит функция без имени — анонимная функция. Она
записывается как обычная, только имя после func пропущено. Именно такую
функцию откладывают в defer func() { ... }(), который мелькнул в прошлой
главе: скобки () в конце сразу её вызывают.
Раз функция — это значение, её можно передавать в другую функцию
аргументом. Это основа множества приёмов: колбэки, стратегии, функции
сортировки. Функцию, которая принимает или возвращает другую функцию, называют
функцией высшего порядка.
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 лежит 0, и дальше программа работает с этим нулём как
с настоящим результатом. Отбрасывайте ошибку через _, только когда точно
знаете, что она здесь невозможна.
«Голый return» в длинной функции. Технически работает, но читать тяжело:
непонятно, что вернулось. Именованные возвраты — для коротких функций.
Захват переменной цикла в замыкании (до Go 1.22). Если внутри for
создавать замыкания, они захватывают одну и ту же переменную цикла. В
старых версиях Go все замыкания видели её финальное значение. Начиная с
версии языка Go 1.22 (директива go в модуле) переменные, объявленные
циклом через :=, создаются заново на каждой итерации. При = с заранее
объявленной переменной она остаётся общей. В чужом коде вы ещё встретите
обходной приём v := v внутри цикла.
Путаница ... в объявлении и при вызове. В сигнатуре nums ...int делает
параметр variadic. При вызове sum(s...) раскрывает готовый срез s в
аргументы. Это две разные роли одного синтаксиса.
Напишите larger(a, b int) int, которая возвращает большее из двух чисел. Вызовите её для (2, 7), (9, 3) и (4, 4). Затем сохраните a := 2, вызовите larger(a, 7) и напечатайте a: изменится ли исходная переменная?
Вторая часть: напишите stats(nums ...int) (lo, hi int). Для непустого списка она возвращает наименьшее и наибольшее число. Проверьте stats(5, 1, 9, 3).
Результаты вызовов — 7, 9, 4; исходная a остаётся 2.
func larger(a, b int) int { if a > b { return a } return b}func stats(nums ...int) (lo, hi int) { lo, hi = nums[0], nums[0] for _, n := range nums { if n < lo { lo = n } if n > hi { hi = n } } return}
При равенстве larger возвращает b, но числовой результат тот же. Параметры — копии аргументов; чтобы обновить a, результат нужно присвоить явно: a = larger(a, 7). stats(5, 1, 9, 3) вернёт 1 9. Пустой вызов stats() упадёт на nums[0]: такой функции нужна проверка len(nums) или ошибка в результате.
Почему функция называется larger, а не max? С Go 1.21 в языке есть встроенные max и min: max(2, 7) работает без всякого объявления. Своя функция с тем же именем «заслонит» встроенную, как переменная в блоке из прошлой главы. Это законно, но сбивает с толку читателя. По той же причине результаты stats названы lo и hi.
Вы можете передавать параметры, возвращать несколько результатов и использовать функции как значения. Следующая глава — указатели: там станет видно, как изменить переменную вызывающего, если простой копии аргумента недостаточно.