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‑подобных языках, где с «указателями на функции» можно случайно вызвать экзистенциальный кризис.
Вот небольшая шпаргалка:
| Тип | Как читается | Что это обычно означает |
|---|---|---|
|
ничего не принимает, ничего не возвращает | действие: печать, лог, «сделай что-то» |
|
принимает , возвращает |
правило преобразования числа |
|
два → |
«операция калькулятора», которая может сломаться |
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 и кучей временных переменных — это уже кандидат на вынесение в именованную функцию. Через неделю вы сами себе скажете спасибо.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