GraphLMS

Основы
Начать
Глава 03 · Основы~12 мин чтения

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

О чём глава

Любая программа рано или поздно должна решить «что делать дальше»: пойти по одной ветке или по другой, повторить шаг несколько раз, аккуратно прибраться за собой в конце. За это в Go отвечают три инструмента — if, for и switch, плюс особое слово defer, которое откладывает работу «на потом».

Хорошая новость: их немного, и они устроены проще, чем в большинстве языков. У Go всего один цикл — for. Никаких отдельных while и do-while, как в C или JavaScript. Скобок вокруг условий тоже нет. В этой главе разберём каждую конструкцию по очереди, с интуицией и примерами, которые можно запустить прямо на странице.

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

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

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

Обратите внимание: else стоит на той же строке, что и закрывающая скобка предыдущего блока. Это не вкусовщина — так требует автоформаттер gofmt, и иначе код просто не соберётся в привычном стиле.

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

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

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

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("готово")
}

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

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

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

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

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

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

Переменная i живёт только внутри цикла. На каждом проходе проверяется условие i <= 5; как только оно станет ложным, цикл закончится.

Форма «while»

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

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

Цикл удваивает n, пока тот меньше 100. Управление переменной полностью на нас: мы сами меняем n внутри тела.

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

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

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

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

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

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

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 может содержать несколько значений через запятую.

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 — оно передаёт управление в следующую ветку без проверки её условия. Это редкость, но знать о ней стоит.

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, только читается приятнее.

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, чьё условие истинно, и на этом всё. Такой «switch-true» — идиоматичный способ заменить лестницу из if ... else if в Go.

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

defer откладывает вызов функции до момента, когда текущая функция вот-вот завершится — неважно, через return или из-за паники. Это решает вечную проблему: открыл ресурс — не забудь закрыть.

Главное применение — освобождение ресурсов рядом с местом их получения. Открыли файл — тут же пишем defer 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).

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, а не при фактическом выполнении. Запомним это и проверим на примере.

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

Несмотря на то что x потом стал 99, отложенный вызов напечатает 10: значение x «снялось» в момент, когда мы написали defer. Это частый источник путаницы, поэтому держите правило в голове.

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

  • Забыли фигурные скобки или поставили лишние круглые. В Go условие пишется без (), а тело всегда в {}, даже на одну строку. if x > 0 fmt.Println(x) не скомпилируется.
  • Ждёте break в switch, как в C. Здесь он не нужен: после отработавшего case выход происходит сам. А вот если вам нужен провал — пишите fallthrough явно.
  • Думаете, что defer берёт «свежее» значение переменной. Аргументы вычисляются в момент defer, а не при выходе из функции. Если нужна именно итоговая величина — передавайте её через замыкание: defer func() { fmt.Println(x) }().
  • Используете переменную из init-выражения if снаружи. Она видна только внутри if/else. После конструкции её уже нет — это специально, чтобы не засорять область видимости.

Что дальше

Теперь у вас есть полный набор для управления потоком выполнения: ветвление через if и switch, повторение через единственный цикл for, и аккуратная уборка через defer. Эти конструкции вы будете использовать буквально в каждой функции.

Дальше мы соберём данные в структуры и коллекции, где впервые по-настоящему пригодится форма for ... range. А defer встретится снова в главе про ошибки и панику — именно там раскроется его связка с восстановлением после паники.