Mutex защищает общую память. Канал передаёт владение значением между горутинами. RWMutex выгоден, когда чтений много, а запись редкая.
Конкурентность Go golang собеседование
В Go есть два честных стиля. Первый: не делить память, а передавать сообщения. Второй: делить память и охранять её замком. Документация любит первое. Прод любит оба. Счётчик, кэш в структуре, карта сессий — обычно mutex. Пайплайн стадий, пул задач, «отдай ресурс воркеру» — канал.
Нет правила «каналы всегда правильнее». Канал на каждый инкремент метрики — лишние горутины и сложность. Мьютекс вокруг всего хендлера, включая HTTP-вызов, держит всех и ловит взаимную блокировку. Выбор — про то, что является инвариантом: ячейка памяти или передача владения.
Аналогия: общий блокнот на столе — нужен замок (mutex). Передать блокнот коллеге из рук в руки — канал: после передачи вы его больше не пишете.
Mutex
sync.Mutex — нерекурсивный замок. Два Lock подряд из той же горутины — взаимная блокировка. Unlock без Lock — panic. Копировать структуру, в которой лежит mutex, нельзя. Копия уносит состояние «заперт / открыт». Две горутины думают, что владеют разными замками, а память общая. Поэтому методы с мьютексом — на указателе.
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
Счётчик состояния — mutex. Unlock в defer, методы на указателе, структуру не копируют.
Пример. Сделали c2 := c1, где у Counter внутри mutex. Одна горутина лочит c1, другая — копию c2. Это разные замки, поле n одно. Получается гонка.
Долгая работа под замком (RPC, диск) держит всех остальных. Под замком оставляют короткий участок: поправили map, сняли. Unlock не в defer легко забыть на ветке return с ошибкой.
RWMutex и каналы
RWMutex пускает много читателей или одного писателя. Имеет смысл, когда чтение доминирует и само короткое. Писатель может голодать, если читатели не кончаются. Рекурсии нет: RLock и затем Lock в той же горутине — взаимная блокировка. «Я читал, теперь хочу писать, не отпуская» так не делают: отпустили, взяли Lock, перепроверили данные.
Канал уместнее, когда передаёте задачу или единственное владение буфером: после send отправитель больше не трогает те же байты. Давление очереди получается само: медленный читатель тормозит писателя. Для счётчика метрик канал обычно хуже атомика или mutex.
Пример. Кэш конфига читают на каждый запрос, пишут раз в минуту — RWMutex. Очередь писем воркеру — канал. Счётчик RPS — atomic или mutex, не канал.
Типичная ошибка
Скопировать структуру с mutex (в том числе встроить mutex и отдать значение по ошибке). Unlock не в defer. RLock и затем Lock в одной горутине. И догма «только каналы»: для двух полей в одной структуре mutex проще и яснее.
На собеседовании
Нет единого победителя. Счётчик и кэш в структуре — mutex или atomic. Передача задачи воркеру — канал. RWMutex — много коротких чтений. Замок не рекурсивный, писатель не должен голодать.