Так как Go стал для меня вторым основным языком после C++, стало очевидно, что надо понимать как он работает не только снаружи, но и изнутри. Я немного сомневался с чего начать, то-ли с горутин, то-ли с каналов. Приблизительно представляя как может быть реализовано и то и другое, первым и наиболее разумным кандидатом на пристальное изучение оказались каналы. Ну что сказать, интересно!
Реализация каналов вместе со всей остальной низкоуровневой частью лежит в src/runtime/chan.go, и довольно легко поддается анализу. Физически, канал представлен структурой hchan
, где наиболее интересно выглядят следующие моменты:
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
elemsize uint16
elemtype *_type
buf unsafe.Pointer // points to an array of dataqsiz elements
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
...
}
Как всем известно, канал в Go может работать как в синхронном, так и асинхронном (насколько хватает внутренней очереди) режиме. Очередь создается только один раз на этапе создания самого канала в функции makechan
и не может менять свой размер или тип позднее. Сама очередь представляет собой ни что иное как кусок памяти, куда/откуда через memmove
пишутся и читаются данные, размер которых хранится в elemsize
, а тип определяется через elemtype
. Просто, незатейливо и более чем ожидаемо при отсутствии обобщенного программирования в языке. С другой стороны, были бы конструкторы, деструкторы, наследование и прочее, то такой красоты уже особо б не вышло, а вышла бы метамагия с enable_if
.
У каждого канала имеется список ожидающих данные go-рутин recvq
, представляющий собой обычный двунаправленный список. Если на момент отправки сообщений несколько go-рутин ожидают сообщения, то из списка берется первая ожидающая (FIFO) go-рутина и ей отправляется сообщение без его копирования во внутренний буфер канала, а планировщик уведомляется о том, что необходимо запланировать выполнение выбранной для приема сообщения go-рутины. Если сообщения никто не ждет и в буфере еще есть свободное место, то элемент копируется. Ни места в буфере ни получателя сообщения? Ждите отправки, ваша go-рутина зарегистрирована в очереди sendq
, а планировщик уведомлен о том, что дальнейшее выполнение отправляющей go-рутины необходимо отложить.
Доставить одно сообщение всем слушателям невозможно и единственное событие которое отправляется всем подписчикам – это закрытие канала. На этом свойстве базируется довольно распространенный паттерн использования каналов, ожидание события несколькими подписчиками, когда закрытие канала и есть событие которое все ждут и получают оповещение разом (ну ладно, последовательно с точки зрения рантайма Go, но это уже мелкие детали).
Модуль runtime экспортирует ряд функций, таких как reflect_makechan
, reflect_chanclose
, reflect_chansend
и т.п., которые на этапе компиляции трансформируются в привычные ch <- data
вызовы. Более подробно про то, как создать каналы динамически и с ними работать стоит поглядеть тут, отличный пример работы с каналами напрямую, без синтаксического сахара. А вот наглядный пример трансформации кода на этапе компиляции:
select {
case v, ok = <-c:
... foo
default:
... bar
}
// as
if c != nil && selectnbrecv2(&v, &ok, c) {
... foo
} else {
... bar
}
Так что, все очень просто и при этом очень эффективно.