Условные конструкции и циклы
Привет! Мы на третьем уроке — здесь начинаем управлять потоком выполнения программы. Без условий и циклов любой код был бы просто последовательностью команд, как робот, который делает одно и то же.
В Go всё просто и мощно:
- Только один цикл —
for(но он умеет всё!) - Удобный
ifс возможностью объявлять переменные прямо в условии - Многофункциональный
switch
Открывайте редактор и пробуйте каждый пример — это лучший способ понять, как работает управление потоком.
Условные конструкции
Оператор if
В Go конструкции if, else if и else служат для условного выполнения кода: программа проверяет логическое условие в круглых скобках после if (без фигурных скобок вокруг условия — это особенность языка), и если оно истинно (true), выполняется блок кода в фигурных скобках; если ложно — проверяется следующая ветка else if (их может быть сколько угодно), а если ни одно условие не выполнилось — срабатывает финальный else (он опционален).
Эти конструкции нужны для принятия решений в программе: выбора действий в зависимости от значений переменных, результатов вычислений, ввода пользователя или ошибок — например, проверка возраста, валидация данных, обработка разных случаев в калькуляторе или реакция на ошибку при открытии файла.
В Go if также может иметь инициализацию переменной перед условием (например, if err := someFunc(); err != nil { ... }), что удобно для локальной обработки ошибок без загрязнения внешней области видимости.
if условие {
// выполняется, если условие true
}
if условие {
// true
} else {
// false
}
if условие1 {
// ...
} else if условие2 {
// ...
} else {
// ни одно не подошло
}
Примеры
var age int = 25
if age < 0 {
return "Возраст не может быть отрицательным!"
} else if age < 13 {
return "Ребёнок"
} else if age < 18 {
return "Подросток"
} else if age < 65 {
return "Взрослый"
} else {
return "Пенсионер"
}
Особенность Go: инициализация в if
Можно объявить переменную прямо в условии — она будет видна только внутри блока if/else.
var num int = 2
if v := num * 2; v > 0 {
fmt.Printf("%d — положительное, удвоенное: %d\n", num, v)
} else {
fmt.Printf("%d — не положительное, удвоенное: %d\n", num, v)
}
// fmt.Println(v) // ошибка! v не существует здесь
Это очень удобно при работе с ошибками:
if result, err := someFunction(); err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Результат:", result)
}
Разница между этими двумя вариантами
Первый вариант:
var num int = 2
if v := num * 2; v > 0 {
fmt.Printf("%d — положительное, удвоенное: %d\n", num, v)
} else {
fmt.Printf("%d — не положительное, удвоенное: %d\n", num, v)
}
// fmt.Println(v) // ошибка! v не существует здесь
Второй вариант:
var (
num int = 2
v int = num * 2
)
if v > 0 {
fmt.Printf("%d — положительное, удвоенное: %d\n", num, v)
} else {
fmt.Printf("%d — не положительное, удвоенное: %d\n", num, v)
}
// fmt.Println(v) // Всё сработает!
1. Область видимости (scope) переменной v — Главное видимое различие
-
В первом варианте (
if v := num * 2; v > 0 { ... }):vобъявлена внутри условия if (это называется if-scope)vдоступна только внутри блокаifи внутриelse- За пределами
if-else—vне существует → ошибка компиляции
-
Во втором варианте (
var v int = num * 2):vобъявлена в scope функции- Доступна во всей функции, включая после
if-else
2. Время вычисления выражения num * 2
Вот менее очевидная, но важная разница:
-
В первом варианте:
- Выражение
num * 2вычисляется только один раз — при входе вif. - Даже если потом в
elseмы используемv, это то же самое значение.
- Выражение
-
Во втором варианте:
- Тоже вычисляется один раз (при объявлении
v). - По факту — точно такое же поведение.
- Тоже вычисляется один раз (при объявлении
По производительности и вычислениям — разницы нет.
3. Идиоматичность и стиль Go
Вот где настоящая разница в практике:
-
Первый вариант (с объявлением в if) — более идиоматичный в Go, когда:
- Переменная нужна только для проверки условия и используется внутри
if/else. - Ты хочешь ограничить область видимости максимально (принцип наименьшей видимости).
- Это делает код чище и предотвращает случайное использование переменной позже.
Пример из стандартной библиотеки и реальных проектов:
if err := doSomething(); err != nil {
return err // err доступна и в else, если есть
} - Переменная нужна только для проверки условия и используется внутри
-
Второй вариант — используй, когда:
- Переменная нужна и после
if-else. - Или ты хочешь явно показать, что она используется в нескольких местах.
- Переменная нужна и после
Вывод: есть ли разница кроме видимости?
| Аспект | Первый вариант (v в if) | Второй вариант (v перед if) |
|---|---|---|
Область видимости v | Только внутри if-else | Во всей функции |
Время вычисления num*2 | Один раз | Один раз |
Доступность после if-else | Нет | Да |
| Идиоматичность в Go | Предпочтительнее (меньше scope) | Нормально, но если не нужно — избыточно |
| Производительность | Одинаковая | Одинаковая |
Заключение:
Основная разница — в области видимости. Других существенных различий (по скорости, поведению) нет.
Но в сообществе Go первый стиль считается лучше, потому что он:
- Уменьшает область видимости.
- Делает код понятнее (читатель видит: "v нужна только для этой проверки").
- Предотвращает баги от случайного использования переменной позже.
Используй объявление вif, когда переменная нужна только там — это общепринятый стиль в Go
Оператор switch
В Go конструкция switch предназначена для выбора одного из множества вариантов выполнения кода в зависимости от значения выражения (или условий в особом формате) — это удобная и читаемая альтернатива длинной цепочке if-else if-else.
После ключевого слова switch может стоять выражение (например, переменная), и тогда каждый case проверяет равенство этому выражению, либо switch может быть без выражения — тогда в case пишутся полноценные логические условия (как в if).
Выполняется только первый подходящий case (или default, если ничего не подошло), и благодаря автоматическому break в конце каждого case (в отличие от C) код не «проваливается» дальше — если нужно продолжить выполнение следующего case, добавляют специальное слово fallthrough. switch часто используют для обработки разных значений одной переменной (тип, статус, код ошибки, день недели), разбора тегов или даже для замены сложных условных цепочек, делая код чище и быстрее (компилятор оптимизирует простые switch лучше, чем if-else).
switch выражение {
case значение1:
// код
case значение2, значение3: // несколько вариантов
// код
default:
// если ничего не подошло
}
Примеры
var day int = 2
switch day {
case 1:
fmt.Println("Понедельник")
case 2:
fmt.Println("Вторник") // Вернётся "Вторник"
case 3:
fmt.Println("Среда")
case 4:
fmt.Println("Четверг")
case 5:
fmt.Println("Пятница 😎")
case 6, 7:
fmt.Println("Выходной! 🎉")
default:
fmt.Println("Неверный день")
}
Switch без выражения — альтернатива цепочке if-else:
var score int = 85
switch {
case score >= 90:
fmt.Println("Отлично! 🌟")
case score >= 75:
fmt.Println("Хорошо")
case score >= 60:
fmt.Println("Удовлетворительно")
default:
fmt.Println("Нужно подтянуться")
}
fallthrough — редкая, но полезная фишка: принудительно перейти к следующему case.
var n int = 10
switch {
case n < 0:
fmt.Println("Отрицательное")
fallthrough
case n%2 == 0:
fmt.Println("Чётное")
default:
fmt.Println("Положительное нечётное")
}
Циклы
В Go конструкция for — это единственный цикл в языке (нет while, do-while и т.п.), но он настолько гибкий, что покрывает все возможные сценарии. Классический вид — как в C: for i := 0; i < 10; i++ { ... }, где есть инициализация, условие и пост-итерация.
Однако в Go for может быть и в упрощённых формах: просто с условием — for i < 10 { i++ ... } (аналог while), или вовсе без ничего — for { ... } (бесконечный цикл, прерывается break или return). Самая популярная идиома — for index, value := range collection { ... }, которая позволяет удобно перебирать массивы, срезы, строки (по рунам), мапы (ключи и значения) и каналы.
Внутри цикла можно использовать break (выход из цикла), continue (переход к следующей итерации) и даже метки (labels) для выхода из вложенных циклов — например, outer: for ... { for ... { if condition { break outer } } }.
for в Go прост, предсказуем, эффективен и идеально подходит для всего: от простого счёта до обработки данных в коллекциях, делая код чистым и читаемым.
Классические формы for
// Стандартный цикл счётчик
for i := 0; i < 10; i++ {
fmt.Print(i, " ")
}
// Вывод: 0 1 2 3 4 5 6 7 8 9
// Как while в других языках
count := 0
for count < 5 {
fmt.Println("count =", count)
count++
}
/* Вывод:
count = 0
count = 1
count = 2
count = 3
count = 4
*/
// Бесконечный цикл
for {
// что-то делаем
break // обязательно выйдем когда-нибудь!
}
for range — итерация по коллекциям
Самая часто используемая форма — перебор элементов.
numbers := []int{10, 20, 30, 40}
// Индекс и значение
for i, val := range numbers {
fmt.Printf("numbers[%d] = %d\n", i, val)
}
/* Вывод:
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 40
*/
// Только значение (часто используем _ для игнора индекса)
for _, val := range numbers {
fmt.Println(val * 2)
}
/* Вывод:
20
40
60
80
*/
// Строки — перебор рун (Unicode символов)
text := "Привет, Go! 🚀"
for i, r := range text {
fmt.Printf("Символ %d: %c (код: %d)\n", i, r, r)
}
/* Вывод:
Символ 0: П (код: 1055)
Символ 2: р (код: 1088)
Символ 4: и (код: 1080)
Символ 6: в (код: 1074)
Символ 8: е (код: 1077)
Символ 10: т (код: 1090)
Символ 12: , (код: 44)
Символ 13: (код: 32)
Символ 14: G (код: 71)
Символ 15: o (код: 111)
Символ 16: ! (код: 33)
Символ 17: (код: 32)
Символ 18: 🚀 (код: 128640)
*/
Управление циклами
break— выйти из цикла полностьюcontinue— пропустить остаток текущей итерации и перейти к следующей- Метки (labels) — для выхода из вложенных циклов
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i == 2 && j == 3 {
break outer // выйдем из обоих циклов
}
fmt.Printf("(%d,%d) ", i, j)
}
fmt.Println()
}
/* Вывод:
(0,0) (0,1) (0,2) (0,3) (0,4)
(1,0) (1,1) (1,2) (1,3) (1,4)
(2,0) (2,1) (2,2)
*/