null

Go вместе изучать Go. Часть 2

Введение

Язык программирования Go (Golang) отличается своей простотой и лаконичностью. Одна из его сильных сторон — минималистичный набор управляющих конструкций, которые при этом остаются мощными и выразительными. В Go отсутствуют привычные циклы while и do-while, нет скобок вокруг условий if, а конструкции вроде switch и defer реализованы с необычным, но очень удобным поведением.

В этой статье мы подробно рассмотрим основные управляющие конструкции Go:

  1. for — единственный цикл в языке, заменяющий все привычные виды итераций;
  2. if и else — условные конструкции с возможностью объявления переменных;
  3. switch — компактная и безопасная альтернатива множественным if-блокам;
  4. defer — уникальный механизм отложенного выполнения функций.

Мы также разберём синтаксис, особенности и практические примеры по каждому из этих инструментов.

1. Цикл for

В Go существует только одна конструкция для циклов — оператор for.

Несмотря на это, благодаря гибкости синтаксиса он покрывает все типы циклов, известные в других языках: for, while, do-while и бесконечные циклы.

1.1. Классическая форма for

Базовый синтаксис выглядит так:

for инициализация; условие; пост-выражение {
    // тело цикла
}

  • Инициализация (init statement) — выполняется один раз перед первой итерацией.
  • Условие (condition expression) — проверяется перед каждой итерацией; если оно false, цикл завершается.
  • Пост-выражение (post statement) — выполняется после каждой итерации (обычно увеличение счётчика).

Пример:

package main

import "fmt"

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println("Сумма:", sum)
}

Особенности:

  • Скобки вокруг частей for не ставятся — это делает код чище.
  • Фигурные скобки { } обязательны.
  • Переменная i, объявленная в инициализации, существует только внутри цикла.

1.2. Пропуск частей цикла

Любой из трёх элементов (инициализация, условие, пост-выражение) можно опустить:

for ; sum < 1000; {
	sum += sum
}

В Go это часто используется для имитации цикла while — просто уберите точки с запятой:

for sum < 1000 {
	sum += sum
}

1.3. Бесконечный цикл

Если опустить условие, цикл станет бесконечным:

for {
	fmt.Println("Работаю вечно!")
}

Чтобы выйти из бесконечного цикла, используют break или return.

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

Оператор if в Go используется для проверки условий и выполнения блоков кода в зависимости от результата выражения.

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

if условие {
    // действия, если условие истинно
} else {
    // действия, если условие ложно
}

Отличие от других языков:

  • Нет круглых скобок вокруг условия.
  • Фигурные скобки обязательны — даже если тело состоит из одной строки.

Пример:

package main

import (
	"fmt"
	"math"
)

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}

func main() {
	fmt.Println(sqrt(2), sqrt(-4))
}

2.2. Объявление переменных внутри if

Go позволяет объявлять переменные прямо в условии:

if v := math.Pow(x, n); v < lim {
	return v
}

Переменная v существует только внутри блока if и, при наличии, в соответствующем else.

Пример:

package main

import (
	"fmt"
	"math"
)

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g\n", v, lim)
	}
	// v здесь уже недоступна
	return lim
}

func main() {
	fmt.Println(pow(3, 2, 10), pow(3, 3, 20))
}

Такой подход повышает читаемость и ограничивает область видимости переменных.

3. Конструкция switch

Оператор switch — это краткая и безопасная альтернатива множественным if-else блокам.

Он позволяет выполнить один из нескольких вариантов кода в зависимости от значения выражения.

3.1. Основной синтаксис

switch выражение {
case значение1:
	// действия
case значение2:
	// действия
default:
	// действия по умолчанию
}

Пример:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Вы запустили GO на ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("macOS.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", os)
	}
}

3.2. Отличия от switch в других языках

В Go не нужно писать break — выполнение останавливается автоматически после успешного case.

Условия в case могут быть не константами и не обязательно целыми числами.

Можно использовать короткое объявление переменной в switch.

3.3. Проверка по порядку

Go выполняет case сверху вниз, и прекращает выполнение при первом совпадении.

Пример:

switch i {
case 0:
case f():
}

Функция f() не вызовется, если i == 0.

3.4. switch без условия

Если не указать выражение, switch будет работать как switch true — то есть выполняет первый case, где условие истинно. Это позволяет создавать читаемые цепочки условий:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Доброе утро!")
	case t.Hour() < 17:
		fmt.Println("Добрый день.")
	default:
		fmt.Println("Добрый вечер.")
	}
}

4. Отложенные вызовы (defer)

Оператор defer откладывает выполнение функции до выхода из текущей функции.

Это особенно полезно для освобождения ресурсов, закрытия файлов, разблокировки мьютексов и других завершающих действий.

4.1. Простой пример

package main

import "fmt"

func main() {
	defer fmt.Println("из tune-it")
	fmt.Println("привет ")
}

Результат:

привет
из tune-it

Как это работает:

