Введение
Управляющие конструкции — это то, что превращает набор данных в программу: циклы, ветвления, выбор из вариантов. В большинстве языков для этого есть for, while, do-while, тернарный оператор и switch со своими причудами. Go же в своём стиле максимального минимализма даёт всего ТРИ конструкции: for, if и switch. И этого хватает на всё.
Ключевая идея, которая удивляет новичков: в Go есть ровно один цикл — for. Никаких while и do-while. Один for с разным набором частей заменяет их все. В этой главе мы разберём все формы for, перебор коллекций через range и новинку Go 1.22 for i := range n; изучим if/else с фирменным init-выражением; и подробно пройдём switch — он в Go гораздо мощнее и удобнее, чем в C-подобных языках.
Классический for с тремя частями
Самая полная форма for состоит из трёх частей, разделённых точками с запятой: инициализация, условие, пост-выражение. Ровно как в C, но БЕЗ круглых скобок вокруг — их в Go нет.
package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i) // 0 1 2 3 4
}
}
Читается это так: перед первым проходом выполняется i := 0; перед каждым проходом проверяется i < 5 — если ложно, цикл завершается; после каждого прохода выполняется i++. Переменная i живёт только внутри цикла и снаружи недоступна — это правильно, счётчик не должен «протекать» в окружающий код.
for в роли while
Если убрать инициализацию и пост-выражение, оставив одно условие, получится то, что в других языках называется while. Точки с запятой при этом тоже убираются:
i := 0
for i < 10 {
fmt.Println(i)
i += 2
}
Это полноценная замена while: цикл крутится, пока условие истинно. Go намеренно не вводит отдельное ключевое слово — зачем, если for уже умеет это делать. Меньше синтаксиса — меньше того, что нужно держать в голове.
Бесконечный цикл
Убери вообще всё — получится бесконечный цикл. Это основа для серверов, воркеров и любых процессов, которые работают, пока их не остановят.
count := 0
for {
count++
if count == 3 {
break // выходим из цикла
}
fmt.Println("итерация", count)
}
Выйти из такого цикла можно оператором break, а перескочить к следующей итерации — оператором continue. Именно на конструкции for { ... } с выходом по условию строятся, например, циклы чтения из канала или обработки входящих соединений.
break и continue
break прерывает цикл целиком, continue — только текущую итерацию, переходя к следующей. Разберём на фильтрации:
for i := 1; i <= 10; i++ {
if i%2 != 0 {
continue // нечётные пропускаем
}
if i > 8 {
break // на 10 и дальше выходим совсем
}
fmt.Println(i) // 2 4 6 8
}
Здесь continue отбрасывает нечётные числа, не выполняя оставшуюся часть тела, а break полностью останавливает цикл, когда значение превысило порог. Эта пара покрывает почти все потребности в управлении ходом цикла. Для вложенных циклов есть ещё метки, но к ним прибегают редко — обычно это сигнал, что логику стоит вынести в отдельную функцию.
Перебор через range
Когда нужно пройти по коллекции — срезу, карте, строке, каналу — используют форму for ... range. Она возвращает на каждой итерации индекс (или ключ) и значение, избавляя от ручного счётчика и ошибок с границами.
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("index=%d value=%d
", i, v)
}
Часто нужен только один из двух — тут-то и пригождается пустой идентификатор из главы 2. Нужны только значения — заглушаем индекс через _. Нужны только индексы — просто не пишем вторую переменную.
// только значения
for _, v := range nums {
fmt.Println(v)
}
// только индексы
for i := range nums {
fmt.Println(i)
}
Для строк range ведёт себя особенно: он декодирует UTF-8 и выдаёт РУНЫ вместе с их байтовым смещением. Это правильный способ идти по символам многобайтового текста, о чём мы говорили в главе 1.
for i, r := range "Привет" {
// i — смещение в байтах (0, 2, 4, ...), r — руна
fmt.Printf("%d: %c
", i, r)
}
Обрати внимание: смещения идут не 0, 1, 2, а 0, 2, 4 — потому что каждая кириллическая руна занимает два байта. Но r при этом — полноценный символ, а не половина байта. Именно так надо перебирать текст, если в нём возможна не-латиница.
for i := range n — новинка Go 1.22
До версии 1.22 для простого «повторить N раз» приходилось писать классический for i := 0; i < n; i++. В Go 1.22 range научился работать прямо с целым числом: for i := range n перебирает i от 0 до n-1. Короче и без шанса ошибиться в условии (тот самый off-by-one).
// перебор с индексом: 0 1 2 3 4
for i := range 5 {
fmt.Println(i)
}
// просто повторить 3 раза, индекс не нужен
for range 3 {
fmt.Println("go")
}
Форма for i := range n полностью эквивалентна for i := 0; i < n; i++, но читается чище. А for range n без переменной — идеальный способ выразить «сделай это N раз». Граничные случаи ведут себя интуитивно: range 0 и range по отрицательному числу не делают ни одной итерации.
count := 0
for range -5 {
count++
}
fmt.Println(count) // 0 — ни одной итерации
Ещё одно важное изменение Go 1.22, о котором стоит знать: переменная цикла теперь СОЗДАЁТСЯ ЗАНОВО на каждой итерации. Раньше это была одна и та же переменная, что порождало классический баг при запуске горутин в цикле — все они видели последнее значение. Теперь эта грабля устранена на уровне языка.
if и else
Ветвление if в Go выглядит привычно, но снова БЕЗ скобок вокруг условия и с обязательными фигурными скобками даже для одной строки. Условие обязано быть булевым — «истинных» чисел и строк, как в некоторых языках, тут нет.
age := 20
if age >= 18 {
fmt.Println("совершеннолетний")
} else {
fmt.Println("несовершеннолетний")
}
Цепочки условий строятся через else if. Тернарного оператора (?:) в Go нет принципиально — считается, что явный if/else читается лучше.
score := 75
if score >= 90 {
fmt.Println("отлично")
} else if score >= 70 {
fmt.Println("хорошо")
} else if score >= 50 {
fmt.Println("удовлетворительно")
} else {
fmt.Println("неудовлетворительно")
}
if с init-выражением
У if в Go есть фирменная фишка: перед условием можно поставить короткий оператор инициализации, отделённый точкой с запятой. Переменные, объявленные в нём, видны только внутри if и его else. Это невероятно удобно для обработки ошибок — самой частой конструкции в Go.
import "strconv"
if n, err := strconv.Atoi("42"); err == nil {
fmt.Println("число:", n)
} else {
fmt.Println("не удалось разобрать:", err)
}
// n и err здесь уже недоступны — и это хорошо
Идиома такая: вызвали функцию, тут же в условии проверили результат, и переменные не «протекли» в остальной код. Это держит область видимости узкой и делает намерение очевидным. Ты будешь видеть паттерн if err := doSomething(); err != nil { return err } в Go-коде буквально повсюду — это стандартный способ обработки ошибок.
switch: мощнее, чем ты привык
В C-подобных языках switch — источник багов из-за проваливания между вариантами: забыл break — и выполнение потекло дальше. В Go всё наоборот: каждый case завершается автоматически, break писать не нужно. Проваливание есть, но оно ЯВНОЕ и требует специального слова fallthrough.
name := "Marcus"
switch name {
case "Tim":
fmt.Println("Привет, Тим")
case "Jenny":
fmt.Println("Привет, Дженни")
case "Marcus":
fmt.Println("Привет, Маркус")
default:
fmt.Println("Незнакомец")
}
Как только совпал case, выполнилось его тело и switch завершился — без всякого break. Ветка default срабатывает, если ни один case не подошёл, и её можно ставить в любом месте (по идиоме — в конце).
Несколько значений в одном case
В одном case можно перечислить несколько значений через запятую — сработает, если совпало любое из них. Это заменяет собой громоздкие цепочки ||.
switch day {
case "суббота", "воскресенье":
fmt.Println("выходной")
case "понедельник", "вторник", "среда", "четверг", "пятница":
fmt.Println("рабочий день")
}
Ещё одна форма — switch БЕЗ выражения. Тогда каждый case содержит булево условие, и это получается элегантная замена длинной цепочки if/else if. Многие считают такой стиль более читаемым.
switch {
case score >= 90:
fmt.Println("отлично")
case score >= 70:
fmt.Println("хорошо")
case score >= 50:
fmt.Println("удовлетворительно")
default:
fmt.Println("неудовлетворительно")
}
У switch, как и у if, тоже есть init-выражение: switch x := compute(); x { ... }. Оно работает так же — переменная видна только внутри switch.
fallthrough: явное проваливание
Если тебе всё же нужно, чтобы после совпавшего case выполнился и следующий, поставь в конце fallthrough. Это редкая конструкция, но иногда она уместна. Главное — в Go проваливание видно явно, а не прячется в забытом break.
switch "Marcus" {
case "Marcus":
fmt.Println("Маркус")
fallthrough // принудительно проваливаемся в следующий case
case "Medhi":
fmt.Println("Медхи")
case "Julian":
fmt.Println("Джулиан") // сюда НЕ попадём — fallthrough прыгает только на один case
}
// Вывод: Маркус, затем Медхи
Важная тонкость: fallthrough проваливается в следующий case БЕЗУСЛОВНО, не проверяя его условие, и только на ОДИН case вперёд. Используй его осознанно и редко — в большинстве случаев лучше перечислить значения через запятую.
Переключение по типу (кратко)
Особая форма — type switch — переключается не по значению, а по ТИПУ. Она работает с интерфейсами (подробно интерфейсы разберём позже) и позволяет узнать, какой конкретный тип скрывается за переменной. Синтаксис x.(type) допустим только внутри switch.
func describe(x interface{}) {
switch v := x.(type) {
case int:
fmt.Printf("целое: %d
", v)
case string:
fmt.Printf("строка длиной %d
", len(v))
case bool:
fmt.Printf("логическое: %t
", v)
default:
fmt.Printf("неизвестный тип: %T
", v)
}
}
describe(42) // целое: 42
describe("hi") // строка длиной 2
describe(true) // логическое: true
Внутри каждого case переменная v уже имеет соответствующий конкретный тип, так что с ней можно работать напрямую. Пока просто запомни, что такой инструмент есть — он пригодится, когда дойдём до интерфейсов и обработки значений неизвестного заранее типа.
Кейс из реального проекта
Напишем ядро парсера аргументов простого CLI-калькулятора, который принимает операцию и два числа: calc add 2 3. Здесь сходятся все конструкции главы — range по аргументам, if с init-выражением для разбора чисел, switch по нескольким значениям и обработка ошибок.
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
args := os.Args[1:]
if len(args) != 3 {
fmt.Fprintln(os.Stderr, "использование: calc ОП A B (ОП: add|sub|mul|div)")
os.Exit(1)
}
op := args[0]
// Разбираем оба операнда, отлавливая ошибку через if с init-выражением.
a, err := strconv.ParseFloat(args[1], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "не число: %q
", args[1])
os.Exit(1)
}
b, err := strconv.ParseFloat(args[2], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "не число: %q
", args[2])
os.Exit(1)
}
var result float64
switch op {
case "add", "+":
result = a + b
case "sub", "-":
result = a - b
case "mul", "*":
result = a * b
case "div", "/":
if b == 0 {
fmt.Fprintln(os.Stderr, "деление на ноль")
os.Exit(1)
}
result = a / b
default:
fmt.Fprintf(os.Stderr, "неизвестная операция: %q
", op)
os.Exit(1)
}
fmt.Printf("%g
", result)
}
Разберём, как конструкции работают вместе. Проверку количества аргументов делает обычный if. Разбор каждого числа идёт с проверкой ошибки — тот самый идиоматичный паттерн Go, где результат функции сразу проверяется. Выбор операции — switch с несколькими псевдонимами в каждом case (и add, и +), что делает интерфейс дружелюбнее. А защита от деления на ноль — вложенный if прямо внутри нужного case. Ветка default ловит любую неизвестную операцию. Собрать и попробовать:
go build -o calc main.go
./calc add 2 3 # 5
./calc div 10 4 # 2.5
Типичные ошибки
Скобки вокруг условия
Привычка из C/Java ставить круглые скобки вокруг условия. В Go это не ошибка, но и не идиома — gofmt их не уберёт, а рецензенты поморщатся. Хуже, когда забывают ОБЯЗАТЕЛЬНЫЕ фигурные скобки: даже для однострочного тела if они нужны всегда.
// НЕ надо: лишние круглые скобки, и без фигурных не скомпилируется
if (x > 0) fmt.Println(x)
// надо
if x > 0 {
fmt.Println(x)
}
Ожидание проваливания case как в C
Программисты, пришедшие из C или Java, иногда ждут, что после case выполнение потечёт дальше, и городят логику под это. В Go каждый case завершается сам. Если реально нужно проваливание — есть явный fallthrough. И наоборот: не пиши break в конце каждого case «на всякий случай» — он там лишний.
Байтовый индекс вместо range по строке
Перебирать многобайтовую строку через индекс for i := 0; i < len(s); i++ и обращаться к s[i] — значит получать отдельные БАЙТЫ, а не символы. Для не-латиницы это выдаст мусор. Иди по строке через range, который декодирует руны.
s := "Привет"
// НЕ надо для не-латиницы: s[i] — это байт
for i := 0; i < len(s); i++ {
fmt.Printf("%c", s[i]) // Ð, Ÿ... мусор
}
// надо
for _, r := range s {
fmt.Printf("%c", r) // П, р, и, в, е, т
}
Забытое условие выхода из бесконечного цикла
Написал for { ... } и забыл break или условие завершения — получил зависание программы. Бесконечный цикл всегда должен иметь чёткий выход: break, return или завершение по внешнему сигналу. Особенно легко на этом попасться, если условие выхода зависит от переменной, которую в теле забыл менять.
Практика
- Напиши FizzBuzz от 1 до 30 через
for i := range 30: кратные 3 — «Fizz», кратные 5 — «Buzz», кратные обоим — «FizzBuzz». Используйswitchбез выражения. - Пройди по строке
"Go — это круто!"черезrangeи посчитай отдельно количество рун и количество байтов. Объясни разницу. - Реализуй функцию, которая через
ifс init-выражением разбирает строку в число и возвращает «чётное»/«нечётное»/«не число». - Напиши
switchпо типу (type switch), который принимаетinterface{}и печатает разное сообщение дляint,float64,stringи любого другого типа. - Собери бесконечный цикл
for {}, который читает числа (эмулируй срезом), суммирует их и выходит поbreak, встретив отрицательное. Выведи накопленную сумму.
Итог
Ты освоил все управляющие конструкции Go, а их всего три. Единственный цикл for заменяет while, do-while и бесконечный цикл в зависимости от набора частей; range элегантно перебирает коллекции и строки (с правильным декодированием рун), а новинка for i := range n из Go 1.22 делает счётные циклы короче и безопаснее. Ветвление if с фирменным init-выражением лежит в основе идиоматичной обработки ошибок. А switch в Go мощнее привычного: без случайного проваливания, с несколькими значениями в одном case, безусловной формой вместо цепочек else if и переключением по типу. Это завершает базовый блок курса: у тебя теперь есть переменные, типы и логика — всё, чтобы писать осмысленные программы. Дальше нас ждут функции, коллекции (массивы, срезы, карты) и структуры, где эти основы соберутся в полноценные приложения.
Комментарии 0
Пока нет комментариев. Станьте первым!