🐹 Go

गो में नियंत्रण प्रवाह

Условия и циклы в Go: if, for, switch и defer

Go намеренно сделали простым: здесь нет while, нет do-while — есть только один цикл for, но в трёх разных формах. Нет скобок вокруг условий. Switch работает без break. А defer — уникальный оператор, которого нет в большинстве языков. Давай разберёмся!

if: условие без скобок

В Go условие пишется БЕЗ скобок — это отличает Go от Java, C# и JavaScript. Зато фигурные скобки тела ОБЯЗАТЕЛЬНЫ, даже для одной строки.

package main

import "fmt"

func main() {
    age := 16
    
    // В Go нет скобок вокруг условия!
    if age >= 18 {
        fmt.Println("Взрослый")
    } else {
        fmt.Println("Несовершеннолетний")
    }
    
    // Нельзя так (без скобок тела):
    // if age >= 18 fmt.Println("OK") // ОШИБКА
    
    // if-else if-else
    score := 75
    if score >= 90 {
        fmt.Println("Отлично")
    } else if score >= 75 {
        fmt.Println("Хорошо")
    } else if score >= 60 {
        fmt.Println("Удовлетворительно")
    } else {
        fmt.Println("Плохо")
    }
}

if с инициализацией: уникальная фича Go

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // if может содержать инициализирующий оператор
    // Переменная n видна только внутри if/else блока!
    if n, err := strconv.Atoi("42"); err == nil {
        fmt.Printf("Число: %d\n", n) // 42
    } else {
        fmt.Printf("Ошибка: %v\n", err)
    }
    // fmt.Println(n) // ОШИБКА! n не виден здесь
    
    // Это удобно для обработки ошибок:
    if result, err := divide(10, 2); err != nil {
        fmt.Println("Ошибка:", err)
    } else {
        fmt.Println("Результат:", result) // 5
    }
}

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("деление на ноль")
    }
    return a / b, nil
}

for: единственный цикл в Go

В Go нет while и do-while. Есть только for, но в трёх формах — и этого достаточно!

Форма 1: классический for (как C/C++)

package main

import "fmt"

func main() {
    // Обычный for: (инициализация; условие; шаг)
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ") // 0 1 2 3 4
    }
    fmt.Println()
    
    // Обратный счёт
    for i := 10; i >= 1; i-- {
        fmt.Print(i, " ") // 10 9 8 7 6 5 4 3 2 1
    }
    fmt.Println()
    
    // С шагом 2
    for i := 0; i <= 10; i += 2 {
        fmt.Print(i, " ") // 0 2 4 6 8 10
    }
    fmt.Println()
}

Форма 2: for как while

package main

import "fmt"

func main() {
    // for только с условием = while в других языках
    health := 100
    damage := 15
    
    for health > 0 {
        fmt.Printf("HP: %d\n", health)
        health -= damage
    }
    fmt.Println("Игрок погиб!")
    
    // Аналог do-while
    attempt := 0
    for {
        attempt++
        fmt.Printf("Попытка %d\n", attempt)
        if attempt >= 3 {
            break // Выход из цикла
        }
    }
}

Форма 3: for range — перебор коллекций

package main

import "fmt"

func main() {
    // Перебор среза
    fruits := []string{"Яблоко", "Банан", "Вишня"}
    for i, fruit := range fruits {
        fmt.Printf("%d: %s\n", i, fruit)
    }
    // 0: Яблоко
    // 1: Банан
    // 2: Вишня
    
    // Только значение (без индекса)
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }
    
    // Только индекс
    for i := range fruits {
        fmt.Println(i)
    }
    
    // Перебор map
    ages := map[string]int{
        "Алиса": 20,
        "Борис": 25,
        "Вера":  22,
    }
    for name, age := range ages {
        fmt.Printf("%s: %d лет\n", name, age)
    }
    
    // Перебор строки (по рунам)
    for i, ch := range "Привет" {
        fmt.Printf("%d: %c\n", i, ch)
    }
}

break и continue

package main

import "fmt"

func main() {
    // break — выход из цикла
    for i := 0; i < 10; i++ {
        if i == 5 {
            break
        }
        fmt.Print(i, " ") // 0 1 2 3 4
    }
    fmt.Println()
    
    // continue — пропуск итерации
    for i := 0; i < 10; i++ {
        if i%2 != 0 {
            continue // Пропускаем нечётные
        }
        fmt.Print(i, " ") // 0 2 4 6 8
    }
    fmt.Println()
    
    // Именованные метки для вложенных циклов
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break outer // Выходим из ВНЕШНЕГО цикла
            }
            fmt.Printf("(%d,%d) ", i, j)
        }
    }
    // (0,0) (0,1) (0,2) (1,0)
}