Аргументы функции вычисляются сразу, когда вызывается defer.

Но сам вызов откладывается до завершения функции main().

4.2. Порядок выполнения (стек defer)

Отложенные функции помещаются в стек, и выполняются в обратном порядке — последняя добавленная выполняется первой.

Пример:

package main

import "fmt"

func main() {
	fmt.Println("считаем")

	for i := 0; i < 5; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("закончили считать")
}

Вывод:

считаем
закончили считать
4
3
2
1
0

Это свойство делает defer удобным инструментом для автоматического завершения действий в обратном порядке их выполнения — например, закрытия нескольких открытых файлов.

Заключение

Управляющие конструкции в Go — это фундаментальные инструменты, которые определяют поток выполнения программы. Они реализованы просто и последовательно, без лишнего синтаксического «шума», что делает код Go лаконичным и понятным даже для новичков.

Вот основные выводы из изученного:

for — единственная конструкция цикла в Go, заменяющая все привычные аналоги (for, while, do-while).

Она гибкая и может использоваться как обычный цикл, условный (while) или бесконечный.

Отсутствие скобок делает код визуально чище.

if и else — простые, но мощные условные операторы.

Не требуют круглых скобок вокруг условий.

Позволяют объявлять переменные прямо внутри выражения if, ограничивая их область видимости.

switch — безопасная и выразительная альтернатива множественным if-else.

Не требует break — выполнение останавливается автоматически.

Может работать с любыми типами данных, а не только с числами.

Поддерживает использование без условия, превращаясь в элегантную замену длинных логических цепочек.

defer — уникальный механизм Go для отложенного выполнения функций.

Гарантирует выполнение важных операций (например, освобождение ресурсов) при выходе из функции.

Отложенные вызовы выполняются в порядке LIFO (последним добавлен — первым выполнен).

Благодаря этим конструкциям Go остаётся простым, но мощным языком: он поощряет чистый, предсказуемый и безопасный стиль программирования, в котором разработчик фокусируется на логике задачи, а не на сложных синтаксических деталях.

Дополнительные материалы

Примеры реального применения defer в Go

1. Закрытие файлов после чтения или записи

Самый распространённый сценарий — автоматическое закрытие файла, даже если в функции произойдёт ошибка.

package main

import (
	"bufio"
	"fmt"
	"os"
)

func readFile(path string) {
	file, err := os.Open(path)
	if err != nil {
		fmt.Println("Ошибка открытия файла:", err)
		return
	}
	defer file.Close() // файл гарантированно закроется при выходе из функции

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}

	if err := scanner.Err(); err != nil {
		fmt.Println("Ошибка при чтении:", err)
	}
}

func main() {
	readFile("data.txt")
}

Зачем нужен defer:

Без него пришлось бы явно вызывать file.Close() в нескольких местах, включая обработку ошибок.

С defer — файл закроется всегда, независимо от того, где функция завершится.

2. Закрытие HTTP-ответа (response.Body)

При работе с сетью важно закрывать тело ответа (Body) после использования. Это предотвращает утечку сетевых ресурсов.

package main

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

func fetch(url string) {
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("Ошибка запроса:", err)
		return
	}
	defer resp.Body.Close() // тело ответа закроется автоматически

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("Ошибка чтения:", err)
		return
	}
	fmt.Println("Длина ответа:", len(body))
}

func main() {
	fetch("https://golang.org")
}

Преимущество: даже если чтение тела завершится ошибкой, соединение всё равно будет корректно закрыто.

3. Разблокировка мьютекса (mutex unlock)

При работе с многопоточностью важно разблокировать ресурсы даже при ошибках.

package main

import (
	"fmt"
	"sync"
)

var mu sync.Mutex
var counter int

func increment() {
	mu.Lock()
	defer mu.Unlock() // гарантированное освобождение блокировки

	counter++
	fmt.Println("Counter:", counter)
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}
	wg.Wait()
}

Плюс: даже если внутри increment() произойдёт ошибка, defer mu.Unlock() гарантирует, что блокировка снимется и не приведёт к “deadlock”.

4. Очистка временных файлов

При создании временных файлов можно использовать defer, чтобы они удалялись после использования:

package main

import (
	"fmt"
	"os"
)

func processTempFile() {
	file, err := os.CreateTemp("", "example-*.txt")
	if err != nil {
		panic(err)
	}
	defer os.Remove(file.Name()) // файл будет удалён в конце функции

	fmt.Println("Создан временный файл:", file.Name())
	file.WriteString("Временные данные...")
}

func main() {
	processTempFile()
}

Итог: временный файл удалится автоматически — не нужно помнить о ручной очистке.

5. Отслеживание выполнения (логирование входа/выхода из функции)

defer часто используют для отладки — чтобы автоматически логировать начало и завершение функций.

package main

import "fmt"

func trace(msg string) func() {
	fmt.Println("Начало:", msg)
	return func() { fmt.Println("Конец:", msg) }
}

