Как Go запускает тысячи конкурентных задач и как они передают друг другу данные. Внутри — схемы, которые можно трогать.
Листайте стрелками ← → на клавиатуре, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки.
Слово go перед вызовом запускает функцию и сразу возвращает управление. Вызывающий код не ждёт её завершения.
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
work(id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // ждём, пока все вызовут Done
}
Когда main возвращается, программа завершается вместе со всеми горутинами, даже недоделанными. Поэтому их нужно дождаться: через sync.WaitGroup, канал или errgroup.
GOMAXPROCS, по умолчанию по числу ядер. У каждого P своя очередь готовых горутин.Если у P кончилась работа, он берёт горутину из глобальной очереди или крадёт половину чужой очереди (work stealing). Нажмите «Системный вызов»: горутина уходит в ядро вместе со своим M, а P сразу получает другой M и продолжает работу.
ch := make(chan int)
go func() { ch <- 42 }() // отправка
v := <-ch // получение
Отправка блокирует горутину, пока кто-нибудь не придёт за значением. Получение блокирует, пока кто-нибудь не отправит.
Передача происходит, только когда встретились оба. Поэтому небуферизованный канал не только передаёт данные, но и синхронизирует горутины. Попробуйте нажимать кнопки в разном порядке.
Пока в буфере есть место, отправитель не ждёт. Буфер полон — отправитель засыпает. Буфер пуст — засыпает получатель.
Под схемой — настоящие поля структуры канала из рантайма Go. Буфер кольцевой: индексы sendx и recvx бегают по кругу. Каждое нажатие — новая горутина, которая выполняет операцию.
make(chan T) выделяет hchan в куче и возвращает указатель. Копия переменной канала указывает на тот же канал.lock. Канал — это очередь под мьютексом плюс тесная работа с планировщиком.sendq или recvq и снимается с P (gopark). Она не крутится в цикле и не тратит CPU.goready).
type hchan struct {
qcount uint // элементов в буфере
dataqsiz uint // cap(ch)
buf unsafe.Pointer // кольцевой буфер
elemsize uint16
closed uint32
elemtype *_type
sendx uint // куда писать
recvx uint // откуда читать
recvq waitq // спящие получатели
sendq waitq // спящие отправители
lock mutex
}
Что делает каждая операция в зависимости от состояния канала. Нажмите на клетку таблицы.
Закрывает канал отправитель, и только когда отправлять больше нечего. Получатель узнаёт о закрытии через v, ok := <-ch или когда завершается цикл for v := range ch.
select блокируется, пока хотя бы один case не сможет выполниться.default делает select неблокирующим, а time.After добавляет таймаут.Включайте и выключайте готовность каналов. Если запустить select, когда ничего не готово, а потом включить канал, select проснётся.
Несколько горутин читают задачи из одного канала. Каждую задачу получит ровно один воркер — раздачу работы делает сам канал.
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs { // до close(jobs)
results <- process(j)
}
}
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 0; w < n; w++ {
go worker(jobs, results)
}
for j := 1; j <= 12; j++ {
jobs <- j
}
close(jobs)
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}
ch <- 1 ждёт получателя, но получать должна та же самая горутина, строкой ниже. Других горутин нет, и рантайм видит, что все спят, поэтому падает с deadlock. Исправить можно буфером make(chan int, 1) или отправкой из отдельной горутины.
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Print(v)
}
range видит ok = false и завершает цикл. Паники при чтении из закрытого канала не бывает — паникует только отправка.
func first(urls []string) string {
ch := make(chan string)
for _, u := range urls {
go func() { ch <- fetch(u) }()
}
return <-ch // берём самый быстрый ответ
}
ch <- …: это утечка, и сборщик мусора их не уберёт. Исправление — буфер на все ответы: make(chan string, len(urls)), или отмена через context. Гонки на u нет: с Go 1.22 у каждой итерации своя переменная.
var ch chan int // nil
select {
case v := <-ch:
fmt.Println(v)
default:
fmt.Println("пусто")
}
default. Без default горутина заснула бы навсегда. Этим пользуются на практике: присвоить каналу nil — значит выключить его case в select.close — паника.case в select.select выбирает случайный из готовых case; default делает его неблокирующим.context.Context. Чтобы защитить общий счётчик, sync.Mutex проще канала.Не общайтесь через разделяемую память — разделяйте память, общаясь.Go Proverbs, Роб Пайк