Пока наши программы выполняли строки строго сверху вниз. Но уже задача со сдачей из прошлой главы просит большего: если денег не хватает, сдачу считать нельзя, нужно сообщить об этом. А чтобы сложить сто чисел, не хочется писать сто строк. Программе нужно уметь выбирать ветку и повторять шаги.
В Go для этого три конструкции: if, for и switch. Плюс слово defer, которое откладывает вызов до выхода из функции. Их немного, и устроены они проще, чем во многих языках: цикл всего один (for, без отдельных while и do-while), а круглые скобки вокруг условий не пишут.
if проверяет условие и выполняет блок, если оно истинно. Условие — это всегда выражение типа bool, и его не нужно оборачивать в круглые скобки. А вот фигурные скобки вокруг тела обязательны, даже для одной строки.
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 можно объявить переменную, которая будет жить только внутри if/else. Синтаксис: короткое объявление, точка с запятой, затем условие.
Покажем на функции strconv.Atoi из стандартной библиотеки. Она преобразует текст в число и возвращает два значения: само число и ошибку. Если ошибки нет, вторым значением приходит nil — специальное «пустое» значение. Объявлять такие функции мы научимся в следующей главе, а ошибки подробно разберём в девятой; здесь нам нужен только if.
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("готово")}
Вывод:
число: 84готово
Здесь n и err объявлены прямо в заголовке if. Они доступны и в ветке if, и в ветке else, но за пределами конструкции их нет. Часть программы, где имя видно, называют областью видимости. Узкая область видимости не даёт случайно использовать переменную там, где она уже не имеет смысла. Замените "42" на "сорок два" и запустите снова: сработает ветка else.
Любые фигурные скобки открывают новую область видимости. Если внутри неё написать := с уже существующим именем, Go не изменит старую переменную, а создаст новую с тем же именем, которая «заслонит» внешнюю до конца блока. Это называется затенением (shadowing):
package mainimport "fmt"func main() { status := "новый" if true { status := "проверен" // новая переменная, видна только в этом блоке fmt.Println("внутри:", status) } fmt.Println("снаружи:", status)}
Вывод:
внутри: проверенснаружи: новый
Внешний status не изменился. Чтобы поменять его, внутри блока нужно обычное присваивание status = "проверен". Компилятор такую ошибку не ловит: обе записи корректны. На ревью это одна из первых вещей, которые я ищу, особенно с переменной err.
for в Go умеет всё, что в других языках делают несколько разных циклов. У него есть несколько форм, и важно понять, что это одна и та же конструкция, просто с разным набором частей.
Три части через точку с запятой: инициализация, условие, шаг. Знакомо по C, только без скобок.
package mainimport "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. Отдельного ключевого слова не нужно — это всё ещё for.
package mainimport "fmt"func main() { n := 1 for n < 100 { n *= 2 } fmt.Println("первая степень двойки >= 100:", n)}
Цикл удваивает n, пока тот меньше 100: 1, 2, 4, …, 64, 128. На 128 условие ложно, и программа печатает первая степень двойки >= 100: 128. Переменную здесь меняем мы сами, внутри тела. Забудете это сделать — цикл станет бесконечным.
Есть ещё одна форма — 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, чьё условие истинно, и на этом всё. Обратите внимание на порядок: 18 подходит и под t < 25, и под default, но сработает первая подходящая ветка. В Go так обычно и заменяют длинную лестницу из if ... else if.
defer перед вызовом функции значит: «выполни это не сейчас, а когда текущая функция будет завершаться». Неважно, как она завершится: дойдёт до конца, выйдет через return из середины или аварийно остановится (такую остановку называют паникой, о ней в главе об ошибках).
Зачем это нужно? Программы постоянно берут ресурсы, которые потом надо вернуть: открытый файл, сетевое соединение, блокировку. Если у функции пять мест выхода, закрытие легко забыть в одном из них. defer позволяет написать закрытие один раз, сразу после открытия.
Типичный вид — фрагмент функции, которая читает файл. os.Open открывает файл и, как strconv.Atoi, возвращает два значения: файл и ошибку. return err выходит из функции с этой ошибкой, а f.Close() закрывает файл:
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значение 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) }(). Такие функции разберём в следующей главе.
Используете переменную из init-выражения if снаружи. Она видна только внутри if/else; снаружи компилятор скажет undefined: n.
В main сложите нечётные числа от 1 до 10 включительно. Используйте обычный for; чётные числа пропускайте через continue. Перед циклом отложите печать готово через defer, а после цикла напечатайте сумму. Предскажите порядок двух строк.
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, и с ростом программы она превратится в простыню. В следующей главе, «Функции», вы научитесь выносить кусок логики под отдельное имя, передавать ему входные данные и получать результат. Форма range вернётся в коллекциях, а связка defer и recover — в обработке ошибок.