Модуль
1Горутины: основы← вы здесь2Каналы3select и таймауты4sync: Mutex, Once, atomic
Урок 1~12 минут

Горутины: основы

Горутина — это не поток

В других языках конкурентность строится на потоках ОС. В Go — на горутинах. Разница принципиальная:

Поток ОСГорутина
Начальный стек~2 MB~2 KB
Создание~1 мс~1 мкс
Переключениеядро ОСпланировщик Go
Типичное кол-восотнисотни тысяч

Горутины — это зелёные потоки, которые Go-планировщик мультиплексирует на несколько OS-потоков. Модель называется M:N — M горутин на N потоков.


Запуск горутины: ключевое слово go

Добавь go перед вызовом функции — она запустится в отдельной горутине:

go
package main
 
import (
    "fmt"
    "time"
)
 
func say(s string) {
    for i := 0; i < 3; i++ {
        fmt.Println(s)
        time.Sleep(100 * time.Millisecond)
    }
}
 
func main() {
    go say("горутина")  // запускается конкурентно
    say("main")         // выполняется в main-горутине
}

Важно: если main завершается — все горутины убиваются, даже если не закончили работу. Вот почему здесь say("main") выступает как "заглушка" — она держит программу живой.

Анонимная горутина:

go
go func(n int) {
    fmt.Println("горутина получила:", n)
}(42)  // аргументы передаём сразу

Проблема: гонка завершения

go
func main() {
    go fmt.Println("привет из горутины")
    // main завершается — горутина может не успеть напечатать
}

Вывод непредсказуем — иногда строка печатается, иногда нет. Нужна синхронизация.


sync.WaitGroup: ждём горутины

WaitGroup — счётчик: добавляем горутины, каждая сигнализирует о завершении, Wait блокирует пока счётчик не обнулится:

go
package main
 
import (
    "fmt"
    "sync"
)
 
func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // уменьшает счётчик при выходе
    fmt.Printf("worker %d начал работу\n", id)
    // ... работа ...
    fmt.Printf("worker %d завершил\n", id)
}
 
func main() {
    var wg sync.WaitGroup
 
    for i := 1; i <= 5; i++ {
        wg.Add(1)          // увеличиваем счётчик перед запуском
        go worker(i, &wg)  // передаём указатель!
    }
 
    wg.Wait()  // ждём всех
    fmt.Println("все workers завершены")
}

Три правила WaitGroup:

  1. Add(n) вызывать до запуска горутины, не внутри неё
  2. Передавать только по указателю (*sync.WaitGroup)
  3. Done() всегда через defer — чтобы не забыть при панике

Планировщик Go: модель G-M-P

Понимание планировщика помогает писать эффективный конкурентный код.

G — Goroutine    (горутина, единица работы)
M — Machine      (OS-поток, реальный поток)
P — Processor    (логический процессор, очередь задач)

Каждый P имеет локальную очередь горутин (G). Каждый M привязан к одному P и выполняет горутины из его очереди. Количество P = GOMAXPROCS (по умолчанию = число CPU).

P1 (очередь: G3, G5)     P2 (очередь: G4, G6)
    |                          |
    M1 (выполняет G1)          M2 (выполняет G2)

Work stealing: если очередь P1 пуста, он "крадёт" горутины из очереди другого P. Это обеспечивает равномерную нагрузку без ручного управления.


GOMAXPROCS: параллелизм

go
import "runtime"
 
func main() {
    // сколько CPU доступно
    fmt.Println(runtime.NumCPU())        // например, 8
 
    // сколько P используется сейчас
    fmt.Println(runtime.GOMAXPROCS(0))   // 0 = читать, не менять
 
    // установить вручную
    runtime.GOMAXPROCS(4)
}

Или через переменную окружения:

bash
GOMAXPROCS=1 go run main.go  # однопоточный режим — полезно для отладки гонок

Типичный паттерн: пул workers

go
func main() {
    jobs := make(chan int, 100)
    var wg sync.WaitGroup
 
    // запускаем 3 worker-горутины
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := range jobs {  // читаем из канала пока он открыт
                fmt.Printf("worker %d обрабатывает задачу %d\n", id, j)
            }
        }(w)
    }
 
    // отправляем 9 задач
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)  // сигнал: задач больше нет
 
    wg.Wait()
}

Каналы (chan) — главный способ общения между горутинами. О них — в следующем уроке.


Горутины и замыкания: классическая ловушка

go
// Плохо: все горутины захватывают одну переменную i
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // скорее всего выведет 3 3 3
    }()
}
 
// Хорошо: передаём значение как аргумент
for i := 0; i < 3; i++ {
    go func(n int) {
        fmt.Println(n)  // 0 1 2 (в любом порядке)
    }(i)
}
 
// Или в Go 1.22+: переменная цикла создаётся заново на каждой итерации
for i := range 3 {
    go func() {
        fmt.Println(i)  // безопасно с Go 1.22
    }()
}
Горутина — не поток ОС. Планировщик Go мультиплексирует тысячи горутин на несколько OS-потоков модели M:N. Начальный стек ~2KB против ~2MB у потока.
GoroutinesGo runtime scheduler
main goroutineRUNNINGpid: 1
0 running  0 done  8 slots free
-- нет горутин, нажми "go func()" --
package main import "fmt" func main() { go func() { // запуск горутины fmt.Println("goroutine работает") }() // немедленный вызов // main не ждёт горутину автоматически // используй sync.WaitGroup или каналы }
🎯
Миссия 1 из 4
Сколько байт занимает начальный стек горутины?