Easy📖Теория7 min

Управляющие конструкции

Условия, циклы, switch, defer, goto и метки в Go

Управляющие конструкции

Условный оператор if/else

В Go оператор if не требует круглых скобок вокруг условия, но фигурные скобки обязательны даже для одной строки.

Базовый синтаксис

package main

import "fmt"

func main() {
    x := 42

    // Simple if
    if x > 0 {
        fmt.Println("positive")
    }

    // if/else
    if x%2 == 0 {
        fmt.Println("even")
    } else {
        fmt.Println("odd")
    }

    // if/else if/else
    if x < 0 {
        fmt.Println("negative")
    } else if x == 0 {
        fmt.Println("zero")
    } else {
        fmt.Println("positive")
    }
}

if с инициализацией (init statement)

Уникальная особенность Go — возможность объявить переменную прямо в if. Переменная существует только в scope if/else:

package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    // Variable 'err' is scoped to the if/else block
    if err := doSomething(); err != nil {
        fmt.Println("Error:", err)
    }
    // err is NOT accessible here

    // Practical example: opening a file
    if f, err := os.Open("config.json"); err != nil {
        fmt.Println("Cannot open file:", err)
    } else {
        // 'f' is accessible in the else block too
        defer f.Close()
        fmt.Println("File opened:", f.Name())
    }

    // Common pattern with type conversion
    if n, err := strconv.Atoi("42"); err == nil {
        fmt.Printf("Converted: %d\n", n)
    }
}

func doSomething() error {
    return nil
}

Идиоматический Go: init statement в if — стандартный паттерн для обработки ошибок. Он ограничивает scope переменных и делает код более читаемым.

Цикл for

В Go есть только один оператор цикла — for. Он заменяет for, while и do-while из других языков.

Классический for

func main() {
    // Classic three-component for loop
    for i := 0; i < 10; i++ {
        fmt.Println(i)
    }

    // Components are optional
    // Only condition (like while in other languages)
    n := 1
    for n < 100 {
        n *= 2
    }
    fmt.Println(n) // 128

    // Infinite loop (like while(true))
    counter := 0
    for {
        counter++
        if counter >= 5 {
            break
        }
    }
}

range — итерация по коллекциям

package main

import "fmt"

func main() {
    // Range over slice
    fruits := []string{"apple", "banana", "cherry"}
    for index, value := range fruits {
        fmt.Printf("%d: %s\n", index, value)
    }

    // Ignore index with blank identifier
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }

    // Only index (value is optional)
    for i := range fruits {
        fmt.Printf("index: %d\n", i)
    }

    // Range over map
    colors := map[string]string{
        "red":   "#FF0000",
        "green": "#00FF00",
        "blue":  "#0000FF",
    }
    for key, value := range colors {
        fmt.Printf("%s = %s\n", key, value)
    }
    // WARNING: map iteration order is random!

    // Range over string (iterates by runes, not bytes)
    for i, ch := range "Привет" {
        fmt.Printf("byte index %d: %c (U+%04X)\n", i, ch, ch)
    }

    // Range over channel
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
    for v := range ch {
        fmt.Println(v) // 1, 2, 3
    }

    // Range over integer (Go 1.22+)
    for i := range 5 {
        fmt.Println(i) // 0, 1, 2, 3, 4
    }
}

Захват переменных в циклах

До Go 1.22 была известная ловушка с захватом переменных цикла в замыканиях:

package main

import "fmt"

func main() {
    // Go 1.22+: each iteration creates a NEW variable
    // This works correctly now!
    funcs := make([]func(), 5)
    for i := range 5 {
        funcs[i] = func() {
            fmt.Println(i) // Each closure captures its own 'i'
        }
    }
    for _, f := range funcs {
        f() // Prints: 0, 1, 2, 3, 4
    }

    // Before Go 1.22, the same code would print: 4, 4, 4, 4, 4
    // because all closures shared the same loop variable
}

Важно: Начиная с Go 1.22, каждая итерация цикла for создаёт новую копию переменной. Это исправляет многолетнюю ловушку с замыканиями и горутинами в циклах.

break и continue

