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Чтобы дождаться завершения группы горутин без костыля time.Sleep
CЧтобы создавать каналы
DЧтобы остановить программу
3. Что идиоматично делают перед использованием Mutex в Go?
AСразу берут Mutex для всего
BСначала рассматривают каналы, а Mutex — как запасной вариант
CИзбегают конкурентности вовсе
DИспользуют глобальные переменные
Поддержать проект