Горутины и каналы
Горутины — лёгкие потоки Go, а каналы — безопасный способ передавать между ними данные.
Горутина — функция, выполняемая конкурентно. Запустить её — это просто слово
goперед вызовом. Канал — типизированная труба для безопасного обмена данными между горутинами.
Горутина в одно слово
Горутины — не операционные потоки, а лёгкие задачи, которыми управляет рантайм Go. Их можно запускать тысячами: каждая стоит лишь несколько килобайт. Запуск — ключевое слово go перед вызовом функции.
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Printf("воркер %d работает\n", id)
}
func main() {
for i := 1; i <= 3; i++ {
go worker(i) // запуск конкурентно
}
time.Sleep(100 * time.Millisecond) // ждём, иначе main завершится раньше
fmt.Println("готово")
}Здесь есть проблема: main может завершиться раньше горутин, и тогда мы ничего не увидим. time.Sleep — костыль для демонстрации. Правильный способ синхронизации — каналы и WaitGroup (следующий урок).
Каналы: труба для данных
Канал создают через make(chan T). Отправка — ch <- v, приём — v := <-ch. Стрелка всегда «показывает» направление потока данных. Ключевая особенность: приём из небуферизованного канала блокируется, пока кто-то не отправит, и наоборот. Это и есть синхронизация.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "привет из горутины" // отправка
}()
msg := <-ch // приём: main ждёт здесь, пока не придёт значение
fmt.Println(msg)
}Вывод:
привет из горутины
Заметьте: здесь нет time.Sleep. Приём <-ch сам дожидается отправки. Канал одновременно передаёт данные и синхронизирует горутины — в этом его красота.
Девиз Go про конкурентность
«Не общайтесь, разделяя память; разделяйте память, общаясь.»
То есть вместо общих переменных под замками горутины обмениваются сообщениями через каналы. Эта модель (CSP) заметно снижает число гонок данных.
Буферизованные каналы
Если задать каналу ёмкость — make(chan int, 3) — он становится буферизованным. Отправка не блокируется, пока в буфере есть место. Канал закрывают через close, после чего его можно дочитать через for range.
package main
import "fmt"
func main() {
ch := make(chan int, 3) // буфер на 3
ch <- 1
ch <- 2
ch <- 3
close(ch) // закрываем: больше не отправить
for v := range ch { // читаем, пока канал не опустеет
fmt.Println(v)
}
}Вывод:
1 2 3
| Операция | Синтаксис |
| Создать канал | make(chan T) |
| Отправить | ch <- v |
| Принять | v := <-ch |
| Закрыть | close(ch) |
Итог
- Горутина запускается словом
go; их можно создавать тысячами. - Канал передаёт данные и синхронизирует: приём из небуферизованного канала ждёт отправки.
- Идиома Go — общаться через каналы, а не через общую память под замками.