func work() {
	defer trace("work")() // defer вызывает возвращённую функцию при выходе
	fmt.Println("Работаем...")
}

func main() {
	work()
}

Результат:

Начало: work 
Работаем... 
Конец: work

Преимущество: удобно для профилирования, логирования и отладки больших систем.

6. Обработка паник (восстановление через recover)

defer позволяет безопасно перехватывать паники и предотвращать «падение» программы.

package main

import "fmt"

func safeDivide(a, b int) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Восстановлено после паники:", r)
		}
	}()
	fmt.Println("Результат:", a/b)
}

func main() {
	safeDivide(10, 2)
	safeDivide(5, 0)
	fmt.Println("Программа завершена без краша")
}

Смысл: если произойдёт деление на ноль, recover() «поймает» панику и программа продолжит работу.

7. Измерение времени выполнения функции

С помощью defer удобно измерять производительность функций.

package main

import (
	"fmt"
	"time"
)

func measure(name string) func() {
	start := time.Now()
	return func() {
		fmt.Printf("%s заняла %v\n", name, time.Since(start))
	}
}

func slowOperation() {
	defer measure("slowOperation")()
	time.Sleep(2 * time.Second)
}

func main() {
	slowOperation()
}

Вывод:

slowOperation заняла 2.0001234s

Используется для: профилирования и мониторинга производительности.

8. Очистка после ошибок при нескольких шагах

Если в функции выполняются несколько действий, каждое из которых нужно откатить при ошибке, defer поможет автоматизировать порядок их отмены:

package main

import "fmt"

func setup() {
	fmt.Println("Настройка системы...")
	defer fmt.Println("Откат: очищаем ресурсы 1")
	defer fmt.Println("Откат: очищаем ресурсы 2")
	defer fmt.Println("Откат: очищаем ресурсы 3")

	fmt.Println("Что-то пошло не так!")
}

func main() {
	setup()
}

Вывод:

Настройка системы...
Что-то пошло не так!
Откат: очищаем ресурсы 3
Откат: очищаем ресурсы 2
Откат: очищаем ресурсы 1

Особенность: порядок выполнения обратный — это делает defer отличным инструментом для “rollback”-механизмов.

9. Использование в тестах

В тестах часто применяют defer для очистки данных после проверки:

package main

import (
	"fmt"
	"os"
)

func main() {
	f, _ := os.Create("test.txt")
	defer os.Remove("test.txt") // удаляем после теста
	defer f.Close()             // закрываем файл

	fmt.Fprintln(f, "Тестовые данные")
	fmt.Println("Тест завершён")
}

Результат: даже если тест завершится ошибкой, временный файл будет закрыт и удалён.

defer — это не просто “отложенный вызов”, а мощный инструмент гарантированного завершения действий.

Он делает код надёжным, чистым и безопасным даже в сложных ситуациях, когда возможны ошибки, паники или несколько точек выхода из функции.

Ошибки (error) и паника (panic) в Go

В Go существует два механизма для обработки непредвиденных ситуаций — ошибки и паника. Они решают разные задачи и используются в разных случаях.

Ошибка (error)

Ошибки — это обычная часть логики программы.

Функции возвращают значения типа error, чтобы сообщить, что операция не удалась, но выполнение можно продолжить.

Пример:

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

result, err := divide(10, 0)
if err != nil {
	fmt.Println("Ошибка:", err)
	return
}

Ошибки:

  • ожидаемы — это нормальный сценарий работы;
  • обрабатываются явно (if err != nil);
  • не прерывают программу.

Используются для:

  • неверного ввода пользователя;
  • отсутствующих файлов;
  • сетевых сбоев;
  • бизнес-ошибок.

Паника (panic)

Паника — это критическая ситуация, когда программа не может безопасно продолжать выполнение.

Она может быть вызвана:

  • автоматически (например, деление на ноль, выход за границы массива);
  • вручную через panic("сообщение").
func checkAge(age int) {
	if age < 0 {
		panic("возраст не может быть отрицательным")
	}
}

Когда происходит паника:

  • Обычное выполнение останавливается.
  • Go вызывает все отложенные (defer) функции.
  • Если паника не перехвачена, программа аварийно завершается.

Восстановление после паники — recover()

Чтобы не “уронить” программу, панику можно перехватить через recover() внутри defer:

func safeRun() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Восстановлено после паники:", r)
		}
	}()
	panic("что-то пошло не так")
}

recover() возвращает значение из panic() и позволяет продолжить работу программы.

Используйте error, когда ошибка — часть нормального потока выполнения.

Используйте panic только для фатальных, непоправимых ситуаций, когда программа не может продолжить работу.

Если нужно перехватить панику и не завершать выполнение, воспользуйтесь defer + recover().

Иными словами:

Ошибка — это “что-то пошло не так, но мы можем продолжить”.

Паника — это “что-то пошло настолько не так, что продолжать опасно”.

Вперед