Перейти к основному содержимому

Функции

Привет! Четвёртый урок — один из самых важных. Функции — это фундамент хорошего кода. Они позволяют разбивать большую задачу на маленькие, переиспользовать логику и делать программу понятной и чистой.

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

Что такое функция?

Функция — это именованный (или анонимный) блок кода, который выполняет конкретную задачу и может принимать входные данные (параметры) и возвращать результат.

Преимущества функций:

  • Избежание дублирования (DRY — Don't Repeat Yourself)
  • Читаемость — код разбит на логические части
  • Тестируемость — каждую функцию можно проверить отдельно
  • Переиспользуемость — используйте в разных местах программы

Объявление функций

Базовый синтаксис:

func имяФункции(параметр1 тип, параметр2 тип) типВозвращаемогоЗначения {
// тело функции
return значение
}

Простые примеры

import "fmt"

// Без параметров и без возврата
func sayHello() {
fmt.Println("Привет из функции!")
}

// С параметрами, без возврата
func greet(name string) {
fmt.Printf("Привет, %s! 🚀\n", name)
}

// С параметрами и одним возвращаемым значением
func add(a, b int) int {
return a + b
}

// С несколькими возвращаемыми значениями (очень популярно в Go!)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("деление на ноль запрещено")
}

return a / b, nil
}

func main() {
sayHello()
greet("Марина")

sum := add(7, 8)
fmt.Println("7 + 8 =", sum) // 15

result, err := divide(10, 3)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Printf("10 / 3 = %.2f\n", result)
}

_, err = divide(5, 0) // _ — игнорируем первое значение
if err != nil {
fmt.Println("Ошибка:", err)
}
}

Параметры функций

Передача по значению (по умолчанию)

Go всегда копирует значение при передаче в функцию.

func increment(x int) {
x++
fmt.Println("Внутри функции x =", x) // 11
}

func main() {
value := 10
increment(value)
fmt.Println("Снаружи value =", value) // всё ещё 10!
}

Передача по ссылке (указатели)

Чтобы изменить оригинал — передавайте указатель *T.

func incrementPtr(x *int) {
*x++ // разыменовываем и увеличиваем
}

func main() {
value := 10
incrementPtr(&value) // & — адрес переменной
fmt.Println("Теперь value =", value) // 11
}


Правило: если функция должна менять исходные данные — используйте указатель.

Вариадические функции (varargs)

Функция может принимать любое количество аргументов одного типа.

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, 40)) // 100
fmt.Println(sum()) // 0

slice := []int{1, 2, 3, 4}
fmt.Println(sum(slice...)) // ... распаковывает слайс
}

Возвращаемые значения

Именованные возвращаемые значения (named return)

Очень удобная фича Go — можно назвать возвращаемые переменные.

func rectInfo(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // "голый" return — возвращает названные переменные
}

func main() {
a, p := rectInfo(5, 8)
fmt.Printf("Площадь: %.1f, Периметр: %.1f\n", a, p) // Площадь: 40.0, Периметр: 26.0
}

Это делает код чище и позволяет использовать defer для модификации возвращаемых значений.

defer

В Go ключевое слово defer позволяет отложить выполнение функции (или метода) до момента, когда текущая функция завершит свою работу — независимо от того, завершится она нормально (по return) или из-за паники (panic). Отложенный вызов добавляется в очередь (стек), и все defer гарантированно сработают даже при ошибке или раннем возврате.

Как работает defer

func main() {
defer fmt.Println("Я выполнюсь самым последним!")

fmt.Println("Сначала это")
fmt.Println("Потом это")
// ← здесь функция main завершается
}
// Вывод:
// Сначала это
// Потом это
// Я выполнюсь самым последним!

Главное правило: отложенные вызовы выполняются в обратном порядке (LIFO — Last In, First Out), как стопка тарелок.

func main() {
defer fmt.Println("Первый defer")
defer fmt.Println("Второй defer")
defer fmt.Println("Третий defer")
}
// Вывод:
// Третий defer
// Второй defer
// Первый defer

