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

Лекция 4. Функции, несколько результатов, closures и `defer`

Цели лекции

После лекции студент должен:

  • объявлять и вызывать функции;
  • понимать сигнатуру функции;
  • использовать несколько возвращаемых значений;
  • отличать параметры от аргументов;
  • применять variadic-параметры;
  • передавать функции как значения;
  • создавать анонимные функции и closures;
  • понимать захват внешних переменных;
  • использовать defer для гарантированного завершения действий;
  • разделять программу на небольшие функции с ясной ответственностью.

1. Зачем нужны функции

Функция объединяет последовательность действий под именем. Она помогает:

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

Пример функции, вычисляющей площадь прямоугольника:

func rectangleArea(width float64, height float64) float64 {
return width * height
}

Вызов:

area := rectangleArea(5, 3)
fmt.Println(area) // 15

width и height — параметры функции. Значения 5 и 3 — аргументы конкретного вызова.

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

Общая форма:

func имя(параметры) возвращаемые_типы {
тело
}

Пример без параметров и результата:

func printSeparator() {
fmt.Println("----------------")
}

Пример с одним параметром:

func greet(name string) {
fmt.Printf("Привет, %s!\n", name)
}

Пример с результатом:

func square(number int) int {
return number * number
}

Функции объявляются на уровне пакета. Именованную функцию нельзя объявить внутри другой функции, но внутри функции можно создать анонимную функцию.

3. Сокращённая запись параметров одного типа

Если соседние параметры имеют одинаковый тип, его можно указать один раз:

func add(left, right int) int {
return left + right
}

Это эквивалентно:

func add(left int, right int) int {
return left + right
}

Сокращение удобно, пока имена остаются понятными.

4. Сигнатура функции

Сигнатура описывает:

  • количество параметров;
  • их типы;
  • количество результатов;
  • их типы;
  • наличие variadic-параметра.

Имена параметров в тип функции не входят:

func(int, int) int

Этому типу соответствуют обе функции:

func add(a, b int) int {
return a + b
}
func max(left, right int) int {
if left > right {
return left
}
return right
}

5. Возврат значения

return завершает функцию и возвращает результат:

func absolute(value int) int {
if value < 0 {
return -value
}
return value
}

Компилятор требует, чтобы функция с результатом возвращала его на всех возможных путях выполнения.

Плохой вариант:

func sign(value int) string {
if value > 0 {
return "positive"
}
// ошибка: для value <= 0 результат отсутствует
}

6. Несколько возвращаемых значений

Функция Go может вернуть несколько результатов:

func divide(dividend, divisor int) (int, int) {
quotient := dividend / divisor
remainder := dividend % divisor
return quotient, remainder
}

Получение результатов:

quotient, remainder := divide(17, 5)
fmt.Println(quotient) // 3
fmt.Println(remainder) // 2

Это не кортеж как отдельный объект. Результаты являются частью сигнатуры функции.

Если одно значение не нужно:

quotient, _ := divide(17, 5)

Использовать _ следует осознанно. Особенно опасно без причины отбрасывать ошибку.

7. Результат и признак успешности

Несколько результатов позволяют явно сообщать, удалось ли получить значение:

func findEven(numbers []int) (int, bool) {
for _, number := range numbers {
if number%2 == 0 {
return number, true
}
}
return 0, false
}

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

number, found := findEven([]int{1, 3, 8, 11})
if !found {
fmt.Println("Чётное число не найдено")
return
}
fmt.Println("Найдено:", number)

Нулевое значение 0 само по себе не позволяет отличить «найден ноль» от «ничего не найдено». Дополнительный bool устраняет неоднозначность.

8. Результат и ошибка

Идиоматичная функция Go часто возвращает значение и error:

func parseAge(text string) (int, error) {
age, err := strconv.Atoi(text)
if err != nil {
return 0, fmt.Errorf("parse age %q: %w", text, err)
}
if age < 0 {
return 0, fmt.Errorf("age must not be negative: %d", age)
}
return age, nil
}

Вызов:

age, err := parseAge("20")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Возраст:", age)

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

9. Именованные результаты

Результатам можно дать имена:

func dimensions() (width int, height int) {
width = 1920
height = 1080
return width, height
}

Разрешён короткий return без перечисления результатов:

func dimensions() (width int, height int) {
width = 1920
height = 1080
return
}

Такой возврат называют naked return. В короткой функции он может быть понятен, но в длинной скрывает возвращаемые значения. Обычно лучше писать их явно.

Именованные результаты полезны прежде всего как документация:

func splitSeconds(total int) (hours, minutes, seconds int) {
hours = total / 3600
minutes = total % 3600 / 60
seconds = total % 60
return hours, minutes, seconds
}