switch: без break, с умными условиями

В Go switch НЕ нужен break! Каждый case автоматически останавливается. Это устраняет классическую ошибку «забыл break».

package main

import "fmt"

func main() {
    day := 3
    
    // Обычный switch
    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:           // Несколько значений в одном case
        fmt.Println("Выходной!")
    default:
        fmt.Println("Неверный день")
    }
    // Среда
    
    // switch с условиями (без выражения — как if-else)
    score := 85
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B") // Выведет это
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("F")
    }
    
    // switch с инициализацией
    switch n := 42; {
    case n < 0:
        fmt.Println("Отрицательное")
    case n == 0:
        fmt.Println("Ноль")
    default:
        fmt.Println("Положительное") // Выведет это
    }
    
    // fallthrough — выполнить следующий case тоже
    x := 1
    switch x {
    case 1:
        fmt.Println("Один")
        fallthrough // Продолжаем в следующий case
    case 2:
        fmt.Println("Два или после одного")
    case 3:
        fmt.Println("Три")
    }
    // Один
    // Два или после одного
}

defer: отложенное выполнение

defer — уникальная конструкция Go. Она откладывает выполнение функции до момента, когда текущая функция вернётся. Как напоминание «перед выходом из комнаты — выключи свет».

package main

import "fmt"

func main() {
    fmt.Println("Начало")
    defer fmt.Println("Конец (defer)") // Выполнится последним!
    fmt.Println("Середина")
    fmt.Println("Ещё середина")
    // Порядок вывода:
    // Начало
    // Середина
    // Ещё середина
    // Конец (defer)
    
    // Несколько defer — стек LIFO (последний вошёл — первый вышел)
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    fmt.Println("Основной код")
    // Порядок:
    // Основной код
    // defer 3
    // defer 2
    // defer 1
}

Практическое применение defer: очистка ресурсов

package main

import (
    "fmt"
    "os"
)

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Файл ТОЧНО закроется когда функция завершится!
    
    // Работаем с файлом...
    fmt.Println("Обрабатываем файл:", filename)
    
    return nil
    // file.Close() вызовется здесь автоматически
}

func main() {
    err := processFile("test.txt")
    if err != nil {
        fmt.Println("Ошибка:", err)
    }
}

Типичные ошибки

Ошибка 1: Скобки вокруг условия

// В Go не нужны скобки!
if (x > 0) { }     // Работает, но IDE предупредит
if x > 0 { }       // Правильный стиль Go

Ошибка 2: Бесконечный цикл без break

// Этот цикл никогда не закончится!
for {
    fmt.Println("бесконечно")
    // Нужен break или return!
}

// Правильно:
for {
    // ... логика ...
    if done {
        break
    }
}

Итого

  • ✅ if — без скобок вокруг условия, скобки тела обязательны
  • ✅ if с инициализацией — if x := f(); x > 0 { }
  • ✅ for — единственный цикл в Go (три формы: классический, while, range)
  • ✅ for range — перебор срезов, map, строк
  • ✅ switch — без break, case автоматически останавливается; fallthrough если нужно продолжить
  • ✅ defer — откладывает выполнение до возврата функции, удобно для очистки ресурсов

Рекурсия и defer в практике

package main

import (
    "fmt"
    "os"
)

// defer для очистки ресурсов (самое частое применение)
func processFile(name string) error {
    f, err := os.Open(name)
    if err != nil {
        return fmt.Errorf("не удалось открыть файл: %w", err)
    }
    defer f.Close()  // ГАРАНТИРОВАННО закроется при выходе из функции!
    
    // Работаем с файлом...
    fmt.Printf("Обрабатываем: %s\n", name)
    return nil
}

// Порядок defer: LIFO (последний вошёл — первый вышел)
func deferOrder() {
    defer fmt.Println("Первый defer")   // Выполнится третьим
    defer fmt.Println("Второй defer")   // Выполнится вторым
    defer fmt.Println("Третий defer")   // Выполнится первым
    fmt.Println("Основной код")
    // Вывод:
    // Основной код
    // Третий defer
    // Второй defer
    // Первый defer
}

// defer с изменением возвращаемого значения
func doubleReturns() (result int) {
    defer func() {
        result *= 2  // Изменяем именованное возвращаемое значение!
    }()
    return 5  // result = 5, потом defer * 2 = 10
}

func main() {
    deferOrder()
    fmt.Println("Результат:", doubleReturns())  // 10
}

Panic и recover: обработка критических ошибок

package main

import "fmt"

// panic — выбросить критическую ошибку
// recover — поймать panic (только внутри defer!)

func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("поймали панику: %v", r)
        }
    }()
    
    if b == 0 {
        panic("деление на ноль!")  // Это вызовет recover выше
    }
    return a / b, nil
}