func main() {
    // break exits the loop
    for i := 0; i < 100; i++ {
        if i == 5 {
            break // Exit loop when i == 5
        }
        fmt.Println(i) // 0, 1, 2, 3, 4
    }

    // continue skips to the next iteration
    for i := 0; i < 10; i++ {
        if i%2 == 0 {
            continue // Skip even numbers
        }
        fmt.Println(i) // 1, 3, 5, 7, 9
    }
}

Switch

Go значительно улучшил switch по сравнению с C/C++/Java.

Expression switch

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Basic switch
    day := "Monday"
    switch day {
    case "Monday":
        fmt.Println("Start of work week")
    case "Friday":
        fmt.Println("Almost weekend!")
    case "Saturday", "Sunday": // Multiple values in one case
        fmt.Println("Weekend!")
    default:
        fmt.Println("Midweek")
    }

    // No break needed — Go breaks automatically
    // (unlike C/C++/Java)

    // Switch with init statement
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("macOS")
    case "linux":
        fmt.Println("Linux")
    default:
        fmt.Printf("Other: %s\n", os)
    }

    // Switch without expression (like if/else chain)
    hour := 14
    switch {
    case hour < 12:
        fmt.Println("Morning")
    case hour < 17:
        fmt.Println("Afternoon")
    case hour < 21:
        fmt.Println("Evening")
    default:
        fmt.Println("Night")
    }
}

fallthrough

По умолчанию Go не проваливается в следующий case. Чтобы явно провалиться, используйте fallthrough:

func main() {
    n := 3
    switch {
    case n > 0:
        fmt.Println("positive")
        fallthrough // Explicitly fall through to next case
    case n > -5:
        fmt.Println("greater than -5")
        // No fallthrough here — stops
    case n > -10:
        fmt.Println("greater than -10") // NOT executed
    }
    // Output:
    // positive
    // greater than -5
}

Внимание: fallthrough передаёт управление следующему case безусловно — условие следующего case не проверяется. Используется редко.

Type switch

Type switch проверяет конкретный тип значения интерфейса:

package main

import "fmt"

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Integer: %d\n", v)
    case string:
        fmt.Printf("String: %q (length %d)\n", v, len(v))
    case bool:
        fmt.Printf("Boolean: %t\n", v)
    case []int:
        fmt.Printf("Int slice: %v (length %d)\n", v, len(v))
    case nil:
        fmt.Println("nil value")
    default:
        fmt.Printf("Unknown type: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(true)
    describe([]int{1, 2, 3})
    describe(nil)
    describe(3.14)
}

Defer

defer откладывает вызов функции до момента возврата из текущей функции. Это мощный механизм для гарантированной очистки ресурсов.

Базовое использование

package main

import (
    "fmt"
    "os"
)

func main() {
    fmt.Println("Start")
    defer fmt.Println("Deferred 1")
    defer fmt.Println("Deferred 2")
    defer fmt.Println("Deferred 3")
    fmt.Println("End")

    // Output:
    // Start
    // End
    // Deferred 3  (LIFO order!)
    // Deferred 2
    // Deferred 1
}

Ключевое правило: Отложенные вызовы выполняются в порядке LIFO (Last In, First Out) — стек.

Типичные паттерны defer

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
    "sync"
)

// Pattern 1: Closing resources
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, fmt.Errorf("open file: %w", err)
    }
    defer f.Close() // Guaranteed to close, even if ReadAll fails

    data, err := io.ReadAll(f)
    if err != nil {
        return nil, fmt.Errorf("read file: %w", err)
    }
    return data, nil
}

// Pattern 2: Unlocking mutex
type SafeCounter struct {
    mu sync.Mutex
    v  map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock() // Guaranteed to unlock
    c.v[key]++
}

// Pattern 3: Closing HTTP response body
func fetchURL(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close() // Always close the body

    body, err := io.ReadAll(resp.Body)
    return string(body), err
}

// Pattern 4: Timing a function
func timed(name string) func() {
    start := time.Now()
    return func() {
        fmt.Printf("%s took %v\n", name, time.Since(start))
    }
}

