Как устроены каналы в Go под капотом
Что проверяют
Знаете ли вы, что канал это обычная структура в рантайме с мьютексом внутри, а не магия языка. Следом почти всегда спрашивают, что будет при записи в закрытый канал и чем канал дороже sync.Mutex на горячем пути.
Как отвечать
make(chan T, n) вызывает runtime.makechan, который выделяет в куче структуру hchan. Главные поля:
buf,dataqsiz,qcount: кольцевой буфер наnэлементов, его размер и текущее заполнение. У небуферизированного канала буфера нет;sendx,recvx: индексы записи и чтения в кольце;sendq,recvq: очереди ждущих горутин, каждая завёрнута вsudogс указателем на значение;lock: мьютекс рантайма, любая операция с каналом берёт его;closed: флаг закрытия.
Отправка идёт по трём веткам. Если в recvq есть ждущий получатель, значение копируется прямо в его стек, минуя буфер, и горутина будится. Если в буфере есть место, значение кладётся в buf[sendx]. Иначе отправитель создаёт sudog, встаёт в sendq и паркуется через gopark, а поток ОС уходит выполнять другие горутины. Чтение симметрично. При полном буфере и ждущем отправителе получатель забирает голову буфера, а значение отправителя переносит в хвост, так порядок FIFO сохраняется.
close ставит флаг и будит всех: получатели дочитывают остаток буфера, потом получают нулевое значение и ok == false, отправители падают в панику. select перебирает ветки в случайном порядке и блокирует все участвующие каналы в порядке их адресов, чтобы не словить взаимную блокировку внутри рантайма.
Где спотыкаются
Путают nil-канал и закрытый. Запись и чтение из nil блокируют навсегда, запись в закрытый канал и повторный close дают панику, а чтение из закрытого сразу возвращает нулевое значение.
Считают, что канал защищает данные. Он копирует значение, но если передан указатель, слайс или мапа, обе горутины работают с одной памятью, и гонка остаётся.
Используют канал там, где хватит мьютекса или sync/atomic. Каждая операция берёт лок и может переключить горутину, для простого счётчика это заметно дороже.