Перейти к содержимому
Теория · Функции как значения · урок 1 из 12

Функции как значения

Чему вы научитесь

Чему вы научитесь:

  • передавать функцию аргументом, хранить её в переменной и возвращать из другой функции;
  • объяснять, что такое замыкание и почему оно видит переменные вокруг себя;
  • откладывать вызов до выхода из функции через defer и знать порядок отложенных вызовов;
  • останавливать панику через recover.
Сортировочная машина SortFunc: в щели карточка с правилом byLen, плитки выходят от короткой к длинной

Представьте карточку с рецептом. Её можно отдать другому человеку, и он приготовит блюдо по вашим правилам, хотя сам рецепт не придумывал. Карточку можно убрать в коробку и достать позже. Сама карточка — не блюдо, а инструкция, как его сделать.

В Go функцию тоже можно передать другому коду, как такую карточку. Функция slices.Sort упорядочивает строки по алфавиту, а для сортировки по длине нужно другое правило сравнения. Правило — это код, и в Go его передают функции slices.SortFunc обычным аргументом. Представьте slices.SortFunc как сортировочную машину со щелью для карточки. Слова засыпают сверху, карточку с правилом вставляют в щель, и машина раскладывает слова так, как написано на карточке.

Функция как аргумент

slices.SortFunc принимает аргументом другую функцию:

GO
package main

import (
    "fmt"
    "slices"
)

func byLen(a, b string) int { return len(a) - len(b) }

func main() {
    words := []string{"kiwi", "fig", "banana"}
    slices.SortFunc(words, byLen)
    fmt.Println(words) // [fig kiwi banana]
}

Давайте разберём, что здесь происходит:

  1. func byLen(a, b string) int сравнивает две строки по длине и возвращает разницу длин.
  2. words := []string{"kiwi", "fig", "banana"} создаёт срез из трёх слов.
  3. slices.SortFunc(words, byLen) получает byLen и сама вызывает её для пар элементов.
  4. fmt.Println(words) печатает [fig kiwi banana].

Знак результата говорит slices.SortFunc, как расставить пару:

  • отрицательный результат — a должна стоять раньше b;
  • положительный — a должна стоять позже b;
  • ноль — порядок неважен.

Для "fig" и "kiwi" функция byLen даёт 3 - 4, то есть -1, поэтому fig встаёт раньше.

Так одна функция slices.SortFunc сортирует по любому правилу: по длине, по последней букве, по убыванию. Правило меняется, а код сортировки остаётся прежним — меняется только карточка.

Тип функции

Имя функции без скобок — это сама функция, а не её вызов:

  • byLen передаёт карточку;
  • byLen("fig", "kiwi") готовит блюдо прямо сейчас и даёт число.

У функции как у значения есть тип. Тип функции собирают из трёх частей:

  • слово func;
  • типы параметров в скобках: (string, string);
  • тип результата: int.

У byLen это func(string, string) int. Такой тип можно дать переменной:

GO
var cmp func(string, string) int
cmp = byLen
fmt.Println(cmp("fig", "kiwi")) // -1

Разберём по строкам:

  1. var cmp func(string, string) int объявляет переменную с типом функции.
  2. cmp = byLen записывает в неё функцию byLen.
  3. fmt.Println(cmp("fig", "kiwi")) вызывает функцию через переменную и печатает -1, как вызов по имени.

Функцию с другими параметрами или другим результатом компилятор в cmp не запишет.

Значения первого класса

С функцией в Go можно сделать три действия:

  • присвоить переменной;
  • передать аргументом;
  • вернуть из другой функции.

Поэтому функции в Go называют значениями первого класса. На собеседовании часто спрашивают, что это значит, и ответ — эти три действия.

Сравнение функций

Здесь картинка с карточкой заканчивается. Две бумажные карточки можно сравнить, а две функции — нет: byLen == byLen не скомпилируется с ошибкой func can only be compared to nil.

Функцию сравнивают только с nil — нулевым значением переменной вроде cmp, пока в неё ничего не записали.

Функция без имени

Функцию можно записать прямо в выражении, не давая ей имени. Такая запись называется функциональным литералом, или анонимной функцией. Она устроена как обычное объявление функции, только между func и скобкой с параметрами нет имени.

Литерал как аргумент

Литерал пишут прямо на месте аргумента. Вызов slices.SortFunc(words, func(a, b string) int { return len(b) - len(a) }) сортирует слова от длинных к коротким.

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

Литерал как результат

Литерал удобен и для возврата функции из функции:

GO
func pick(op string) func(int, int) int {
    if op == "+" {
        return func(a, b int) int { return a + b }
    }
    return func(a, b int) int { return a * b }
}

func main() {
    f := pick("*")
    fmt.Println(f(3, 4), pick("+")(3, 4)) // 12 7
}

Давайте разберём пример по строкам:

  1. func pick(op string) func(int, int) int объявляет функцию с результатом типа func(int, int) int.
  2. При op == "+" функция pick возвращает литерал, который складывает.
  3. Иначе она возвращает литерал, который умножает.
  4. f := pick("*") получает функцию умножения, поэтому f(3, 4) даёт 12.
  5. pick("+")(3, 4) сначала получает функцию сложения, а затем сразу вызывает её с 3 и 4 — получается 7.

Частая ошибка

Вместо функции передают результат её вызова:

GO
slices.SortFunc(words, byLen()) // ✗
TEXT
not enough arguments in call to byLen

Скобки после byLen вызывают функцию прямо на этой строке, а аргументов для вызова нет. Функции SortFunc нужна сама карточка, чтобы вызывать её для каждой пары. Передавайте имя без скобок: slices.SortFunc(words, byLen).

Главное

  • Функция в Go — значение: её присваивают переменной, передают аргументом и возвращают из функции.
  • Тип функции записывают через func, типы параметров и результата: func(string, string) int.
  • Имя без скобок передаёт саму функцию, а скобки вызывают её на месте.
  • Функциональный литерал — функция без имени, записанная прямо в выражении.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом