---
title: "Условия, циклы, defer"
description: "Пока наши программы выполняли строки строго сверху вниз. Но уже задача со сдачей из прошлой главы просит большего: если денег не хватает, сдачу считать нельзя, нужно сообщить об…"
canonical: https://graphlms.ru/go-basics/book/control-flow
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Условия, циклы, defer

> Источник: https://graphlms.ru/go-basics/book/control-flow — курс «Основы Go», GraphLMS.

## О чём глава

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

В Go для этого три конструкции: `if`, `for` и `switch`. Плюс слово `defer`, которое откладывает вызов до выхода из функции. Их немного, и устроены они проще, чем во многих языках: цикл всего один (`for`, без отдельных `while` и `do-while`), а круглые скобки вокруг условий не пишут.

## if: ветвление без скобок

`if` проверяет условие и выполняет блок, если оно истинно. Условие — это всегда выражение типа `bool`, и его не нужно оборачивать в круглые скобки. А вот фигурные скобки вокруг тела обязательны, даже для одной строки.

```go
if score >= 60 {
    fmt.Println("зачёт")
} else if score >= 40 {
    fmt.Println("пересдача")
} else {
    fmt.Println("незачёт")
}
```

Ветки проверяются сверху вниз, выполняется первая подходящая. При `score = 50` первое условие ложно, второе истинно, напечатается `пересдача`, а до `else` дело не дойдёт.

`else` обязан стоять на той же строке, что и закрывающая скобка предыдущего блока. Это не вопрос стиля. Компилятор сам расставляет точки с запятой в концах строк, и после одинокой `}` он поставит её, закончив `if`. Тогда `else` на новой строке окажется «ни к чему не привязан», и сборка упадёт с `syntax error: unexpected keyword else`.

### if с init-выражением

Перед условием `if` можно объявить переменную, которая будет жить только внутри `if`/`else`. Синтаксис: короткое объявление, точка с запятой, затем условие.

Покажем на функции `strconv.Atoi` из стандартной библиотеки. Она преобразует текст в число и возвращает **два значения**: само число и ошибку. Если ошибки нет, вторым значением приходит `nil` — специальное «пустое» значение. Объявлять такие функции мы научимся в [следующей главе](/go-basics/book/functions), а ошибки подробно разберём [в девятой](/go-basics/book/errors); здесь нам нужен только `if`.

```go play
package main

import (
	"fmt"
	"strconv"
)

func main() {
	if n, err := strconv.Atoi("42"); err == nil {
		fmt.Println("число:", n*2)
	} else {
		fmt.Println("не число:", err)
	}
	// здесь n и err уже не существуют
	fmt.Println("готово")
}
```

Вывод:

```text
число: 84
готово
```

Здесь `n` и `err` объявлены прямо в заголовке `if`. Они доступны и в ветке `if`, и в ветке `else`, но за пределами конструкции их нет. Часть программы, где имя видно, называют *областью видимости*. Узкая область видимости не даёт случайно использовать переменную там, где она уже не имеет смысла. Замените `"42"` на `"сорок два"` и запустите снова: сработает ветка `else`.

### Ловушка: := внутри блока создаёт новую переменную

Любые фигурные скобки открывают новую область видимости. Если внутри неё написать `:=` с уже существующим именем, Go не изменит старую переменную, а создаст новую с тем же именем, которая «заслонит» внешнюю до конца блока. Это называется *затенением* (shadowing):

```go play
package main

import "fmt"

func main() {
	status := "новый"
	if true {
		status := "проверен" // новая переменная, видна только в этом блоке
		fmt.Println("внутри:", status)
	}
	fmt.Println("снаружи:", status)
}
```

Вывод:

```text
внутри: проверен
снаружи: новый
```

Внешний `status` не изменился. Чтобы поменять его, внутри блока нужно обычное присваивание `status = "проверен"`. Компилятор такую ошибку не ловит: обе записи корректны. На ревью это одна из первых вещей, которые я ищу, особенно с переменной `err`.

## for: единственный цикл

`for` в Go умеет всё, что в других языках делают несколько разных циклов. У него есть несколько форм, и важно понять, что это одна и та же конструкция, просто с разным набором частей.

### Классическая форма

Три части через точку с запятой: инициализация, условие, шаг. Знакомо по C, только без скобок.

```go play
package main

import "fmt"

func main() {
	sum := 0
	for i := 1; i <= 5; i++ {
		sum += i
	}
	fmt.Println("сумма 1..5 =", sum)
}
```

Напечатается `сумма 1..5 = 15`. Переменная `i` живёт только внутри цикла. Порядок такой: один раз выполняется `i := 1`, затем перед каждым проходом проверяется `i <= 5`, после каждого прохода выполняется `i++`. Когда `i` станет 6, условие окажется ложным и цикл закончится.

### Форма «while»

Если оставить только условие (без инициализации и шага), получится то, что в других языках называют `while`. Отдельного ключевого слова не нужно — это всё ещё `for`.

```go play
package main

import "fmt"

func main() {
	n := 1
	for n < 100 {
		n *= 2
	}
	fmt.Println("первая степень двойки >= 100:", n)
}
```

Цикл удваивает `n`, пока тот меньше 100: 1, 2, 4, …, 64, 128. На 128 условие ложно, и программа печатает `первая степень двойки >= 100: 128`. Переменную здесь меняем мы сами, внутри тела. Забудете это сделать — цикл станет бесконечным.

### Бесконечный цикл

Если убрать вообще всё, получится бесконечный цикл. Выходят из него по `break` — обычно когда внутри случилось что-то, ради чего цикл и крутился.

```go play
package main

import "fmt"

func main() {
	count := 0
	for {
		count++
		if count == 3 {
			break
		}
	}
	fmt.Println("вышли на счёте:", count)
}
```

`break` немедленно прекращает цикл. Есть и парный к нему `continue` — он пропускает остаток текущей итерации и переходит к следующей.

### range — забегая вперёд

Есть ещё одна форма — `for ... range`, которой удобно перебирать срезы, строки и `map`. Мы подробно разберём её в главе про коллекции, а пока просто покажем, как она выглядит:

```go
for i, v := range []int{10, 20, 30} {
    fmt.Println(i, v)
}
```

`range` сам отдаёт на каждом шаге индекс и значение, так что вручную считать счётчик не нужно.

## switch: ветвление по значению

`switch` — это компактная замена длинной цепочке `if ... else if`. В Go он удобнее, чем в C-подобных языках, по двум причинам.

Во-первых, после каждого `case` **не нужен** `break`: Go сам выходит из `switch`, как только отработал подходящую ветку. Случайно «провалиться» в следующий case нельзя.

Во-вторых, `case` может содержать несколько значений через запятую.

```go play
package main

import "fmt"

func main() {
	for day := 1; day <= 7; day++ {
		switch day {
		case 6, 7:
			fmt.Println(day, "— выходной")
		default:
			fmt.Println(day, "— рабочий день")
		}
	}
}
```

Ветка `default` срабатывает, если ни один `case` не подошёл. Она необязательна и может стоять в любом месте, но по традиции её пишут последней.

### fallthrough: когда провал нужен специально

Иногда поведение «провалиться в следующий case» всё-таки полезно. Для этого есть явное слово `fallthrough` — оно передаёт управление в следующую ветку **без** проверки её условия. Это редкость, но знать о ней стоит.

```go play
package main

import "fmt"

func main() {
	switch level := 2; level {
	case 2:
		fmt.Println("доступ к разделу 2")
		fallthrough
	case 1:
		fmt.Println("доступ к разделу 1")
	case 0:
		fmt.Println("гостевой доступ")
	}
}
```

Здесь подошёл `case 2`, напечаталась его строка, а затем `fallthrough` протолкнул выполнение в `case 1` — несмотря на то, что `level` не равен 1. До `case 0` дело не дошло. Заметьте: в заголовке `switch` тоже можно объявить переменную через init-выражение, как и в `if`.

### switch без условия

Если у `switch` убрать выражение после ключевого слова, каждый `case` становится самостоятельным булевым условием. Получается аккуратная цепочка `if`/`else if`, только читается приятнее.

```go play
package main

import "fmt"

func main() {
	for _, t := range []int{-5, 0, 18, 30} {
		switch {
		case t < 0:
			fmt.Println(t, "— мороз")
		case t == 0:
			fmt.Println(t, "— ноль")
		case t < 25:
			fmt.Println(t, "— тепло")
		default:
			fmt.Println(t, "— жара")
		}
	}
}
```

Выполняется первый `case`, чьё условие истинно, и на этом всё. Обратите внимание на порядок: `18` подходит и под `t < 25`, и под `default`, но сработает первая подходящая ветка. В Go так обычно и заменяют длинную лестницу из `if ... else if`.

## defer: отложенное действие

`defer` перед вызовом функции значит: «выполни это не сейчас, а когда текущая функция будет завершаться». Неважно, как она завершится: дойдёт до конца, выйдет через `return` из середины или аварийно остановится (такую остановку называют паникой, о ней в [главе об ошибках](/go-basics/book/errors)).

