Каналы
Для защиты общей памяти используется WaitGroup и Mutex. Но в Go есть более элегантный способ: каналы (chan). Вместо того чтобы «драться» за одну переменную, горутины передают данные друг другу - это делает код чище и избавляет от многих проблем с блокировками.
1. Что такое канал?
Канал — это типизированная «труба», по которой горутины могут пересылать данные.
- Типизация: В канал
chan intнельзя отправить строку. - Синхронность: По умолчанию каналы блокирующие. Если горутина А пытается отправить данные в канал, она остановится и будет ждать, пока горутина Б их не заберет (и наоборот).
func main() {
// Создаем канал для целых чисел
ch := make(chan int)
go func() {
fmt.Println("Горутина: отправляю данные...")
ch <- 42 // Блокировка здесь, пока кто-то не заберет
}()
val := <-ch // Блокировка здесь, пока кто-то не отправит
fmt.Println("Main: получил", val)
}
2. Буферизированные каналы
Если вы хотите, чтобы горутина не ждала получателя, можно создать буферизированный канал. Он имеет «емкость» (буфер). Отправка блокируется только тогда, когда буфер полностью заполнен.
// Канал на 2 элемента. Отправитель не будет ждать, пока получатель освободит место.
ch := make(chan int, 2)
ch <- 1
ch <- 2
// ch <- 3 // Тут будет блокировка, так как буфер полон (size 2)
3. Паттерн «Worker Pool» (Пул рабочих)
Это классический способ организации работы в Go. У вас есть «задача», которую должны выполнить несколько «рабочих».
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs { // Читаем из канала, пока он не закроется
fmt.Printf("Рабочий %d выполняет задачу %d\n", id, j)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// Запускаем 3 рабочих
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Раздаем 5 задач
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // Важно: закрываем канал, чтобы рабочие узнали, что задач больше нет
// Собираем результаты
for a := 1; a <= 5; a++ {
fmt.Println("Результат:", <-results)
}
}
4. Паттерн «Fan-in» (Сборщик)
Представьте, что у вас есть несколько источников данных, и вы хотите слить всё в один поток.
func producer(msg string, ch chan<- string) {
ch <- msg
}
func main() {
ch := make(chan string)
go producer("Привет из первой горутины", ch)
go producer("Привет из второй горутины", ch)
// Читаем из канала дважды
fmt.Println(<-ch)
fmt.Println(<-ch)
}
5. Выбор с select
Что делать, если нужно слушать сразу несколько каналов или добавить таймаут? Используйте select. Это как switch, но для операций с каналами.
select {
case msg1 := <-ch1:
fmt.Println("Данные из 1:", msg1)
case msg2 := <-ch2:
fmt.Println("Данные из 2:", msg2)
case <-time.After(1 * time.Second):
fmt.Println("Таймаут: никто не прислал данные за 1 секунду")
}
Итоговая шпаргалка по каналам
| Операция | Синтаксис | Что произойдет? |
|---|---|---|
| Отправка | ch <- val | Блокируется, пока кто-то не прочитает (или пока не освободится место в буфере). |
| Чтение | val := <-ch | Блокируется, пока кто-то не отправит данные. |
| Закрытие | close(ch) | Отправитель больше не может писать. Читатели получают данные, пока буфер не опустеет. |
| Итерация | for v := range ch | Читает данные, пока канал не будет закрыт. |
Золотое правило:
- Если данные передаются от одного потока к другому — используйте каналы.
- Если данные — это общие настройки или счетчик, к которому часто обращаются — используйте
Mutex. - Не закрывайте канал со стороны получателя — это всегда должен делать единственный отправитель!