select и sync

select мультиплексирует каналы, а sync.WaitGroup ждёт завершения группы горутин.

select — это switch для каналов: он ждёт, пока готова любая из операций, и выполняет соответствующую ветку.

select: ждать несколько каналов сразу

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

package main

import (
    "fmt"
    "time"
)

func main() {
    fast := make(chan string)
    slow := make(chan string)

    go func() { time.Sleep(10 * time.Millisecond); fast <- "быстрый" }()
    go func() { time.Sleep(50 * time.Millisecond); slow <- "медленный" }()

    select {
    case msg := <-fast:
        fmt.Println("пришло:", msg)
    case msg := <-slow:
        fmt.Println("пришло:", msg)
    }
}

Вывод:

пришло: быстрый

С select часто используют тайм-аут: добавляют ветку с case <-time.After(d), чтобы не ждать вечно. Это стандартный приём для защиты от зависших операций.

sync.WaitGroup: дождаться всех

Помните костыль с time.Sleep из прошлого урока? Правильный инструмент — sync.WaitGroup. Это счётчик: Add увеличивает его на число горутин, каждая горутина по завершении вызывает Done, а Wait блокирует, пока счётчик не обнулится.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done() // отметить завершение
            fmt.Printf("воркер %d завершён\n", id)
        }(i)
    }

    wg.Wait() // ждём все три горутины
    fmt.Println("все воркеры закончили")
}

Вывод (порядок воркеров может отличаться):

воркер 3 завершён
воркер 1 завершён
воркер 2 завершён
все воркеры закончили

Обратите внимание на два момента. Первый: defer wg.Done() гарантирует уменьшение счётчика, даже если горутина упадёт. Второй: i передаётся в горутину аргументом (i) — это защищает от классической ловушки захвата переменной цикла.

sync.Mutex: когда без общей памяти не обойтись

Иногда несколько горутин всё же должны менять общую переменную. Чтобы избежать гонки, её защищают мьютексом: mu.Lock() перед изменением и mu.Unlock() после (обычно через defer). Но в Go это запасной вариант — сперва думают про каналы.

ИнструментЗачем
selectждать несколько каналов, тайм-ауты
sync.WaitGroupдождаться завершения группы горутин
sync.Mutexзащитить общую память от гонок

Итог

  • select — switch для каналов; с time.After даёт тайм-ауты.
  • WaitGroup через Add/Done/Wait заменяет костыль time.Sleep.
  • Mutex защищает общую память, но в Go это запасной вариант после каналов.
Проверьте себя
1. Что делает select?
AСортирует каналы
BЗакрывает все каналы
CЖдёт, пока готова любая из канальных операций, и выполняет её ветку
DСоздаёт новый канал
2. Зачем нужен sync.WaitGroup?
AЧтобы ускорить горутины
BЧтобы создавать каналы
CЧтобы остановить программу
DЧтобы дождаться завершения группы горутин без костыля time.Sleep
3. Что идиоматично делают перед использованием Mutex в Go?
AСначала рассматривают каналы, а Mutex — как запасной вариант
BСразу берут Mutex для всего
CИзбегают конкурентности вовсе
DИспользуют глобальные переменные