Kontrollfluss in 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)
}