Глава 3. Управляющие конструкции: for, if, switch

421 просмотров
0 лайков
0 в избранном

Введение

Управляющие конструкции — это то, что превращает набор данных в программу: циклы, ветвления, выбор из вариантов. В большинстве языков для этого есть 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 с несколькими псевдонимами в каждом caseadd, и +), что делает интерфейс дружелюбнее. А защита от деления на ноль — вложенный 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 или завершение по внешнему сигналу. Особенно легко на этом попасться, если условие выхода зависит от переменной, которую в теле забыл менять.

Практика

  1. Напиши FizzBuzz от 1 до 30 через for i := range 30: кратные 3 — «Fizz», кратные 5 — «Buzz», кратные обоим — «FizzBuzz». Используй switch без выражения.
  2. Пройди по строке "Go — это круто!" через range и посчитай отдельно количество рун и количество байтов. Объясни разницу.
  3. Реализуй функцию, которая через if с init-выражением разбирает строку в число и возвращает «чётное»/«нечётное»/«не число».
  4. Напиши switch по типу (type switch), который принимает interface{} и печатает разное сообщение для int, float64, string и любого другого типа.
  5. Собери бесконечный цикл for {}, который читает числа (эмулируй срезом), суммирует их и выходит по break, встретив отрицательное. Выведи накопленную сумму.

Итог

Ты освоил все управляющие конструкции Go, а их всего три. Единственный цикл for заменяет while, do-while и бесконечный цикл в зависимости от набора частей; range элегантно перебирает коллекции и строки (с правильным декодированием рун), а новинка for i := range n из Go 1.22 делает счётные циклы короче и безопаснее. Ветвление if с фирменным init-выражением лежит в основе идиоматичной обработки ошибок. А switch в Go мощнее привычного: без случайного проваливания, с несколькими значениями в одном case, безусловной формой вместо цепочек else if и переключением по типу. Это завершает базовый блок курса: у тебя теперь есть переменные, типы и логика — всё, чтобы писать осмысленные программы. Дальше нас ждут функции, коллекции (массивы, срезы, карты) и структуры, где эти основы соберутся в полноценные приложения.

Комментарии 0

Для добавления комментариев необходимо войти или зарегистрироваться.

Пока нет комментариев. Станьте первым!