Перейти к основному содержимому

Каналы

Для защиты общей памяти используется 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.
  • Не закрывайте канал со стороны получателя — это всегда должен делать единственный отправитель!