Каналы
Каналы: общение через передачу данных
Философия Go: «не общайся через разделяемую память — разделяй память через общение». Каналы — это типизированные трубы для передачи данных между горутинами.
ch := make(chan int) // небуферизованный канал int
ch <- 42 // отправка (блокирует до получателя)
val := <-ch // получение (блокирует до отправителя)
val, ok := <-ch // получение с проверкой: ok=false если канал закрытТип канала: chan T. Оператор <- указывает направление потока данных.
Небуферизованные каналы: точка синхронизации
Небуферизованный канал (make(chan T)) — это рандеву: оба участника должны быть готовы одновременно:
package main
import "fmt"
func sum(s []int, ch chan int) {
total := 0
for _, v := range s {
total += v
}
ch <- total // отправляем результат
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
ch := make(chan int)
go sum(nums[:5], ch) // считает 1+2+3+4+5 = 15
go sum(nums[5:], ch) // считает 6+7+8+9+10 = 40
a, b := <-ch, <-ch // получаем оба результата
fmt.Println(a + b) // 55
}main блокируется на <-ch пока горутина не отправит результат — это встроенная синхронизация без WaitGroup.
Буферизованные каналы: асинхронная очередь
Буфер позволяет отправить несколько значений без получателя:
ch := make(chan int, 3) // буфер на 3 элемента
ch <- 1 // не блокирует
ch <- 2 // не блокирует
ch <- 3 // не блокирует
ch <- 4 // БЛОКИРУЕТ — буфер полон
fmt.Println(len(ch)) // 3 — элементов в буфере
fmt.Println(cap(ch)) // 3 — ёмкость буфераБуферизованный канал — это FIFO очередь. Отправитель блокируется только когда буфер заполнен, получатель — только когда буфер пуст.
// Типичный use case: ограничитель конкурентности
sem := make(chan struct{}, 5) // не более 5 горутин одновременно
for _, url := range urls {
sem <- struct{}{} // занять слот
go func(u string) {
defer func() { <-sem }() // освободить слот
fetch(u)
}(url)
}Закрытие канала и range
Закрытый канал сигнализирует получателям: «данных больше не будет»:
func generate(n int) chan int {
ch := make(chan int)
go func() {
for i := 0; i < n; i++ {
ch <- i
}
close(ch) // закрываем — получатели узнают об окончании
}()
return ch
}
func main() {
for v := range generate(5) { // range завершится после close
fmt.Println(v) // 0 1 2 3 4
}
}Правила закрытия:
- Закрывает только отправитель, никогда получатель
- Закрывать можно только один раз (повторное close → panic)
- Отправка в закрытый канал → panic
- Чтение из закрытого канала → оставшиеся данные, потом нулевое значение +
ok=false
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
v1, ok1 := <-ch // 10, true
v2, ok2 := <-ch // 20, true
v3, ok3 := <-ch // 0, false — канал закрыт и пустНаправленные каналы
Можно ограничить канал только на отправку или только на получение:
chan<- int // только отправка (write-only)
<-chan int // только получение (read-only)
chan int // двунаправленныйЭто позволяет выражать намерения в сигнатурах функций и ловить ошибки на этапе компиляции:
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i
}
close(out)
// <-out // ошибка компиляции: receive from send-only channel
}
func consumer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
// in <- 1 // ошибка компиляции: send to receive-only channel
}
func main() {
ch := make(chan int, 5)
go producer(ch) // chan int неявно конвертируется в chan<- int
consumer(ch) // и в <-chan int
}Паттерн Pipeline
Каналы отлично подходят для построения конвейеров обработки данных:
// Этап 1: генератор чисел
func numbers(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Этап 2: возводим в квадрат
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Соединяем этапы
nums := numbers(2, 3, 4, 5)
squares := square(nums)
for v := range squares {
fmt.Println(v) // 4 9 16 25
}
}Каждый этап — независимая горутина. Данные текут по каналам. Это идиоматичный Go.
Паттерн Fan-out / Fan-in
Fan-out — один источник, много потребителей:
func fanOut(in <-chan int, n int) []<-chan int {
outs := make([]<-chan int, n)
for i := range outs {
outs[i] = square(in) // несколько worker'ов на один вход
}
return outs
}Fan-in — много источников, один потребитель:
func merge(channels ...<-chan int) <-chan int {
var wg sync.WaitGroup
merged := make(chan int)
forward := func(ch <-chan int) {
defer wg.Done()
for v := range ch {
merged <- v
}
}
wg.Add(len(channels))
for _, ch := range channels {
go forward(ch)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}Эти паттерны — основа построения эффективных конкурентных систем в Go.