Зачем нужен defer?

defer идеален для гарантированного выполнения кода "уборки" (cleanup) в любой ситуации.

Классический пример: Логирование входа/выхода из функции

func process() {
defer fmt.Println("process завершён")
fmt.Println("process начат")
// ...
}

Важные детали работы defer

  1. Аргументы вычисляются сразу

    func main() {
    i := 1
    defer fmt.Println("Значение i:", i) // выведет 1!
    i = 42
    }

    Значение i фиксируется в момент defer, а не в момент выполнения.

    Если нужно актуальное значение — используй замыкание:

    defer func() { fmt.Println("Актуальное i:", i) }()
  2. Defer работает даже при панике

    func main() {
    defer fmt.Println("Я всё равно выполнюсь!")
    panic("Катастрофа!")
    }
    // Вывод:
    // Я всё равно выполнюсь!
    // panic: Катастрофа!


Подробно о панике мы поговорим чуть позже, а пока кратко: panic — это встроенный механизм для обработки непредвиденных, фатальных ошибок во время выполнения программы.

  1. Defer и return Отложенные функции выполняются после вычисления возвращаемого значения, но до самого возврата.

    Это позволяет модифицировать возвращаемые значения (named return parameters):

    func getValue() (result int) {
    defer func() { result *= 2 }()
    result = 10
    return // вернёт 20!
    }

Когда использовать defer

  • Всегда для закрытия ресурсов (Close(), Unlock(), освобождение).
  • Для логирования входа/выхода.
  • Для обработки паник (recover).
  • Для любых "финальных" действий.

Когда НЕ использовать

  • Для простых операций, которые не требуют гарантии выполнения (обычный код лучше).
  • Если производительность критична (defer имеет небольшой overhead, но обычно пренебрежимо малый).

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

Анонимные функции

Можно создавать функции "на лету".

func main() {
// Немедленный вызов
func() {
fmt.Println("Я анонимная и сразу выполняюсь!")
}()

// Присвоение переменной
multiply := func(x, y int) int {
return x * y
}

fmt.Println(multiply(4, 7)) // 28
}

// Вывод:
// Я анонимная и сразу выполняюсь!
// 28

Замыкания (closures)

Замыкания (closures) — это когда ты создаёшь маленькую безымянную функцию (анонимную), и она «запоминает» переменные из того места, где была создана, даже если это место уже закончило работу.

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

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

func main() {
counter1 := makeCounter()
fmt.Println(counter1()) // 1
fmt.Println(counter1()) // 2

counter2 := makeCounter() // независимый счётчик!
fmt.Println(counter2()) // 1
}

Замыкания — мощный инструмент для создания генераторов, фабрик и обработчиков.

Практические примеры

Калькулятор с обработкой ошибок

func calc(op string, a, b float64) (float64, error) {
switch op {
case "+":
return a + b, nil
case "-":
return a - b, nil
case "*":
return a * b, nil
case "/":
if b == 0 {
return 0, fmt.Errorf("деление на ноль")
}
return a / b, nil
default:
return 0, fmt.Errorf("неподдерживаемая операция: %s", op)
}
}


Подробнее о интерфейсе error мы поговорим чуть позднее! А пока вам стоит знать, что интерфейс error представляет собой тип данных, который может быть использован для возврата ошибок из функций, а fmt.Errorf — это функция, которая создает объект ошибки с заданным сообщением

Функции высшего порядка (принимают/возвращают функции)

func apply(numbers []int, fn func(int) int) []int {
result := make([]int, len(numbers))
for i, v := range numbers {
result[i] = fn(v)
}
return result
}

func main() {
nums := []int{1, 2, 3, 4, 5}

squares := apply(nums, func(x int) int { return x * x })
fmt.Println("Квадраты:", squares)

doubles := apply(nums, func(x int) int { return x * 2 })
fmt.Println("Удвоенные:", doubles)
}