Управляющие конструкции
Условный оператор 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для очистки ресурсов и labeledbreak/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 |
Поддерживают метки для вложенных циклов |