Горутины и каналы

Горутины — лёгкие потоки 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 &lt;- "привет из горутины" // отправка
    }()

    msg := &lt;-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 &lt;- 1
    ch &lt;- 2
    ch &lt;- 3
    close(ch) // закрываем: больше не отправить

    for v := range ch { // читаем, пока канал не опустеет
        fmt.Println(v)
    }
}

Вывод:

1
2
3
ОперацияСинтаксис
Создать каналmake(chan T)
Отправитьch <- v
Принятьv := <-ch
Закрытьclose(ch)

Итог

  • Горутина запускается словом go; их можно создавать тысячами.
  • Канал передаёт данные и синхронизирует: приём из небуферизованного канала ждёт отправки.
  • Идиома Go — общаться через каналы, а не через общую память под замками.
Проверьте себя
1. Как запустить функцию в отдельной горутине?
Athread(f())
Bgo f()
Casync f()
Dspawn f()
2. Что происходит при приёме из небуферизованного канала, если никто не отправил?
AВозвращается nil
BГорутина блокируется и ждёт отправки
CВозникает паника
DВозвращается ноль типа
3. Как звучит девиз Go про конкурентность?
AБлокируй всё мьютексами
BНе общайтесь, разделяя память; разделяйте память, общаясь
CИспользуй как можно больше потоков
DИзбегай каналов
Поддержать проект