JavaRush /Курсы /Go SELF /Анонимные функции и замыкания

Анонимные функции и замыкания

Go SELF
9 уровень , 5 лекция
Открыта

1. Анонимная функция: «функция без имени»

Иногда вы хотите передать в функцию не только данные, но и инструкцию «что с ними делать».

Пример из жизни очень программистский: вы даёте другу яблоко (данные) и говорите одно из трёх: «помой», «порежь», «съешь и не задавай вопросов» (поведение). Если вы каждый раз пишете гигантский if/else на 50 строк, где перечисляете все возможные варианты действий, код быстро превращается в простыню, на которой можно укрывать небольшую команду разработчиков.

Функции как значения позволяют писать так: есть общий каркас, а конкретное действие вы передаёте параметром‑функцией. Это особенно удобно, когда каркас повторяется, а действие меняется.

Анонимная функция в Go выглядит как обычная, только без имени. Официально это function literal, но по‑человечески — «анонимка».

func(x int) int {
	return x * 2
}

Важная мысль: пока вы написали func(...) { ... }, вы создали значение. Это ещё не выполнение. Выполнение начинается только когда вы добавили скобки вызова.

Немедленный вызов (IIFE по‑простому)

Иногда удобно выполнить маленький блок кода прямо здесь и сейчас.

func() {
	fmt.Println("=== MiniCalc ===")
	fmt.Println("Введите: a op b rule")
}() // скобки в конце означают вызов

Здесь полезно увидеть две пары скобок: первые () — это список параметров функции (он пустой), вторые () — это вызов.

Это же можно сделать с параметрами и результатом.

res := func(x int) int {
	return x * x
}(5)

fmt.Println(res) // 25

Синтаксис выглядит чуть странно только первые два раза в жизни. Потом мозг смиряется (и даже начинает получать удовольствие, но это уже диагноз).

Анонимная функция как «вычислить и присвоить»

Иногда вычисление требует if, но вы не хотите засорять main временными переменными.

x := -10

sign := func(n int) string {
	if n < 0 {
		return "negative"
	}
	if n == 0 {
		return "zero"
	}
	return "positive"
}(x)

fmt.Println(sign) // negative

Практическое правило: анонимные функции хороши, пока они короткие и находятся рядом с местом использования. Если «анонимка» стала на 30 строк, она перестаёт быть милой и начинает быть подозрительной, как «простая правка в пятницу вечером».

2. Функция как значение: положили в переменную и вызвали

Да, переменная может хранить функцию.

f := func(x int) int {
	return x + 1
}

fmt.Println(f(10)) // 11

Это не магия, это типы. У функции есть тип, и этот тип описывает её сигнатуру.

Как читать тип функции

Тип функции в Go читается слева направо: «функция принимает … и возвращает …». Это одна из причин, почему в Go объявления обычно воспринимаются проще, чем в C‑подобных языках, где с «указателями на функции» можно случайно вызвать экзистенциальный кризис.

Вот небольшая шпаргалка:

Тип Как читается Что это обычно означает
func()
ничего не принимает, ничего не возвращает действие: печать, лог, «сделай что-то»
func(int) int
принимает
int
, возвращает
int
правило преобразования числа
func(int, int) (int, error)
два
int
(int, error)
«операция калькулятора», которая может сломаться

nil‑функция: важный нюанс

Функциональная переменная, объявленная через var, по умолчанию равна nil.

var rule func(int) int
fmt.Println(rule == nil) // true

Если попытаться вызвать rule(10), программа упадёт в runtime с паникой, потому что вы пытаетесь вызвать «ничего». Это та ситуация, где компилятор не спасёт: типы верные, но значения нет.

Поэтому, если функция приходит откуда-то «снаружи» (вы её выбираете по вводу, получаете из функции, храните в поле структуры), нужно держать в голове мысль «а точно ли она не nil?».

Чтобы сигнатуры не выглядели как заклинания: именуем тип функции

Мы уже знакомы с пользовательскими типами (например, type Status int), и здесь можно сделать то же самое: дать имя типу функции. Это сильно повышает читаемость.

