День 3: Goroutines, Channels, Defer/Panic/Recover
🎯 Темы дня
- Goroutines: планировщик GMP, утечки, loop variable capture
- Channels: select fairness, directional channels, nil channel tricks
- Defer/Panic/Recover: порядок выполнения, когда срабатывает
📖 1. Goroutines — глубокое погружение
Планировщик GMP
- G (Goroutine) — легковесный поток (2KB стек)
- M (Machine) — OS thread
- P (Processor) — логический процессор (по умолчанию = числу CPU)
Правило: P содержит очередь локальных G. Если очередь пуста — забирает из глобальной очереди или "ворует" у другого P (work stealing).
⚠️ Ловушка: Loop variable capture (до Go 1.22)
go// ❌ До Go 1.22 for i := 0; i < 3; i++ { go func() { fmt.Println(i) // все выведут 3! }() }
Почему: Переменная i одна на все итерации, горутины захватывают её по ссылке.
Исправления:
go// Вариант 1: Передать как аргумент for i := 0; i < 3; i++ { go func(val int) { fmt.Println(val) }(i) } // Вариант 2: Создать локальную переменную for i := 0; i < 3; i++ { i := i // shadow go func() { fmt.Println(i) }() } // ✅ Go 1.22+: переменная цикла создается на каждую итерацию for i := 0; i < 3; i++ { go func() { fmt.Println(i) // 0, 1, 2 (в случайном порядке) }() }
Утечки горутин
Горутина утекает, если:
- Блокируется на чтении из канала, который никто не закроет
- Блокируется на
sync.Mutex, который никто не отпустит - Бесконечный цикл без проверки
ctx.Done()
Диагностика: pprof → goroutine profile.
📖 2. Channels — тонкости
Select fairness
Если несколько case готовы, select выбирает случайный:
goch1 := make(chan int, 1) ch2 := make(chan int, 1) ch1 <- 1 ch2 <- 2 select { case <-ch1: fmt.Println("ch1") case <-ch2: fmt.Println("ch2") }
Вывод может быть "ch1" или "ch2" — рандомно.
Directional channels
gofunc producer(ch chan<- int) { // только запись ch <- 42 } func consumer(ch <-chan int) { // только чтение val := <-ch fmt.Println(val) }
Зачем: компилятор ловит ошибки на этапе компиляции.
⚠️ Ловушка: Nil channel tricks
govar ch1 chan int // nil ch2 := make(chan int) select { case <-ch1: // заблокируется навсегда case val := <-ch2: fmt.Println(val) }
Паттерн: Отключение ветки select через nil:
govar sendCh chan int if shouldSend { sendCh = dataCh } select { case sendCh <- data: // если sendCh == nil, эта ветка игнорируется case <-ctx.Done(): }
⚠️ Ловушка: Закрытие канала
goch := make(chan int) close(ch) close(ch) // ❌ panic: close of closed channel ch <- 1 // ❌ panic: send on closed channel val, ok := <-ch // ✅ val=0, ok=false
📖 3. Defer/Panic/Recover
Defer — порядок LIFO
gofunc foo() { defer fmt.Println(1) defer fmt.Println(2) defer fmt.Println(3) } // Вывод: 3, 2, 1
Когда вычисляются аргументы
gofunc foo() { x := 1 defer fmt.Println(x) // выведет 1 (аргумент вычисляется сразу) x = 2 }
Но для указателей:
gofunc foo() { x := 1 defer func() { fmt.Println(*&x) // выведет 2 (читает значение в момент выполнения) }() x = 2 }
Defer в цикле — утечка ресурсов
gofor i := 0; i < 1000; i++ { f, _ := os.Open("file.txt") defer f.Close() // ❌ все файлы закроются только при выходе из функции }
Правильно:
gofor i := 0; i < 1000; i++ { func() { f, _ := os.Open("file.txt") defer f.Close() // обработка }() }
Panic и Recover
gofunc safeDiv(a, b int) (result int, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) } }() result = a / b return }
Правила:
recoverработает только вdeferrecoverв обычной функции не сработает- После
recoverпаника прекращается
⚠️ Ловушка: Panic в горутине
gofunc main() { go func() { panic("oops") // ❌ упадет вся программа! }() time.Sleep(time.Second) }
Почему: Паника в горутине не ловится recover из другой горутины.
Правильно:
gogo func() { defer func() { if r := recover(); r != nil { log.Printf("recovered: %v", r) } }() panic("oops") }()
💻 Практика (1–2 часа)
Задание 1: Loop variable capture
Напиши код, который запускает 5 горутин, каждая печатает свой номер. Проверь, что работает корректно.
Задание 2: Select с nil channel
Реализуй паттерн: есть два канала, но один временно отключен. Используй nil channel для отключения ветки select.
Задание 3: Recover в горутине
Напиши функцию, которая запускает горутину, и если та паникует — логирует ошибку, но не роняет программу.
🗣️ Каверзные вопросы
-
Что выведет?
gofunc foo() { defer fmt.Println("defer") fmt.Println("main") panic("oops") }Ответ: "main", "defer", затем panic. Defer выполняется даже при панике.
-
Что выведет?
gofunc foo() (result int) { defer func() { result++ }() return 0 }Ответ: 1. Defer выполняется после
return, но до выхода из функции. Именованный return доступен в defer. -
Почему panic в горутине роняет программу? Ответ: Паника распространяется только в пределах одной горутины. Если не поймана — программа падает.
-
Что будет, если закрыть nil channel? Ответ: Panic.
🧩 Проверь себя
✅ Чек-лист дня
- Понимаю, как работает loop variable capture и как его избежать
- Знаю, что select выбирает случайный готовый case
- Могу использовать nil channel для отключения ветки select
- Понимаю порядок выполнения defer (LIFO)
- Знаю, что recover работает только в defer
- Понимаю, почему panic в горутине роняет программу