func expensiveOperation() {
    defer timed("expensiveOperation")()
    // ... do work
}

Defer — ловушки и подводные камни

package main

import "fmt"

// TRAP 1: Arguments are evaluated immediately
func deferArgs() {
    x := 10
    defer fmt.Println("deferred x =", x) // x=10 is captured NOW
    x = 20
    fmt.Println("current x =", x)
    // Output:
    // current x = 20
    // deferred x = 10  (NOT 20!)
}

// TRAP 2: Defer in loop — all defers execute after function returns
func deferInLoop() {
    // BAD: all file handles stay open until function returns
    for i := 0; i < 1000; i++ {
        f, err := os.Open(fmt.Sprintf("file_%d.txt", i))
        if err != nil {
            continue
        }
        defer f.Close() // 1000 deferred calls accumulate!
    }
}

// BETTER: Use a helper function
func deferInLoopFixed() {
    for i := 0; i < 1000; i++ {
        if err := processFile(i); err != nil {
            fmt.Println(err)
        }
    }
}

func processFile(i int) error {
    f, err := os.Open(fmt.Sprintf("file_%d.txt", i))
    if err != nil {
        return err
    }
    defer f.Close() // Closes when processFile returns
    // ... process file
    return nil
}

// TRAP 3: Defer and named return values
func deferNamedReturn() (result int) {
    defer func() {
        result *= 2 // Modifies the named return value!
    }()
    return 21
    // Actually returns 42!
}

func main() {
    deferArgs()
    fmt.Println(deferNamedReturn()) // 42
}

Goto и метки (Labels)

Labeled break и continue

Метки полезны для выхода из вложенных циклов:

package main

import "fmt"

func main() {
    // Labeled break — exit outer loop
    matrix := [][]int{
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9},
    }

outer:
    for i, row := range matrix {
        for j, val := range row {
            if val == 5 {
                fmt.Printf("Found 5 at [%d][%d]\n", i, j)
                break outer // Breaks out of BOTH loops
            }
        }
    }

    // Labeled continue — skip to next iteration of outer loop
    fmt.Println("Pairs without matching indices:")
nextPair:
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if i == j {
                continue nextPair // Skip to next i
            }
            if i+j == 4 {
                fmt.Printf("(%d, %d) ", i, j)
            }
        }
    }
    fmt.Println()
}

goto

goto существует в Go, но его использование ограничено. Он не может перепрыгнуть через объявление переменной.

package main

import "fmt"

func main() {
    i := 0

loop:
    if i < 5 {
        fmt.Println(i)
        i++
        goto loop
    }

    // goto is sometimes used for error cleanup in C-style code
    // But in Go, defer is preferred
}

// Acceptable use: simplifying error handling in complex setup
func setup() error {
    resource1, err := acquireResource1()
    if err != nil {
        goto cleanup
    }

    resource2, err := acquireResource2()
    if err != nil {
        goto cleanup1
    }

    // Use resources...
    _ = resource1
    _ = resource2
    return nil

cleanup1:
    releaseResource1(resource1)
cleanup:
    return fmt.Errorf("setup failed: %w", err)
}

Практика: В реальном Go-коде goto используется крайне редко. Предпочитайте defer для очистки ресурсов и labeled break/continue для вложенных циклов.

Сравнительная таблица

Конструкция Особенность в Go
if Без скобок, init statement, скобки {} обязательны
for Единственный цикл, заменяет while/do-while
range Итерация по slice, map, string, channel, int (1.22+)
switch Нет fallthrough по умолчанию, без выражения = if/else chain
defer LIFO порядок, аргументы вычисляются сразу
goto Существует, но используется редко
break/continue Поддерживают метки для вложенных циклов

Проверь себя

🧪

Что выведет этот код? ```go x := 10 defer fmt.Println(x) x = 20 fmt.Println(x) ```

🧪

Что произойдёт, если в Go написать `switch` без `break` в case?

🧪

Какую переменную можно объявить в init statement оператора if?

🧪

Какой результат выполнения этого кода? ```go for i := range 3 { fmt.Print(i, " ") } ```

🧪

В каком порядке выполняются отложенные (defer) вызовы?