Перейти к содержанию
Educora
Начальный15 мин4 / 10

Функции, замыкания и defer

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

Проверь себя
В этом уроке ты узнаешь
  • Писать функции, возвращающие несколько и именованные результаты
  • Использовать вариативные функции и передавать функции как значения
  • Объяснять, как замыкание захватывает внешнюю переменную
  • Знать порядок выполнения defer и момент вычисления его аргументов

Каждый раз, когда ты нажимаешь «+» на калькуляторе, срабатывает одно и то же правило сложения. В программировании такой многоразовый фрагмент кода называется функцией. Функции в Go умеют возвращать несколько результатов, принимать другие функции в качестве аргументов и даже «помнить» своё состояние. В этом уроке ты также познакомишься с defer — ключевым словом, которое откладывает действие до конца функции.

Объявление функций и результаты

Функция объявляется ключевым словом func: сначала имя, потом параметры в скобках и в конце тип результата. Параметры одного типа можно сократить: a, b int вместо a int, b int. Отличительная черта Go — функция может возвращать несколько значений: (int, int). Ненужный результат можно отбросить с помощью пустого идентификатора _.

Go
package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func divmod(a, b int) (int, int) {
	return a / b, a % b
}

func rectangle(w, h float64) (area, perimeter float64) {
	area = w * h
	perimeter = 2 * (w + h)
	return
}

func main() {
	fmt.Println(add(2, 3))
	q, r := divmod(17, 5)
	fmt.Println(q, r)
	_, rest := divmod(10, 4)
	fmt.Println(rest)
	fmt.Println(rectangle(3, 2.5))
}
Ожидаемый результат
5
3 2
2
7.5 11

В функции rectangle у результатов есть имена: (area, perimeter float64). Такие переменные создаются с нулевыми значениями при входе в функцию, а return без аргументов возвращает их текущие значения. Как видно в последней строке, вызов функции с несколькими результатами можно даже сразу передать в Println.

Вариативные функции и функции как значения

Если перед типом последнего параметра поставить ..., функция будет принимать любое число аргументов — такая функция называется вариативной. Именно такая функция — fmt.Println. Внутри функции этот параметр — обычный срез. Чтобы передать готовый срез, после его имени пишут ...: sum(scores...). Кроме того, функции в Go — обычные значения: их можно сохранять в переменные, передавать в другие функции и возвращать.

Go
package main

import "fmt"

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

func apply(n int, f func(int) int) int {
	return f(n)
}

func main() {
	fmt.Println(sum())
	fmt.Println(sum(1, 2, 3))
	scores := []int{90, 85, 70}
	fmt.Println(sum(scores...))
	square := func(x int) int { return x * x }
	fmt.Println(apply(7, square))
}
Ожидаемый результат
0
6
245
49

Замыкания

Анонимная функция создаётся прямо внутри кода: func(x int) int { ... }. Если она использует внешнюю переменную, то «захватывает» её и продлевает ей жизнь — такая функция называется замыканием (closure). Функция counter ниже при каждом вызове создаёт новый счётчик: у каждого замыкания своя переменная count.

Go
package main

import "fmt"

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

func main() {
	next := counter()
	fmt.Println(next(), next(), next())
	other := counter()
	fmt.Println(other(), next())
}
Ожидаемый результат
1 2 3
1 4

next и other — два независимых счётчика: other() снова начинает с 1, а next() продолжает свой счёт и возвращает 4. Переменная count продолжает жить после выхода из counter, потому что её удерживает замыкание.

Go 1.21 и ранее: ручная копия
var funcs []func()
for i := 0; i < 3; i++ {
	i := i // needed before Go 1.22
	funcs = append(funcs, func() { fmt.Print(i, " ") })
}
Go 1.22+: копия не нужна
var funcs []func()
for i := 0; i < 3; i++ {
	funcs = append(funcs, func() { fmt.Print(i, " ") })
}

defer: отложить на потом

Ключевое слово defer откладывает вызов функции до завершения текущей функции. Завершится ли функция нормально или выйдет с ошибкой — отложенные вызовы выполнятся обязательно. Если defer несколько, они выполняются в обратном порядке — от последнего к первому (LIFO, как стопка тарелок). Внимание: аргументы вычисляются в момент записи defer, а не потом.

Go
package main

import "fmt"

func main() {
	fmt.Println("start")
	for i := 1; i <= 3; i++ {
		defer fmt.Println("deferred", i)
	}
	x := 10
	defer fmt.Println("x was", x)
	x = 20
	fmt.Println("end, x =", x)
}
Ожидаемый результат
start
end, x = 20
x was 10
deferred 3
deferred 2
deferred 1

Посмотри на результат: хотя x потом стал равен 20, отложенный вызов напечатал 10, потому что аргумент был вычислен в строке с defer. Все отложенные вызовы сработали после строки end, в обратном порядке.

Главное

  • Функция записывается как func имя(параметры) результат; параметры одного типа можно объединить: a, b int.
  • Функция в Go может возвращать несколько значений; ненужный результат отбрасывается через _.
  • Параметр ...T принимает любое число аргументов; готовый срез передаётся как s....
  • Замыкание захватывает и хранит внешнюю переменную; начиная с Go 1.22 у каждой итерации цикла своя переменная.
  • defer откладывает вызов до конца функции, выполняет отложенные вызовы в обратном порядке и сразу вычисляет аргументы.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Какой вызов функции func divmod(a, b int) (int, int) правильный?