type Rule func(int) int

Теперь вместо func(int) int вы можете писать Rule, и код начинает выглядеть дружелюбнее.

3. Функции высшего порядка: передаём функцию как параметр

Функция высшего порядка — это функция, которая принимает другую функцию и использует её внутри. Иногда ещё и возвращает функцию, но с этим мы сегодня познакомимся аккуратно, без драматизма.

apply: применить правило к числу

type Rule func(int) int

func apply(x int, r Rule) int {
	return r(x)				// вызвали функцию переданную как r
}

Использование с именованной функцией:

func double(x int) int { return x * 2 }	// возвращает удвоенный параметр

fmt.Println(apply(10, double)) // 20

И с анонимной:

fmt.Println(apply(10, func(x int) int { return x * x })) // 100

apply ничего не знает про «удвоить» или «квадрат». Он знает только одно: «мне дали x и правило r, я верну r(x)». Это и есть главная сила: отделить каркас от конкретного поведения.

repeat: выполнить действие N раз

Очень простая, но полезная функция. Она принимает количество повторений и действие.

func repeat(n int, action func()) {	//переменная action типа func() без параметров и результата
	for i := 0; i < n; i++ {
		action()
	}
}

Применение:

repeat(3, func() {	//передаем функцию без параметров и результата
	fmt.Println("hi")
})

Внутри repeat обычный for, просто упакованный так, чтобы main был похож на сценарий, а не на мешок с проводами.

Маленький шаг для одного человека

А что будет если написать такой код:

count := 0
repeat(5, func() {
	count++
})
fmt.Println(count) // 5

Анонимная функция «видит» переменную count снаружи и меняет её. В Go это разрешено — такая хитрая штука называется замыканием.

4. Замыкания: функция, которая «помнит» контекст

Замыкание — это функция, которая использует переменные из внешней области видимости.

Можно сказать и так: функция — это не только код, но иногда ещё и состояние, которое она носит с собой.

Классический пример: счётчик

Напишем фабрику счётчиков: функция makeCounter возвращает функцию, которая при каждом вызове увеличивает внутреннее число.

func makeCounter() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

Использование:

c := makeCounter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3

Здесь переменная n живёт «дольше», чем выполнение makeCounter, потому что на неё ссылается возвращённая функция. Это и есть «функция помнит контекст».

5. Пример: MiniCalc v4

Теперь соберём всё в одну мини‑программу. Мы продолжаем развивать наш учебный калькулятор MiniCalc.

Идея версии v4 такая: пользователь вводит выражение a op b, а потом код правила rule, который применится к результату.

Формат ввода:

10 / 2 s

Где rule:

  • "d" означает double, удвоить результат
  • "s" означает square, возвести результат в квадрат
  • "n" означает none, ничего не делать

Типы функций для читаемости

type BinOp func(int, int) (int, error)
type Rule func(int) int

Теперь сигнатуры chooseOp и chooseRule будут выглядеть гораздо приятнее.

Парсинг int из строки

func parseInt(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	return n, nil
}

Операции калькулятора

Сложение и прочее — без сюрпризов.

func add(a, b int) (int, error) { return a + b, nil }
func sub(a, b int) (int, error) { return a - b, nil }
func mul(a, b int) (int, error) { return a * b, nil }

А деление сделаем безопасным.

