Любая программа рано или поздно должна решить «что делать дальше»: пойти по одной ветке или по другой, повторить шаг несколько раз, аккуратно прибраться за собой в конце. За это в Go отвечают три инструмента — if, for и switch, плюс особое слово defer, которое откладывает работу «на потом».
Хорошая новость: их немного, и они устроены проще, чем в большинстве языков. У Go всего один цикл — for. Никаких отдельных while и do-while, как в C или JavaScript. Скобок вокруг условий тоже нет. В этой главе разберём каждую конструкцию по очереди, с интуицией и примерами, которые можно запустить прямо на странице.
if проверяет условие и выполняет блок, если оно истинно. Условие — это всегда выражение типа bool, и его не нужно оборачивать в круглые скобки. А вот фигурные скобки вокруг тела обязательны, даже для одной строки.
if score >= 60 { fmt.Println("зачёт")} else if score >= 40 { fmt.Println("пересдача")} else { fmt.Println("незачёт")}
Обратите внимание: else стоит на той же строке, что и закрывающая скобка предыдущего блока. Это не вкусовщина — так требует автоформаттер gofmt, и иначе код просто не соберётся в привычном стиле.
У if есть приятная особенность: перед условием можно объявить переменную, которая будет жить только внутри if/else. Синтаксис — короткое объявление, точка с запятой, потом условие.
Это типичный приём, когда функция возвращает значение и признак успеха. Переменная не «утекает» в остальной код и видна ровно там, где нужна.
package mainimport ( "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 в Go умеет всё, что в других языках делают несколько разных циклов. У него есть несколько форм, и важно понять, что это одна и та же конструкция, просто с разным набором частей.
Если оставить только условие (без инициализации и шага), получится то, что в других языках называют while. Отдельного ключевого слова не нужно — это всё ещё for.
package mainimport "fmt"func main() { n := 1 for n < 100 { n *= 2 } fmt.Println("первая степень двойки >= 100:", n)}
Цикл удваивает n, пока тот меньше 100. Управление переменной полностью на нас: мы сами меняем n внутри тела.
Есть ещё одна форма — for ... range, которой удобно перебирать срезы, строки и map. Мы подробно разберём её в главе про коллекции, а пока просто покажем, как она выглядит:
for i, v := range []int{10, 20, 30} { fmt.Println(i, v)}
range сам отдаёт на каждом шаге индекс и значение, так что вручную считать счётчик не нужно.
switch — это компактная замена длинной цепочке if ... else if. В Go он удобнее, чем в C-подобных языках, по двум причинам.
Во-первых, после каждого caseне нуженbreak: Go сам выходит из switch, как только отработал подходящую ветку. Случайно «провалиться» в следующий case нельзя.
Во-вторых, case может содержать несколько значений через запятую.
package mainimport "fmt"func main() { for day := 1; day <= 7; day++ { switch day { case 6, 7: fmt.Println(day, "— выходной") default: fmt.Println(day, "— рабочий день") } }}
Ветка default срабатывает, если ни один case не подошёл. Она необязательна и может стоять в любом месте, но по традиции её пишут последней.
Иногда поведение «провалиться в следующий case» всё-таки полезно. Для этого есть явное слово fallthrough — оно передаёт управление в следующую ветку без проверки её условия. Это редкость, но знать о ней стоит.
package mainimport "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 убрать выражение после ключевого слова, каждый case становится самостоятельным булевым условием. Получается аккуратная цепочка if/else if, только читается приятнее.
package mainimport "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 откладывает вызов функции до момента, когда текущая функция вот-вот завершится — неважно, через return или из-за паники. Это решает вечную проблему: открыл ресурс — не забудь закрыть.
Главное применение — освобождение ресурсов рядом с местом их получения. Открыли файл — тут же пишем defer f.Close(), и больше об этом можно не думать: Go закроет файл на любом пути выхода из функции.
f, err := os.Open("data.txt")if err != nil { return err}defer f.Close() // закроется при выходе из функции// ... работаем с f, сколько угодно ветвей и return ...
Закрытие написано прямо под открытием, а не где-то далеко в конце. Глаз сразу видит пару «открыл — закрыл», и забыть про неё трудно.
Если defer несколько, они выполняются в обратном порядке — последний отложенный срабатывает первым. Это как стопка тарелок: что положили сверху, то и берём первым (Last In, First Out).
Сначала отрабатывает всё тело main, и только при выходе срабатывают отложенные вызовы — в обратном порядке: последним отложили defer 3, он же печатается первым. Почему обратный порядок удобен? Ресурсы обычно освобождают в порядке, обратном захвату: что открыли последним — закрываем первым, как при разборке матрёшки.
Ещё одна важная деталь: аргументы отложенного вызова вычисляются сразу, в момент defer, а не при фактическом выполнении. Запомним это и проверим на примере.
package mainimport "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 встретится снова в главе про ошибки и панику — именно там раскроется его связка с восстановлением после паники.