Функции
Учимся создавать собственные функции: параметры, возврат значений, множественный возврат, обработка ошибок, defer и практика
До сих пор весь наш код жил внутри одной-единственной функции main. Мы объявляли переменные, писали условия, крутили циклы — и всё в одном месте. Но представьте себе кухню, где один повар одновременно месит тесто, варит суп, жарит мясо и моет посуду. Хаос! В реальных программах мы разбиваем код на маленькие самостоятельные блоки — функции. Каждая функция делает что-то одно, и делает это хорошо. Сегодня мы научимся создавать собственные функции, возвращать из них значения, обрабатывать ошибки по-гошному и познакомимся с уникальным механизмом defer. Это последний урок модуля «Основы языка» — после него у вас будет полный набор базовых инструментов Go.
📦 Объявление функции
Функция — это именованный блок кода, который можно вызывать сколько угодно раз. Вы уже знакомы с одной функцией — main. Она особенная: именно с неё Go начинает выполнение программы. Но мы можем создавать свои функции. Вот самый простой пример:
package main
import "fmt"
func sayHello() {
fmt.Println("Привет, мир!")
}
func main() {
sayHello()
sayHello()
sayHello()
}
Мы объявили функцию sayHello с помощью ключевого слова func. После имени идут круглые скобки (пока пустые — параметров нет), а затем тело функции в фигурных скобках. В main мы вызываем sayHello() три раза — и три раза на экране появляется «Привет, мир!».
Зачем это нужно? Во-первых, чтобы не повторять один и тот же код. Во-вторых, чтобы давать блокам кода понятные имена. Вместо десяти строк вычислений вы пишете calculateTotal() — и сразу ясно, что происходит.
💡 Подсказка: в Go принято называть функции в стиле camelCase: первое слово с маленькой буквы, каждое следующее — с большой. Например: calculateSum, getUserName, printReport. Функции с заглавной буквы (CalculateSum) имеют особый смысл — они доступны из других пакетов. Об этом мы поговорим позже.
📥 Параметры функции
Функция становится по-настоящему полезной, когда принимает данные снаружи — параметры. Параметры перечисляются в круглых скобках после имени функции. В Go обязательно указывается тип каждого параметра:
package main
import "fmt"
func greet(name string) {
fmt.Println("Привет,", name, "!")
}
func main() {
greet("Алиса")
greet("Боб")
greet("Чарли")
}
Функция greet принимает один параметр name типа string. Когда мы вызываем greet("Алиса"), значение «Алиса» попадает в переменную name внутри функции.
Функция может принимать несколько параметров:
package main
import "fmt"
func add(a int, b int) {
fmt.Println(a, "+", b, "=", a+b)
}
func main() {
add(3, 5) // 3 + 5 = 8
add(10, 20) // 10 + 20 = 30
}
Если несколько параметров подряд имеют одинаковый тип, можно указать тип только один раз:
// Вместо:
func add(a int, b int) { ... }
// Можно написать короче:
func add(a, b int) { ... }
Оба варианта абсолютно идентичны. Сокращённая запись удобна, когда параметров одного типа много. Вот пример с разными типами:
func describe(name string, age int, isStudent bool) {
fmt.Printf("%s, возраст: %d, студент: %t\n", name, age, isStudent)
}
📤 Возвращаемые значения
Функции из примеров выше только выводят текст на экран. Но часто нужно, чтобы функция вычислила что-то и вернула результат обратно. Для этого после списка параметров указывается тип возвращаемого значения, а внутри функции используется return:
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
result := add(3, 5)
fmt.Println("Сумма:", result) // Сумма: 8
// Можно использовать прямо в выражении:
fmt.Println("Двойная сумма:", add(3, 5) * 2) // Двойная сумма: 16
}
Разберём объявление func add(a, b int) int. Слово func говорит, что это функция. add — её имя. (a, b int) — два параметра типа int. А последний int после скобок — это тип возвращаемого значения. Оператор return a + b вычисляет сумму и возвращает её туда, откуда функция была вызвана.
Ещё пример — функция, которая проверяет, чётное ли число:
func isEven(n int) bool {
return n%2 == 0
}
func main() {
fmt.Println(isEven(4)) // true
fmt.Println(isEven(7)) // false
}
Возвращаемый тип — bool. Выражение n%2 == 0 само по себе уже является булевым значением, поэтому его можно сразу вернуть через return.
🎯 Множественный возврат значений
Вот мы добрались до одной из самых крутых особенностей Go. В большинстве языков функция может вернуть только одно значение. В Go функция может вернуть сколько угодно значений одновременно. Возвращаемые типы перечисляются в скобках:
package main
import "fmt"
func divide(a, b float64) (float64, float64) {
quotient := a / b // частное
remainder := float64(int(a) % int(b)) // остаток
return quotient, remainder
}
func main() {
q, r := divide(17, 5)
fmt.Println("Частное:", q) // Частное: 3.4
fmt.Println("Остаток:", r) // Остаток: 2
}
Функция divide возвращает два значения типа float64. При вызове мы используем краткое присваивание с двумя переменными: q, r := divide(17, 5). Каждое возвращённое значение попадает в свою переменную.
Ещё один классический пример — функция, возвращающая минимум и максимум:
func minMax(a, b int) (int, int) {
if a < b {
return a, b
}
return b, a
}
func main() {
min, max := minMax(42, 17)
fmt.Println("Мин:", min, "Макс:", max) // Мин: 17 Макс: 42
}
Если вам не нужно одно из возвращаемых значений, используйте символ подчёркивания _ — так называемый «пустой идентификатор»:
// Нам нужен только максимум, минимум игнорируем:
_, max := minMax(42, 17)
fmt.Println("Макс:", max)
💡 Подсказка: в Go нельзя просто проигнорировать возвращаемое значение, записав его в обычную переменную и не используя — компилятор выдаст ошибку. Подчёркивание _ — единственный способ явно сказать: «Я знаю, что здесь есть значение, но оно мне не нужно».
⚠️ Возврат ошибки — идиома Go
Множественный возврат — это не просто удобная фишка. Это основа обработки ошибок в Go. В отличие от многих языков, в Go нет исключений (exceptions). Вместо этого функция возвращает ошибку как второе значение. Это один из главных паттернов языка:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль невозможно")
}
return a / b, nil
}
func main() {
result, err := divide(10, 3)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Результат:", result)
}
result2, err2 := divide(10, 0)
if err2 != nil {
fmt.Println("Ошибка:", err2)
} else {
fmt.Println("Результат:", result2)
}
}
Разберём подробно. Функция divide возвращает два значения: (float64, error). Тип error — это встроенный интерфейс Go для ошибок. Если всё хорошо, мы возвращаем результат и nil (nil означает «ничего», то есть ошибки нет). Если что-то пошло не так (деление на ноль), мы возвращаем нулевое значение и объект ошибки, созданный через errors.New().
Запомните этот паттерн — вы будете видеть его в Go повсюду:
Паттерн обработки ошибок в Go:
1. Вызываем функцию и получаем результат и ошибку: result, err := someFunction()
2. Проверяем ошибку: if err != nil { ... }
3. Если ошибки нет (err == nil), используем результат
Вот ещё один пример — функция, которая вычисляет квадратный корень (упрощённо):
package main
import (
"errors"
"fmt"
"math"
)
func sqrt(n float64) (float64, error) {
if n < 0 {
return 0, errors.New("нельзя вычислить корень отрицательного числа")
}
return math.Sqrt(n), nil
}
func main() {
val, err := sqrt(25)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Корень из 25 =", val) // Корень из 25 = 5
val2, err2 := sqrt(-4)
if err2 != nil {
fmt.Println("Ошибка:", err2) // Ошибка: нельзя вычислить корень отрицательного числа
return
}
fmt.Println("Корень из -4 =", val2)
}
💡 Подсказка: в Go считается хорошим тоном обрабатывать ошибку сразу после вызова функции. Не откладывайте проверку err != nil — делайте это в следующей же строке. Такой стиль делает код надёжным и предсказуемым.
🏷 Именованные возвращаемые значения
В Go можно дать имена возвращаемым значениям прямо в объявлении функции. Они работают как локальные переменные, инициализированные нулевыми значениями для своего типа. А return без аргументов автоматически вернёт их текущие значения — это называется «голый return» (naked return):
package main
import "fmt"
func rectangleInfo(width, height float64) (area, perimeter float64) {
area = width * height
perimeter = 2 * (width + height)
return // возвращает area и perimeter
}
func main() {
a, p := rectangleInfo(5, 3)
fmt.Println("Площадь:", a) // Площадь: 15
fmt.Println("Периметр:", p) // Периметр: 16
}
Обратите внимание: (area, perimeter float64) — оба возвращаемых значения имеют имена и тип float64. Внутри функции мы просто присваиваем им значения (area = ...), а return без аргументов возвращает оба.
Именованные возвращаемые значения особенно удобны, когда они улучшают читаемость. Сравните:
// Без имён — непонятно, что именно возвращается:
func calculate(x float64) (float64, float64, float64) { ... }
// С именами — сразу ясно:
func calculate(x float64) (sin, cos, tan float64) { ... }
💡 Подсказка: «голый return» удобен в коротких функциях, но в длинных может запутать — непонятно, какие именно значения возвращаются. Поэтому в Go-сообществе рекомендуют использовать голый return только в функциях длиной до 10–15 строк. В длинных функциях лучше явно указывать, что возвращаете: return area, perimeter.
⏳ Отложенный вызов: defer
В Go есть уникальный механизм — defer. Он откладывает выполнение функции до того момента, когда окружающая функция завершится. Звучит странно? Давайте посмотрим:
package main
import "fmt"
func main() {
fmt.Println("Начало")
defer fmt.Println("Это выполнится последним")
fmt.Println("Середина")
fmt.Println("Конец")
}
Результат:
Начало
Середина
Конец
Это выполнится последним
Строка с defer не выполняется в момент, когда программа до неё доходит. Вместо этого Go запоминает: «Когда функция main закончится, нужно будет вызвать fmt.Println("Это выполнится последним")». Поэтому эта строка печатается самой последней.
Зачем это нужно? Главное применение — гарантированная очистка ресурсов. Когда вы открываете файл, подключаетесь к базе данных или захватываете блокировку, нужно не забыть это закрыть/освободить. defer гарантирует, что закрытие произойдёт, даже если дальше в функции случится ошибка:
package main
import (
"fmt"
"os"
)
func readFile() {
file, err := os.Open("data.txt")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
defer file.Close() // Файл закроется, когда readFile завершится
// Дальше работаем с файлом...
fmt.Println("Файл успешно открыт")
// Даже если здесь произойдёт ошибка,
// file.Close() всё равно выполнится!
}
Мы пишем defer file.Close() сразу после открытия файла. Теперь мы можем быть спокойны — файл закроется в любом случае, когда функция завершится. Без defer нам пришлось бы помнить о вызове file.Close() перед каждым return в функции.
Если в функции несколько defer, они выполняются в обратном порядке — как стопка тарелок (последний положенный снимается первым). Это называется LIFO (Last In, First Out):
package main
import "fmt"
func main() {
fmt.Println("Считаем:")
for i := 1; i <= 5; i++ {
defer fmt.Println(i)
}
fmt.Println("Готово!")
}
Результат:
Считаем:
Готово!
5
4
3
2
1
Числа печатаются в обратном порядке: 5, 4, 3, 2, 1. Каждый defer добавлялся в стек, и при завершении функции они извлекаются оттуда в обратном порядке.
Запомните три правила defer:
1. Отложенная функция выполняется при выходе из окружающей функции
2. Аргументы отложенной функции вычисляются сразу (в момент defer), а не при выполнении
3. Несколько defer выполняются в обратном порядке (LIFO)
🔍 Важный нюанс: аргументы defer
Есть одна тонкость, которую новички часто упускают. Аргументы отложенной функции вычисляются в момент вызова defer, а не в момент, когда функция реально выполнится:
package main
import "fmt"
func main() {
x := 10
defer fmt.Println("Значение x:", x) // x запоминается как 10
x = 20
fmt.Println("Текущее x:", x)
}
Результат:
Текущее x: 20
Значение x: 10
Хотя x изменился на 20 до того, как отложенный вызов выполнился, defer запомнил значение x в момент своего объявления — то есть 10. Это важно учитывать при написании кода с defer.
🧱 Функции как строительные блоки
Функции можно вызывать из других функций, комбинировать и выстраивать логику программы как из кирпичиков:
package main
import "fmt"
func square(n int) int {
return n * n
}
func sumOfSquares(a, b int) int {
return square(a) + square(b)
}
func main() {
fmt.Println(sumOfSquares(3, 4)) // 9 + 16 = 25
}
Функция sumOfSquares вызывает square дважды и складывает результаты. Каждая функция решает одну маленькую задачу, а вместе они решают задачу побольше. Это и есть суть декомпозиции — разбиения сложной задачи на простые части.
🛠 Практика: пакет математических утилит
Пришло время собрать всё воедино. Напишем набор математических функций, каждая из которых использует то, что мы сегодня изучили: параметры, возврат значений, множественный возврат и обработку ошибок.
package main
import (
"errors"
"fmt"
"math"
)
// abs возвращает модуль числа
func abs(n float64) float64 {
if n < 0 {
return -n
}
return n
}
// power возводит число в степень
func power(base, exp int) int {
result := 1
for i := 0; i < exp; i++ {
result *= base
}
return result
}
// safeDivide делит a на b с проверкой
func safeDivide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("деление на ноль")
}
return a / b, nil
}
// circleInfo возвращает площадь и длину окружности
func circleInfo(radius float64) (area, circumference float64, err error) {
if radius < 0 {
return 0, 0, errors.New("радиус не может быть отрицательным")
}
area = math.Pi * radius * radius
circumference = 2 * math.Pi * radius
return area, circumference, nil
}
// stats вычисляет сумму, среднее, мин и макс среза чисел
func stats(numbers []float64) (sum, avg, min, max float64, err error) {
if len(numbers) == 0 {
return 0, 0, 0, 0, errors.New("пустой список чисел")
}
min = numbers[0]
max = numbers[0]
for _, n := range numbers {
sum += n
if n < min {
min = n
}
if n > max {
max = n
}
}
avg = sum / float64(len(numbers))
return sum, avg, min, max, nil
}
func main() {
fmt.Println("=== Модуль числа ===")
fmt.Println("abs(-7.5) =", abs(-7.5))
fmt.Println("abs(3.2) =", abs(3.2))
fmt.Println("\n=== Возведение в степень ===")
fmt.Println("2^10 =", power(2, 10))
fmt.Println("3^4 =", power(3, 4))
fmt.Println("\n=== Безопасное деление ===")
if result, err := safeDivide(10, 3); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Printf("10 / 3 = %.2f\n", result)
}
if result, err := safeDivide(10, 0); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Printf("10 / 0 = %.2f\n", result)
}
fmt.Println("\n=== Информация об окружности ===")
area, circ, err := circleInfo(5)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Printf("Радиус 5: площадь = %.2f, длина окружности = %.2f\n", area, circ)
}
fmt.Println("\n=== Статистика ===")
data := []float64{4.5, 2.1, 8.7, 1.3, 6.9}
s, a, mn, mx, err := stats(data)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Printf("Данные: %v\n", data)
fmt.Printf("Сумма: %.1f, Среднее: %.2f, Мин: %.1f, Макс: %.1f\n", s, a, mn, mx)
}
}
Запустите эту программу командой go run main.go. Вот что вы увидите:
=== Модуль числа ===
abs(-7.5) = 7.5
abs(3.2) = 3.2
=== Возведение в степень ===
2^10 = 1024
3^4 = 81
=== Безопасное деление ===
10 / 3 = 3.33
Ошибка: деление на ноль
=== Информация об окружности ===
Радиус 5: площадь = 78.54, длина окружности = 31.42
=== Статистика ===
Данные: [4.5 2.1 8.7 1.3 6.9]
Сумма: 23.5, Среднее: 4.70, Мин: 1.3, Макс: 8.7
Давайте разберём, что здесь демонстрируется:
abs — простая функция с одним параметром и одним возвращаемым значением
power — использование цикла внутри функции для вычислений
safeDivide — возврат (float64, error), классическая обработка ошибок Go
circleInfo — именованные возвращаемые значения + ошибка (три возврата)
stats — множественный возврат пяти значений. Использует срезы (slices), которые мы подробно изучим в следующем уроке — пока просто примите, что []float64 это список чисел
Попробуйте модифицировать программу: добавьте функцию factorial, которая вычисляет факториал числа (5! = 120), или функцию isPrime, которая проверяет, простое ли число. Это отличная тренировка.
📋 Итоги урока
- Функция объявляется через
func имя(параметры) тип_возврата { тело } - Параметры одного типа можно сокращать:
(a, b int)вместо(a int, b int) - Go поддерживает множественный возврат значений — это фундаментальная особенность языка
- Ошибки возвращаются как второе значение:
return result, nilилиreturn 0, err - Паттерн обработки ошибок:
result, err := f()затемif err != nil { ... } - Именованные возвращаемые значения улучшают читаемость и позволяют «голый return»
deferоткладывает вызов функции до завершения окружающей функции- Несколько
deferвыполняются в обратном порядке (LIFO) - Аргументы
deferвычисляются в момент объявления, а не при выполнении - Мы написали пакет математических утилит, применив все изученные концепции
🎉 Поздравляем! Вы завершили Модуль 2 — «Основы языка Go»
Вы изучили переменные и типы, операторы, ввод/вывод, условия, циклы и функции. Теперь у вас есть всё необходимое для написания настоящих программ. В следующем модуле мы перейдём к структурам данных.
В следующем уроке (3.1) мы начнём Модуль 3 и изучим массивы и срезы (slices) — основные коллекции данных в Go. Вы узнаете, как хранить список значений в одной переменной, добавлять и удалять элементы, нарезать части коллекции и перебирать их в циклах. Срезы — это одна из самых используемых структур данных в Go, и вы будете работать с ними каждый день.