Функции
Привет! Четвёртый урок — один из самых важных. Функции — это фундамент хорошего кода. Они позволяют разбивать большую задачу на маленькие, переиспользовать логику и делать программу понятной и чистой.
Представьте: без функций вам пришлось бы копировать один и тот же код снова и снова. С функциями — пишете один раз, вызываете сколько угодно. Плюс 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
-
Аргументы вычисляются сразу
func main() {
i := 1
defer fmt.Println("Значение i:", i) // выведет 1!
i = 42
}Значение
iфиксируется в момент defer, а не в момент выполнения.Если нужно актуальное значение — используй замыкание:
defer func() { fmt.Println("Актуальное i:", i) }() -
Defer работает даже при панике
func main() {
defer fmt.Println("Я всё равно выполнюсь!")
panic("Катастрофа!")
}
// Вывод:
// Я всё равно выполнюсь!
// panic: Катастрофа!
Подробно о панике мы поговорим чуть позже, а пока кратко:panic— это встроенный механизм для обработки непредвиденных, фатальных ошибок во время выполнения программы.
-
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)
}