10. Передача аргументов по значению

В Go аргументы передаются по значению: функция получает копию переданного значения.

func increment(value int) {
value++
}
func main() {
count := 10
increment(count)
fmt.Println(count) // 10
}

Изменение локальной копии не изменяет count. Для изменения значения вызывающей стороны используют указатель; эта тема рассматривается в лекции 7.

Slices и maps тоже передаются по значению, но их значения содержат ссылки на внутренние структуры. Из-за этого изменение элемента может быть видно снаружи. Подробности рассматриваются в лекциях 5 и 6.

11. Variadic-функции

Последний параметр может принимать переменное количество аргументов:

func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}

Вызовы:

fmt.Println(sum()) // 0
fmt.Println(sum(5)) // 5
fmt.Println(sum(1, 2, 3, 4)) // 10

Внутри функции numbers имеет тип []int.

Готовый slice передаётся с ...:

values := []int{10, 20, 30}
fmt.Println(sum(values...))

Variadic-параметр может быть только последним:

func join(separator string, parts ...string) string {
return strings.Join(parts, separator)
}

12. Функция как значение

Функцию можно сохранить в переменной:

operation := add
result := operation(4, 5)
fmt.Println(result)

Тип переменной:

var operation func(int, int) int
operation = add

Можно выбрать функцию во время выполнения:

func subtract(a, b int) int {
return a - b
}
var operation func(int, int) int
if useAddition {
operation = add
} else {
operation = subtract
}
fmt.Println(operation(10, 3))

Zero value функции — nil. Вызов nil-функции приводит к panic, поэтому переменную нужно инициализировать или проверить.

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

Анонимная функция не имеет собственного имени:

multiply := func(a, b int) int {
return a * b
}
fmt.Println(multiply(4, 5))

Её можно вызвать сразу:

result := func(value int) int {
return value * value
}(6)
fmt.Println(result) // 36

Анонимные функции полезны как короткое поведение, передаваемое другой функции. Большую логику лучше оформлять именованной функцией.

14. Функции высшего порядка

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

func apply(value int, operation func(int) int) int {
return operation(value)
}
func double(value int) int {
return value * 2
}
func main() {
fmt.Println(apply(5, double))
result := apply(5, func(value int) int {
return value * value
})
fmt.Println(result)
}

В стандартной библиотеке Go чаще предпочитают явные циклы, но функции как значения широко используются для HTTP-обработчиков, сортировки, тестов и настройки поведения.

15. Closure и захват переменных

Closure — функция, использующая переменные из внешней области:

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

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

next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3

Переменная count продолжает существовать после завершения counter, потому что возвращённая функция хранит ссылку на неё.

Два вызова создают независимые состояния:

first := counter()
second := counter()
fmt.Println(first()) // 1
fmt.Println(first()) // 2
fmt.Println(second()) // 1

Closure удобно для конфигурации и небольшого состояния, но скрытое изменяемое состояние может усложнить понимание и конкурентное использование.

16. Функция, возвращающая функцию

func multiplier(factor int) func(int) int {
return func(value int) int {
return value * factor
}
}
double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15

factor захватывается возвращаемой функцией.

17. defer

defer откладывает вызов функции до завершения текущей функции:

func example() {
fmt.Println("начало")
defer fmt.Println("завершение")
fmt.Println("работа")
}

Вывод:

начало
работа
завершение

Отложенный вызов выполняется и при обычном return, и при раскрутке стека после panic. Он не выполняется при принудительном завершении процесса, например через os.Exit.

18. Порядок выполнения defer

Несколько отложенных вызовов выполняются в обратном порядке — LIFO:

func main() {
defer fmt.Println("первый defer")
defer fmt.Println("второй defer")
defer fmt.Println("третий defer")
fmt.Println("тело функции")
}

Вывод:

тело функции
третий defer
второй defer
первый defer

Такой порядок удобен для освобождения вложенных ресурсов.

19. Когда вычисляются аргументы defer

Аргументы отложенного вызова вычисляются в момент выполнения инструкции defer, а сама функция вызывается позже:

func main() {
value := 10
defer fmt.Println("аргумент:", value)
value = 20
}

Будет выведено 10.

Closure обращается к переменной в момент своего выполнения:

func main() {
value := 10
defer func() {
fmt.Println("closure:", value)
}()
value = 20
}

Будет выведено 20.

Это различие важно при отладке и работе с изменяемым состоянием.

20. defer и ресурсы

Типичный шаблон:

