День 3: Goroutines, Channels, Defer/Panic/Recover

🎯 Темы дня

  1. Goroutines: планировщик GMP, утечки, loop variable capture
  2. Channels: select fairness, directional channels, nil channel tricks
  3. 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 (в случайном порядке)
    }()
}

Утечки горутин

Горутина утекает, если:

  1. Блокируется на чтении из канала, который никто не закроет
  2. Блокируется на sync.Mutex, который никто не отпустит
  3. Бесконечный цикл без проверки ctx.Done()

Диагностика: pprof → goroutine profile.


📖 2. Channels — тонкости

Select fairness

Если несколько case готовы, select выбирает случайный:

go
ch1 := 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

go
func producer(ch chan<- int) { // только запись
    ch <- 42
}

func consumer(ch <-chan int) { // только чтение
    val := <-ch
    fmt.Println(val)
}

Зачем: компилятор ловит ошибки на этапе компиляции.

⚠️ Ловушка: Nil channel tricks

go
var ch1 chan int // nil
ch2 := make(chan int)

select {
case <-ch1: // заблокируется навсегда
case val := <-ch2:
    fmt.Println(val)
}

Паттерн: Отключение ветки select через nil:

go
var sendCh chan int
if shouldSend {
    sendCh = dataCh
}

select {
case sendCh <- data: // если sendCh == nil, эта ветка игнорируется
case <-ctx.Done():
}

⚠️ Ловушка: Закрытие канала

go
ch := 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

go
func foo() {
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
}
// Вывод: 3, 2, 1

Когда вычисляются аргументы

go
func foo() {
    x := 1
    defer fmt.Println(x) // выведет 1 (аргумент вычисляется сразу)
    x = 2
}

Но для указателей:

go
func foo() {
    x := 1
    defer func() {
        fmt.Println(*&x) // выведет 2 (читает значение в момент выполнения)
    }()
    x = 2
}

Defer в цикле — утечка ресурсов

go
for i := 0; i < 1000; i++ {
    f, _ := os.Open("file.txt")
    defer f.Close() // ❌ все файлы закроются только при выходе из функции
}

Правильно:

go
for i := 0; i < 1000; i++ {
    func() {
        f, _ := os.Open("file.txt")
        defer f.Close()
        // обработка
    }()
}

Panic и Recover

go
func 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
}

Правила:

  1. recover работает только в defer
  2. recover в обычной функции не сработает
  3. После recover паника прекращается

⚠️ Ловушка: Panic в горутине

go
func main() {
    go func() {
        panic("oops") // ❌ упадет вся программа!
    }()
    time.Sleep(time.Second)
}

Почему: Паника в горутине не ловится recover из другой горутины.

Правильно:

go
go 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 в горутине

Напиши функцию, которая запускает горутину, и если та паникует — логирует ошибку, но не роняет программу.


🗣️ Каверзные вопросы

  1. Что выведет?

    go
    func foo() {
        defer fmt.Println("defer")
        fmt.Println("main")
        panic("oops")
    }

    Ответ: "main", "defer", затем panic. Defer выполняется даже при панике.

  2. Что выведет?

    go
    func foo() (result int) {
        defer func() {
            result++
        }()
        return 0
    }

    Ответ: 1. Defer выполняется после return, но до выхода из функции. Именованный return доступен в defer.

  3. Почему panic в горутине роняет программу? Ответ: Паника распространяется только в пределах одной горутины. Если не поймана — программа падает.

  4. Что будет, если закрыть nil channel? Ответ: Panic.


🧩 Проверь себя

1. Что выведут горутины до Go 1.22 в этом коде? for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() }
2. Если несколько case в select готовы одновременно, какой выполнится?
3. Что произойдёт при повторном close(ch) уже закрытого канала?
4. Что произойдёт при panic внутри горутины?
5. Что происходит при чтении из nil-канала в select?
6. Что выведет этот код? x := 1; defer fmt.Println(x); x = 2
7. Как планировщик GMP распределяет горутины, если локальная очередь P пуста?
8. Почему defer f.Close() в цикле — утечка ресурсов?

✅ Чек-лист дня

  • Понимаю, как работает loop variable capture и как его избежать
  • Знаю, что select выбирает случайный готовый case
  • Могу использовать nil channel для отключения ветки select
  • Понимаю порядок выполнения defer (LIFO)
  • Знаю, что recover работает только в defer
  • Понимаю, почему panic в горутине роняет программу