func main() {
    result, err := safeDiv(10, 2)
    fmt.Println(result, err)  // 5 
    
    result, err = safeDiv(10, 0)
    fmt.Println(result, err)  // 0 поймали панику: деление на ноль!
}

Обработка ошибок в Go

package main

import (
    "errors"
    "fmt"
)

// Обычная ошибка
var ErrNotFound = errors.New("не найдено")

// Типизированная ошибка (для дополнительной информации)
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("поле %s: %s", e.Field, e.Message)
}

// Функция возвращает ошибку как второе значение
func findUser(id int) (string, error) {
    users := map[int]string{1: "Алиса", 2: "Борис"}
    if name, ok := users[id]; ok {
        return name, nil
    }
    return "", fmt.Errorf("пользователь %d: %w", id, ErrNotFound)
}

func main() {
    // Стандартная обработка ошибок в Go
    name, err := findUser(1)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    fmt.Println("Нашли:", name)
    
    _, err = findUser(99)
    if err != nil {
        // errors.Is — проверяем тип ошибки
        if errors.Is(err, ErrNotFound) {
            fmt.Println("Пользователь не существует")
        } else {
            fmt.Println("Другая ошибка:", err)
        }
    }
}

Итого

  • ✅ if — без скобок вокруг условия, фигурные обязательны
  • ✅ if с инициализацией: if v := f(); v > 0 { }
  • ✅ for — единственный цикл в Go (три формы)
  • ✅ for range — перебор срезов, map, каналов, строк
  • ✅ switch — без break; case автоматически завершается
  • ✅ fallthrough — выполнить следующий case
  • ✅ defer — выполнить перед выходом из функции (LIFO)
  • ✅ panic/recover — для критических, неожиданных ошибок
  • ✅ Ошибки — это обычные возвращаемые значения типа error

Практический пример: FizzBuzz и сортировка пузырьком

package main

import "fmt"

// FizzBuzz — классическая задача
func fizzBuzz(n int) string {
    switch {
    case n%15 == 0:
        return "FizzBuzz"
    case n%3 == 0:
        return "Fizz"
    case n%5 == 0:
        return "Buzz"
    default:
        return fmt.Sprintf("%d", n)
    }
}

// Сортировка пузырьком — с вложенными циклами и defer
func bubbleSort(arr []int) []int {
    n := len(arr)
    result := make([]int, n)
    copy(result, arr)  // Не меняем оригинал
    
    for i := 0; i < n-1; i++ {
        for j := 0; j < n-i-1; j++ {
            if result[j] > result[j+1] {
                result[j], result[j+1] = result[j+1], result[j]  // Swap!
            }
        }
    }
    return result
}

func main() {
    // FizzBuzz
    fmt.Println("FizzBuzz 1-20:")
    for i := 1; i <= 20; i++ {
        fmt.Printf("%s ", fizzBuzz(i))
    }
    fmt.Println()
    
    // Сортировка
    nums := []int{64, 25, 12, 22, 11}
    sorted := bubbleSort(nums)
    fmt.Printf("До: %v\n", nums)
    fmt.Printf("После: %v\n", sorted)
    
    // Бинарный поиск
    target := 22
    left, right := 0, len(sorted)-1
    found := -1
    for left <= right {
        mid := (left + right) / 2
        switch {
        case sorted[mid] == target:
            found = mid
            left = right + 1  // Выход
        case sorted[mid] < target:
            left = mid + 1
        default:
            right = mid - 1
        }
    }
    if found >= 0 {
        fmt.Printf("Найдено %d на позиции %d\n", target, found)
    }
}

Итого и дополнительные примеры

package main

import "fmt"

// Проверка простоты числа
func isPrime(n int) bool {
    if n < 2 { return false }
    for i := 2; i*i <= n; i++ {
        if n%i == 0 { return false }
    }
    return true
}

func main() {
    // Все простые числа до 50
    fmt.Print("Простые до 50: ")
    for i := 2; i <= 50; i++ {
        if isPrime(i) { fmt.Print(i, " ") }
    }
    fmt.Println()
    // 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47
    
    // Числа Фибоначчи через for
    a, b := 0, 1
    fmt.Print("Fibonacci: ")
    for a < 1000 {
        fmt.Print(a, " ")
        a, b = b, a+b
    }
    fmt.Println()
    // 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
    
    // defer + for
    fmt.Println("Обратный отсчёт:")
    for i := 5; i >= 1; i-- {
        defer fmt.Printf("%d... ", i)
    }
    fmt.Println("Пуск!")
    // Пуск!
    // 1... 2... 3... 4... 5... (LIFO)
}