Содержание
«Не общайтесь через разделяемую память — разделяйте память через общение». Эту фразу из Go Proverbs помнят все, и из-за неё каналы тащат туда, где хватило бы одного sync.Mutex. Код становится длиннее, а гонки никуда не деваются.
Разберём простое правило выбора. Этот вопрос часто задают на собесах в формате «а почему здесь не канал?».
Правило в одну строку
Мьютекс защищает состояние. Канал передаёт владение данными или сигнал от одной горутины другой.
Если несколько горутин читают и меняют одну структуру (кэш, счётчик, map с сессиями), нужен мьютекс. Если данные текут по этапам и в каждый момент у них один хозяин, лучше подходят каналы.
| Задача | Выбор | Почему |
|---|---|---|
| Кэш в памяти | sync.RWMutex | Общее состояние, много чтений |
| Счётчик запросов | atomic.Int64 | Одна операция, мьютекс избыточен |
| Пайплайн обработки | каналы | Данные переходят из рук в руки |
| Сигнал «стоп» | context / close(ch) | Широковещательное событие |
| Пул воркеров | канал задач | Очередь с естественным backpressure |
Когда нужен мьютекс
Классика — кэш. Состояние живёт в одной структуре, методы короткие, критическая секция видна глазами.
type Cache struct {
mu sync.RWMutex
items map[string]Item
}
func (c *Cache) Get(key string) (Item, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
it, ok := c.items[key]
return it, ok
}
func (c *Cache) Set(key string, it Item) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = it
}
Тот же кэш на каналах потребует отдельной горутины-владельца, типа запроса с каналом ответа и аккуратной остановки. Кода втрое больше, а каждое чтение превращается в два переключения горутин.
Бесплатные материалы в TelegramРазборы задач с собесов, конспекты и вакансии для Go-разработчиков.
Подписаться →
Когда нужны каналы
Каналы хороши там, где есть поток: данные рождаются в одном месте, обрабатываются в другом и сохраняются в третьем. Каждый этап владеет значением, пока держит его, и делить ничего не нужно.
func parse(ctx context.Context, lines <-chan string) <-chan Event {
out := make(chan Event)
go func() {
defer close(out)
for line := range lines {
ev, err := decode(line)
if err != nil {
continue
}
select {
case out <- ev:
case <-ctx.Done():
return
}
}
}()
return out
}
Обрати внимание на select с ctx.Done(): без него при отмене горутина навсегда зависнет на отправке, если читатель уже ушёл. Это самая частая утечка горутин.
Типичные ошибки
| Ошибка | Что будет | Как поймать |
|---|---|---|
| Запись в map из двух горутин без мьютекса | fatal error: concurrent map writes, процесс падает без recover | go test -race |
Скопировали структуру с sync.Mutex | Копия и оригинал блокируются независимо, защиты нет | go vet (copylocks) |
Повторный Lock в той же горутине | Дедлок: мьютекс в Go не реентерабельный | Код-ревью, таймауты в тестах |
| Отправка в канал, который никто не читает | Утечка горутины | pprof goroutine, goleak в тестах |
close канала со стороны читателя | panic: send on closed channel у писателя | Закрывает только отправитель |
Чек-лист выбора
- Есть общая структура, которую читают и меняют? Мьютекс.
- Меняется одно число?
sync/atomic. - Данные переходят между этапами? Каналы.
- Нужно разослать «стоп» всем?
contextилиclose(ch). - Мьютекс держится дольше пары строк или вокруг I/O? Пересмотри дизайн, а не примитив.