Зачем это нужно? Программы постоянно берут ресурсы, которые потом надо вернуть: открытый файл, сетевое соединение, блокировку. Если у функции пять мест выхода, закрытие легко забыть в одном из них. `defer` позволяет написать закрытие один раз, сразу после открытия.

Типичный вид — фрагмент функции, которая читает файл. `os.Open` открывает файл и, как `strconv.Atoi`, возвращает два значения: файл и ошибку. `return err` выходит из функции с этой ошибкой, а `f.Close()` закрывает файл:

```go
f, err := os.Open("data.txt")
if err != nil {
    return err
}
defer f.Close() // закроется при выходе из функции

// ... работаем с f, сколько угодно ветвей и return ...
```

Закрытие написано прямо под открытием, а не где-то далеко в конце. Глаз сразу видит пару «открыл — закрыл», и забыть про неё трудно.

### Порядок: LIFO

Если `defer` несколько, они выполняются в обратном порядке — последний отложенный срабатывает первым. Это как стопка тарелок: что положили сверху, то и берём первым (Last In, First Out).

```go play
package main

import "fmt"

func main() {
	fmt.Println("начало")
	defer fmt.Println("defer 1")
	defer fmt.Println("defer 2")
	defer fmt.Println("defer 3")
	fmt.Println("конец основного тела")
}
```

Вывод будет таким:

```
начало
конец основного тела
defer 3
defer 2
defer 1
```

Сначала отрабатывает всё тело `main`, и только при выходе срабатывают отложенные вызовы — в обратном порядке: последним отложили `defer 3`, он же печатается первым. Почему обратный порядок удобен? Ресурсы обычно освобождают в порядке, обратном захвату: что открыли последним — закрываем первым, как при разборке матрёшки.

Ещё одно правило, на котором путаются чаще всего: аргументы отложенного вызова вычисляются **сразу**, в момент `defer`, а не при выходе из функции.

```go play
package main

import "fmt"

func main() {
	x := 10
	defer fmt.Println("значение x на момент defer:", x)
	x = 99
	fmt.Println("x в конце:", x)
}
```

Вывод:

```text
x в конце: 99
значение x на момент defer: 10
```

Хотя `x` потом стал 99, отложенный вызов печатает 10: значение `x` «снялось» в строке с `defer`, а напечаталось позже.

## Частые ошибки

- **Забыли фигурные скобки.** В Go тело всегда в `{}`, даже на одну строку. `if x > 0 fmt.Println(x)` не скомпилируется. Круглые скобки вокруг условия допустимы, но `gofmt` их уберёт.
- **`else` на новой строке.** Ошибка компиляции `unexpected keyword else`. Пишите `} else {` в одну строку.
- **`:=` вместо `=` внутри блока.** Создаёт новую переменную, а внешняя остаётся прежней. Компилятор промолчит, ошибка проявится только в результате.
- **Ждёте `break` в `switch`, как в C.** Здесь он не нужен: после отработавшего `case` выход происходит сам. Если провал в следующую ветку действительно нужен, пишите `fallthrough`.
- **Думаете, что `defer` берёт «свежее» значение переменной.** Аргументы вычисляются в момент `defer`. Если нужна итоговая величина, откладывают безымянную функцию: `defer func() { fmt.Println(x) }()`. Такие функции разберём в [следующей главе](/go-basics/book/functions).
- **Используете переменную из init-выражения `if` снаружи.** Она видна только внутри `if`/`else`; снаружи компилятор скажет `undefined: n`.

## Попробуйте сами

В `main` сложите нечётные числа от 1 до 10 включительно. Используйте обычный `for`; чётные числа пропускайте через `continue`. Перед циклом отложите печать `готово` через `defer`, а после цикла напечатайте сумму. Предскажите порядок двух строк.

### Проверка

Сначала напечатается `25`, затем `готово`:

```go
func main() {
    defer fmt.Println("готово")
    sum := 0
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue
        }
        sum += i
    }
    fmt.Println(sum)
}
```

Добавьте `package main` и импорт `fmt`. `continue` пропускает прибавление чётного числа; `defer` срабатывает при выходе из `main`, а не при завершении цикла. Если убрать проверку с `continue`, сумма будет `55`.

## Что дальше

Пока весь код живёт внутри одной `main`, и с ростом программы она превратится в простыню. В следующей главе, [«Функции»](/go-basics/book/functions), вы научитесь выносить кусок логики под отдельное имя, передавать ему входные данные и получать результат. Форма `range` вернётся в [коллекциях](/go-basics/book/collections), а связка `defer` и `recover` — в [обработке ошибок](/go-basics/book/errors).