func readFirstLine(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
scanner := bufio.NewScanner(file)
if scanner.Scan() {
return scanner.Text(), nil
}
if err := scanner.Err(); err != nil {
return "", err
}
return "", nil
}

defer размещается сразу после успешного получения ресурса. Тогда закрытие не потеряется при добавлении новых веток return.

В коде, где ошибка закрытия важна, её тоже следует обработать. Простой defer file.Close() подходит не для каждого сценария записи данных.

21. defer внутри цикла

Отложенный вызов выполняется при завершении функции, а не текущей итерации:

func process(paths []string) error {
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
}
return nil
}

Все файлы останутся открытыми до завершения process. Для большого списка это плохо. Итерацию следует вынести в функцию:

func processOne(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
// обработка файла
return nil
}

22. Рекурсия

Функция может вызывать себя:

func factorial(number int) int {
if number <= 1 {
return 1
}
return number * factorial(number-1)
}

Рекурсии нужна базовая ветка. Без неё вызовы продолжатся до переполнения стека.

Для простых последовательных вычислений цикл обычно понятнее и экономнее:

func factorial(number int) int {
result := 1
for current := 2; current <= number; current++ {
result *= current
}
return result
}

23. Декомпозиция программы

Плохая функция одновременно читает ввод, проверяет данные, вычисляет результат и печатает его. Её сложно переиспользовать и тестировать.

Лучше разделить ответственность:

func calculateAverage(values []int) (float64, bool) {
if len(values) == 0 {
return 0, false
}
total := 0
for _, value := range values {
total += value
}
return float64(total) / float64(len(values)), true
}
func printAverage(values []int) {
average, ok := calculateAverage(values)
if !ok {
fmt.Println("Нет данных")
return
}
fmt.Printf("Среднее: %.2f\n", average)
}

calculateAverage не зависит от терминала и позже легко покрывается тестами.

24. Практический пример: операции над задачами

package main
import "fmt"
func validPriority(priority int) bool {
return priority >= 1 && priority <= 5
}
func priorityLabel(priority int) (string, bool) {
if !validPriority(priority) {
return "", false
}
switch priority {
case 1:
return "низкий", true
case 2, 3:
return "обычный", true
case 4:
return "высокий", true
default:
return "критический", true
}
}
func averagePriority(priorities ...int) (float64, bool) {
total := 0
count := 0
for _, priority := range priorities {
if !validPriority(priority) {
continue
}
total += priority
count++
}
if count == 0 {
return 0, false
}
return float64(total) / float64(count), true
}
func main() {
priorities := []int{1, 3, 5, 8, 2}
for index, priority := range priorities {
label, ok := priorityLabel(priority)
if !ok {
fmt.Printf("Задача %d: некорректный приоритет %d\n", index+1, priority)
continue
}
fmt.Printf("Задача %d: %s\n", index+1, label)
}
average, ok := averagePriority(priorities...)
if !ok {
fmt.Println("Нет корректных приоритетов")
return
}
fmt.Printf("Средний приоритет: %.2f\n", average)
}

Пример демонстрирует декомпозицию, несколько результатов, variadic-параметр и ранний выход.

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

Функция делает слишком много

Если имя функции содержит несколько действий через And, вероятно, её можно разделить.

Игнорирование результата

Отброшенный bool или error может скрыть отсутствие значения или ошибку.

Неясный naked return

В длинной функции return без значений заставляет искать объявления результатов далеко выше.

Избыточный defer в цикле

Ресурсы будут освобождены только при завершении всей функции.

Изменение захваченного состояния

Closure со скрытым изменяемым состоянием сложнее тестировать и небезопасно использовать из нескольких goroutines без синхронизации.

Слишком длинная функция

Размер сам по себе не является абсолютным правилом, но несколько уровней вложенности и множество обязанностей — сигнал для декомпозиции.

26. Вопросы для самопроверки

  1. Чем параметр отличается от аргумента?
  2. Что входит в сигнатуру функции?
  3. Как получить два результата функции?
  4. Зачем возвращать дополнительный bool?
  5. Почему ошибки в Go часто возвращаются отдельным значением?
  6. Передаются ли аргументы по ссылке?
  7. Как передать slice в variadic-функцию?
  8. Что является zero value функционального типа?
  9. Что такое closure?
  10. Когда вычисляются аргументы отложенного вызова?
  11. В каком порядке выполняются несколько defer?
  12. Почему defer внутри большого цикла может быть проблемой?

Итоги

Функции в Go являются типизированными значениями и могут возвращать несколько результатов. Эта возможность лежит в основе явной обработки отсутствующих значений и ошибок. Анонимные функции и closures позволяют передавать поведение и сохранять состояние, а defer связывает освобождение ресурса с областью функции.