Горутины и каналы
Горутина — это функция, запущенная в собственном потоке выполнения планировщиком Go, а не операционной системой напрямую. Создать её стоит go func(){ ... }(): несколько байт стека вместо мегабайта у ОС-потока, поэтому в проде нормально держать десятки тысяч горутин одновременно.
Канал — это типизированная очередь для обмена данными между горутинами вместо общей памяти под мьютексом. Небуферизированный канал make(chan int) синхронный: отправка блокируется, пока кто-то не примет значение. Буферизированный make(chan int, 3) блокируется только когда буфер полон.
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * j
}
}Здесь <-chan int и chan<- int — однонаправленные типы каналов: компилятор не даст воркеру случайно записать в jobs или прочитать из results. Это не рантайм-проверка, а статическая гарантия на уровне сигнатуры функции.
Утечка горутины через незакрытый канал
Самая частая причина утечки: горутина блокируется на отправке в канал, из которого никто больше не читает.
func leaky() <-chan int {
ch := make(chan int)
go func() {
ch <- expensiveCompute() // если получателя не будет — зависнет тут навсегда
}()
return ch
}
func main() {
leaky() // результат никто не читает — горутина живёт до конца программы
}Горутина из примера выше никогда не завершится и никогда не будет собрана GC — в отличие от обычных объектов, заблокированная горутина не мусор, планировщик считает её живой. При частом вызове leaky() в цикле это неограниченный рост числа горутин, которые видно в pprof как висящие на chan send.
Чинится это честным контекстом с отменой — вместо голой отправки в канал, select между отправкой и ctx.Done():
func leaky(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
select {
case ch <- expensiveCompute():
case <-ctx.Done():
}
}()
return ch
}Запусти сам и убедись: без WaitGroup программа почти наверняка завершится раньше, чем горутина успеет напечатать что-либо — main не ждёт.
Убери wg.Add(1)/wg.Wait() и посмотри, что выведется — скорее всего, ничего из горутин: main завершится первым.
select с default — неблокирующая проверка
select без default блокируется, пока не готов хотя бы один из кейсов — так реализуется мультиплексирование каналов. С default он превращается в неблокирующую проверку: если ни один канал не готов прямо сейчас, выполняется default и select не ждёт.
select {
case v := <-ch:
fmt.Println("получено:", v)
default:
fmt.Println("канал пуст, ждать не буду")
}nil-канал — не паника, а вечная блокировка
Чтение или запись в nil-канал (необъявленный, только var ch chan int) не паникует — она блокируется навсегда, потому что рантайм трактует "неинициализированный канал" как канал без единого готового к обмену конца. Это отличает каналы от, например, nil-мапы: чтение из nil-мапы работает и возвращает zero value, а вот запись в неё уже паникует. У каналов nil ведёт себя иначе, чем у обеих операций с мапой — и именно поэтому nil-канал иногда осознанно используют внутри select, чтобы навсегда выключить один из кейсов, не удаляя его из кода.