func div(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

Выбор операции как функции

Вот тут и начинается «функция как значение»: мы не считаем сразу, мы возвращаем функцию, которая умеет считать.

func chooseOp(op string) (BinOp, error) {
	switch op {
	case "+":
		return add, nil
	case "-":
		return sub, nil
	case "*":
		return mul, nil
	case "/":
		return div, nil
	default:
		return nil, errors.New("unknown operator")
	}
}

Если оператор неизвестен, мы возвращаем nil и ошибку. Это важно: если ошибку проигнорировать и попытаться вызвать nil‑функцию, получите падение.

Выбор правила как функции

Здесь мы активно используем анонимные функции: правила короткие, им отдельные имена обычно не нужны.

func chooseRule(code string) (Rule, error) {
	switch code {
	case "d":
		return func(x int) int { return x * 2 }, nil
	case "s":
		return func(x int) int { return x * x }, nil
	case "n":
		return func(x int) int { return x }, nil
	default:
		return nil, errors.New("unknown rule")
	}
}

Применение правила

func apply(x int, r Rule) int {
	return r(x)
}

Красивый заголовок через repeat

func repeat(n int, action func()) {
	for i := 0; i < n; i++ {
		action()
	}
}

Собираем run и main

Сделаем run() error, чтобы main был совсем короткий и читабельный. Такой стиль удобно держит «счастливый путь» внизу, а ошибки возвращаются наверх. Это хорошо сочетается с идеей, что ошибки — значения, и ими можно управлять обычным кодом.

func run() error {
	repeat(20, func() { fmt.Print("=") })
	fmt.Println()

	var aS, opS, bS, ruleS string
	_, err := fmt.Scan(&aS, &opS, &bS, &ruleS)
	if err != nil {
		return err
	}

	a, err := parseInt(aS)
	if err != nil {
		return fmt.Errorf("bad a: %v", err)
	}
	b, err := parseInt(bS)
	if err != nil {
		return fmt.Errorf("bad b: %v", err)
	}

	op, err := chooseOp(opS)
	if err != nil {
		return err
	}
	res, err := op(a, b)
	if err != nil {
		return err
	}

	rule, err := chooseRule(ruleS)
	if err != nil {
		return err
	}

	fmt.Println("result:", apply(res, rule))
	return nil
}

main остаётся совсем маленьким:

func main() {
	if err := run(); err != nil {
		fmt.Println("error:", err)
	}
}

Чтобы увидеть поток выполнения целиком, можно представить это так:

flowchart TD
    A["Считываем: a op b rule"] --> B["parseInt(a), parseInt(b)"]
    B --> C["chooseOp(op) -> BinOp"]
    C --> D["res := op(a,b)"]
    D --> E["chooseRule(rule) -> Rule"]
    E --> F["final := apply(res, Rule)"]
    F --> G["печатаем final"]

6. Типичные ошибки

Ошибка №1: написали анонимную функцию и забыли её вызвать.
func() { ... } само по себе ничего не выполняет. Это просто значение. Чтобы выполнить, нужны скобки вызова: func() { ... }().

Ошибка №2: путают «функцию» и «результат вызова».
double и double(10) — разные сущности. Первое можно передать в apply, второе уже число. Если вы попытаетесь передать число туда, где ждут функцию, компилятор будет прав, когда возмутится.

Ошибка №3: забыли проверить ошибку и вызвали nil‑функцию.
В chooseOp и chooseRule на плохой ввод возвращается nil, error. Если игнорировать err и попытаться вызвать результат, программа упадёт в runtime. Поэтому стиль «вызвал → сразу проверил err» здесь жизненно необходим.

Ошибка №4: замыкание “поймало” переменную цикла, а не значение.
Если функция использует переменную i, объявленную в цикле, и вызывается позже, она может увидеть не то значение, которое вы ожидали. Лечится копией v := i внутри тела цикла и использованием v в замыкании.

Ошибка №5: анонимная функция выросла до размеров мини‑романа.
Короткая анонимка рядом с использованием — отлично. Анонимка на 30 строк с несколькими if и кучей временных переменных — это уже кандидат на вынесение в именованную функцию. Через неделю вы сами себе скажете спасибо.

1
Задача
Go SELF, 9 уровень, 5 лекция
Недоступна
Печатный разделитель
Печатный разделитель
1
Задача
Go SELF, 9 уровень, 5 лекция
Недоступна
Выбор эффекта
Выбор эффекта
1
Задача
Go SELF, 9 уровень, 5 лекция
Недоступна
Шаговый счётчик
Шаговый счётчик
1
Задача
Go SELF, 9 уровень, 5 лекция
Недоступна
Сборный калькулятор
Сборный калькулятор
1
Опрос
Функции, 9 уровень, 5 лекция
Недоступен
Функции
Функции и параметры
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