Перейти до змісту

Канали

Канал (channel) — це типізований канал зв'язку, що дозволяє горутинам спілкуватися та синхронізуватися, передаючи значення. Одна горутина надсилає; інша отримує. Канал безпечно обробляє передачу, тож вам не потрібні блокування, щоб переміщати дані між горутинами.

Девіз Go: не спілкуйтеся, ділячись пам'яттю; діліться пам'яттю, спілкуючись. Надавайте перевагу передаванню значень каналом перед захистом спільних змінних м'ютексом.

Створіть канал через make, надсилайте через ch <- v, отримуйте через v := <-ch:

Поширене застосування — повернення результату з горутини: канал і доставляє значення, і синхронізує, тож спільна змінна не потрібна:

total := make(chan int)
go func() {
    sum := 0
    for _, n := range []int{1, 2, 3, 4, 5} {
        sum += n
    }
    total <- sum        // надіслати результат назад у main
}()
fmt.Println(<-total)    // output: 15

Небуферизовані канали синхронізують

Канал вище небуферизований: надсилання блокується, доки інша горутина не буде готова отримати, і навпаки. Обмін — це рандеву: обидві сторони зустрічаються в одну мить. Це робить небуферизований канал інструментом синхронізації, а не просто трубою: отримання не може завершитися раніше за надсилання.

done := make(chan struct{})
go func() {
    fmt.Println("working")
    done <- struct{}{}      // сигнал про завершення
}()
<-done                       // блокує, доки горутина не подасть сигнал
fmt.Println("finished")
// output:
// working
// finished

chan struct{} — це ідіоматичний канал «лише сигнал, без даних».

Буферизовані канали тримають значення

make(chan T, n) дає каналу буфер на n. Надсилання блокується лише коли буфер повний; отримання блокується лише коли він порожній. Це розчіплює відправника й отримувача під час сплесків.

jobs := make(chan string, 2)   // невелика черга завдань
jobs <- "email #1"             // не блокує — у буфері є місце
jobs <- "email #2"             // не блокує — тепер повний
fmt.Println(len(jobs), cap(jobs))   // output: 2 2
fmt.Println(<-jobs, <-jobs)          // output: email #1 email #2

len — це скільки значень буферизовано просто зараз; cap — розмір буфера. Використовуйте буфер, коли свідомо хочете запас; за замовчуванням беріть небуферизований, бо він безкоштовно дає синхронізацію.

Закриття каналу

close(ch) позначає, що більше значень не надсилатимуть. Отримувачі ще можуть вичерпати те, що буферизовано, а потім отримають нульове значення. Отримання у формі comma-ok відрізняє справжнє значення від «закрито й порожньо»:

ch := make(chan int, 2)
ch <- 10
close(ch)

v, ok := <-ch
fmt.Println(v, ok)   // output: 10 true   — справжнє значення
v, ok = <-ch
fmt.Println(v, ok)   // output: 0 false   — закрито й вичерпано

Правила: закривати має лише відправник, і лише раз. Надсилання в закритий канал панікує; закриття вже закритого каналу панікує. Закриття — це широкомовлення: його бачить кожен отримувач.

Перебір каналу через range

for v := range ch отримує значення, доки канал не буде закрито й вичерпано, а тоді завершує цикл. Це чистий спосіб споживати потік:

greetings := make(chan string)

go func() {                          // виробник
    for _, name := range []string{"alice", "bob", "carol"} {
        greetings <- "hello, " + name
    }
    close(greetings)                 // більше значень немає; без цього range блокується назавжди
}()

for g := range greetings {           // споживач — завершується, коли канал закрито
    fmt.Println(g)
}
// output:
// hello, alice
// hello, bob
// hello, carol

Напрям каналу в сигнатурах

Параметр функції може обмежити канал до лише надсилання (chan<- T) чи лише отримання (<-chan T). Це документує намір і дозволяє компілятору зупиняти неправильне використання.

func produce(out chan<- int) { out <- 42; close(out) }   // лише надсилання
func consume(in <-chan int)  { fmt.Println(<-in) }        // лише отримання

ch := make(chan int, 1)
produce(ch)
consume(ch)            // output: 42

Взаємні блокування та nil-канали

Якщо кожна горутина заблокована в очікуванні каналу, середовище це виявляє й аварійно завершується:

func main() {
    ch := make(chan int)
    <-ch        // ніхто ніколи не надішле
}
// fatal error: all goroutines are asleep - deadlock!

nil-канал (для якого не викликали make) блокується назавжди і на надсиланні, і на отриманні — інколи корисно в select, щоб вимкнути випадок, але інакше це баг.

Швидка довідка

Операція Значення
make(chan T) небуферизований — рандеву надсилання/отримання
make(chan T, n) буферизований — блокує лише коли повний/порожній
ch <- v / v := <-ch надіслати / отримати
v, ok := <-ch ok стає false, коли закрито й вичерпано
close(ch) більше жодних надсилань; лише відправник, раз
for v := range ch отримувати, доки не закрито
chan<- T / <-chan T параметр лише-надсилання / лише-отримання
надсилання в закритий канал паніка
усі горутини заблоковані fatal: deadlock

Джерела