# Go: Goroutines and Concurrent Programming

Эта статья представляет собой введение в конкурентное программирование на Go через практические примеры. Давайте сразу перейдем к написанию конкурентной программы!

---

## Goroutines

Предположим, у нас есть функция, которая произносит фразу слово за словом с паузами:

```go
// say выводит каждое слово фразы.
func say(phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("Simon says: %s...\n", word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
}
```

Вызовем её из функции main:

```go
func main() {
    say("go is awesome")
}
```

Теперь создадим двух говорящих, каждый со своей фразой:

```go
// say выводит каждое слово фразы.
func say(id int, phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("Worker #%d says: %s...\n", id, word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
}
```

Запустим программу:

```go
func main() {
    say(1, "go is awesome")
    say(2, "cats are cute")
}
```

Функции выполняются последовательно. Чтобы они говорили одновременно, добавим `go` перед вызовом функции `say()`:

```go
func main() {
    go say(1, "go is awesome")
    go say(2, "cats are cute")
    time.Sleep(500 * time.Millisecond)
}
```

Теперь они действительно соревнуются за наше внимание! Когда мы пишем `go f()`, функция `f()` выполняется независимо от остальных.

Если вы знакомы с конкурентностью в Python, JavaScript или других языках с async/await, не пытайтесь применить этот опыт к Go. Go использует совершенно другой подход к конкурентности. Попробуйте взглянуть на это свежим взглядом.

Функции, запускаемые с `go`, называются **goroutines**. Среда выполнения Go управляет этими goroutines и распределяет их между потоками операционной системы, работающими на ядрах CPU. По сравнению с потоками ОС, goroutines легковесны, поэтому вы можете создавать сотни или тысячи их.

Вы можете задаться вопросом, зачем нам нужен `time.Sleep()` в функции main. Давайте это проясним.

---

## Зависимые и независимые goroutines

Goroutines полностью независимы. Когда мы вызываем `go say()`, функция выполняется сама по себе. `main` не ждёт её завершения. Поэтому если мы напишем main так:

```go
func main() {
    go say(1, "go is awesome")
    go say(2, "cats are cute")
}
```

— программа ничего не выведет. `main` завершается раньше, чем наши goroutines успевают что-то сказать, и поскольку main завершена, вся программа завершается.

Функция `main` также является goroutine, но она запускается неявно при старте программы. Таким образом, у нас есть три goroutines: `main`, `say(1)` и `say(2)`, все они независимы. Единственная особенность в том, что когда `main` завершается, всё остальное тоже завершается.

### WaitGroup

Использование `time.Sleep()` для ожидания goroutines — плохая идея, потому что мы не можем предсказать, сколько времени они займут. Лучший подход — использовать wait group:

```go
func main() {
    var wg sync.WaitGroup // (1)

    wg.Add(1)             // (2)
    go say(&wg, 1, "go is awesome")

    wg.Add(1)             // (2)
    go say(&wg, 2, "cats are cute")

    wg.Wait()             // (3)
}

// say выводит каждое слово фразы.
func say(wg *sync.WaitGroup, id int, phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("Worker #%d says: %s...\n", id, word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
    wg.Done()             // (4)
}
```

`wg` ➊ имеет внутри счётчик. Вызов `wg.Add(1)` ➋ увеличивает его на единицу, а `wg.Done()` ➍ уменьшает. `wg.Wait()` ➌ блокирует goroutine (в данном случае `main`) до тех пор, пока счётчик не достигнет нуля. Таким образом, `main` ждёт завершения `say(1)` и `say(2)` перед выходом.

Однако этот подход смешивает бизнес-логику (`say`) с логикой конкурентности (`wg`). В результате мы не можем легко запустить `say` в обычном, неконкурентном коде.

В Go принято разделять логику конкурентности и бизнес-логику. Обычно это делается с помощью отдельных функций. В простых случаях, как у нас, подойдут даже анонимные функции:

```go
func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        say(1, "go is awesome")
    }()

    go func() {
        defer wg.Done()
        say(2, "cats are cute")
    }()

    wg.Wait()
}
```

Вот как это работает:

* Мы знаем, что будет две goroutines, поэтому сразу вызываем `wg.Add(2)`. Альтернативно, мы можем вызывать `wg.Add(1)` перед запуском каждой goroutine — результат будет тот же.
    
* Анонимные функции запускаются с `go` так же, как и обычные.
    
* `defer wg.Done()` гарантирует, что goroutine уменьшит счётчик перед выходом, даже если `say` вызовет панику.
    
* Сама `say` ничего не знает о конкурентности и просто работает.
    

### WaitGroup.Go

Метод `WaitGroup.Go` (Go 1.25+) автоматически увеличивает счётчик wait group, запускает функцию в goroutine и уменьшает счётчик, когда она завершается. Это означает, что мы можем переписать пример выше без использования `wg.Add()` и `wg.Done()`:

```go
func main() {
    var wg sync.WaitGroup

    wg.Go(func() {
        fmt.Println("go is awesome")
    })

    wg.Go(func() {
        fmt.Println("cats are cute")
    })

    wg.Wait()
    fmt.Println("done")
}
```

Реализация использует `Add` и `Done` так же, как мы делали раньше:

```go
// https://github.com/golang/go/blob/master/src/sync/waitgroup.go
func (wg *WaitGroup) Go(f func()) {
    wg.Add(1)
    go func() {
        defer wg.Done()
        f()
    }()
}
```

---

## Channels

Запуск множества goroutines — это здорово, но как они обмениваются данными? В Go goroutines могут передавать значения друг другу через **channels**. Канал — это как окно, через которое одна goroutine может что-то бросить, а другая — поймать.

```plaintext
┌─────────────┐    ┌─────────────┐
│ goroutine A │    │ goroutine B │
│             └────┘             │
│        X <-  chan  <- X        │
│             ┌────┐             │
│             │    │             │
└─────────────┘    └─────────────┘
```

Goroutine B отправляет значение X в goroutine A.

Вот как это работает:

```go
func main() {
    // Чтобы создать канал, используйте `make(chan type)`.
    // Канал может принимать только значения указанного типа:
    messages := make(chan string)

    // Чтобы отправить значение в канал,
    // используйте синтаксис `channel <-`.
    // Отправим "ping":
    go func() { messages <- "ping" }()

    // Чтобы получить значение из канала,
    // используйте синтаксис `<-channel`.
    // Получим "ping" и выведем его:
    msg := <-messages
    fmt.Println(msg)
}
```

Когда программа выполняется, первая goroutine (анонимная) отправляет сообщение второй (`main`) через канал `messages`.

Отправка значения через канал — это синхронная операция. Когда отправляющая goroutine записывает значение в канал (`messages <- "ping"`), она блокируется и ждёт, пока кто-то получит это значение (`<-messages`). Только после этого она продолжает работу:

```go
func main() {
    messages := make(chan string)

    go func() {
        fmt.Println("B: Sending message...")
        messages <- "ping"                    // (1)
        fmt.Println("B: Message sent!")       // (2)
    }()

    fmt.Println("A: Doing some work...")
    time.Sleep(500 * time.Millisecond)
    fmt.Println("A: Ready to receive a message...")

    <-messages                               //  (3)

    fmt.Println("A: Message received!")
    time.Sleep(100 * time.Millisecond)
}
```

После отправки сообщения в канал ➊ goroutine B блокируется. Только когда goroutine A получает сообщение ➌, goroutine B продолжает и выводит "message sent" ➋.

Таким образом, каналы не только передают данные, но и помогают синхронизировать независимые goroutines. Это пригодится позже.

---

## Паттерн Producer-Consumer

В программировании часто встречается паттерн "производитель-потребитель":

* **Производитель** поставляет данные.
    
* **Потребитель** получает и обрабатывает их.
    

В следующих примерах мы исследуем, как производители и потребители могут взаимодействовать через каналы.

Работаем с функцией, которая считает цифры в словах:

```go
// counter хранит количество цифр в каждом слове.
// Ключ — слово, значение — количество цифр.
type counter map[string]int

// countDigitsInWords считает количество цифр
// в словах фразы.
func countDigitsInWords(phrase string) counter {
    words := strings.Fields(phrase)
    // ...
    return stats
}
```

### Результатный канал

Сделаем следующее:

1. Запустим goroutine.
    
2. В этой goroutine пройдёмся по словам, посчитаем цифры в каждом и запишем в канал `counted` (производитель).
    
3. Во внешней функции читаем значения из канала и заполняем счётчик `stats` (потребитель).
    

```go
func countDigitsInWords(phrase string) counter {
    words := strings.Fields(phrase)
    counted := make(chan int)

    go func() {
        // Проходим по словам,
        // считаем количество цифр в каждом,
        // и записываем в канал counted.
    }()

    // Читаем значения из канала counted
    // и заполняем stats.

    return stats
}
```

---

## Генератор

До сих пор мы предполагали, что функция `countDigitsInWords` заранее знает все слова.

Но в реальной жизни мы не можем рассчитывать на такую роскошь. Данные могут приходить из базы данных или по сети, и функция не знает, сколько слов будет.

Давайте смоделируем эту ситуацию, передав функцию-генератор `next` вместо фразы. Каждый вызов `next()` даёт нам следующее слово из источника. Когда слов больше нет, она возвращает пустую строку.

Последовательная программа выглядела бы так:

```go
// counter хранит количество цифр в каждом слове.
// Ключ — слово, значение — количество цифр.
type counter map[string]int

// countDigitsInWords считает количество цифр в словах,
// получая следующее слово через next().
func countDigitsInWords(next func() string) counter {
    stats := counter{}

    for {
        word := next()
        if word == "" {
            break
        }
        count := countDigits(word)
        stats[word] = count
    }

    return stats
}

func main() {
    phrase := "0ne 1wo thr33 4068"
    next := wordGenerator(phrase)
    stats := countDigitsInWords(next)
    printStats(stats)
}
```

Теперь добавим конкурентность.

### Генератор с goroutines

Если вы попытаетесь решить упражнение как предыдущее, столкнётесь с парой проблем:

```go
func countDigitsInWords(next func() string) counter {
    counted := make(chan int)

    // считаем цифры в словах
    go func() {
        for {
            // должна вернуться, когда
            // слов больше нет
            word := next()
            count := countDigits(word)
            counted <- count
        }
    }()

    // заполняем stats словами
    stats := counter{}
    for {
        count := <-counted
        // как выйти из цикла,
        // когда слов больше нет?
        if ... {
            break
        }
        // откуда должно браться слово?
        stats[...] = count
    }

    return stats
}
```

Подумайте, что отправлять в канал `counted`, чтобы решить обе проблемы. Обратите внимание на тип пары.

---

## Reader и Worker

Для более сложных задач полезно иметь goroutine для чтения данных (reader) и другую для обработки данных (worker). Используем этот подход в нашей функции:

```plaintext
┌───────────────┐               ┌───────────────┐
│ отправляет    │               │ считает цифры │               ┌────────────────┐
│ слова для     │ → (pending) → │ в словах     │ → (counted) → │ заполняет stats│
│ подсчёта      │               │              │               └────────────────┘
└───────────────┘               └───────────────┘
  reader           канал          worker            канал        внешняя функция
```

Сделаем следующее:

1. Запустим goroutine, которая получает слова из генератора и отправляет их в канал `pending` (reader).
    
2. Запустим вторую goroutine, которая читает из `pending`, считает цифры и записывает в канал `counted` (worker).
    
3. Во внешней функции читаем из `counted` и обновляем итоговый счётчик `stats`.
    

```go
func countDigitsInWords(next func() string) counter {
    pending := make(chan string)
    counted := make(chan pair)

    // отправляет слова для подсчёта
    go func() {
        // Получаем слова из генератора
        // и отправляем их в канал pending.
    }()

    // считает цифры в словах
    go func() {
        // Читаем слова из канала pending,
        // считаем количество цифр в каждом слове,
        // и отправляем результаты в канал counted.
    }()

    // Читаем значения из канала counted
    // и заполняем stats.

    return stats
}
```

---

## Именованные goroutines

После разделения логики между reader и worker функция стала довольно объёмной:

```go
func countDigitsInWords(next func() string) counter {
    // ...

    // отправляет слова для подсчёта
    go func() {
        // ...
    }()

    // считает цифры в словах
    go func() {
        // ...
    }()

    // заполняет stats
    // ...

    return stats
}
```

Чётко видны три логических блока:

1. Отправка слов для подсчёта.
    
2. Подсчёт цифр в словах.
    
3. Заполнение итоговых результатов.
    

Было бы удобно выделить эти блоки в отдельные функции, которые обмениваются данными через каналы:

```go
func countDigitsInWords(next func() string) counter {
    pending := make(chan string)
    go submitWords(next, pending)

    counted := make(chan pair)
    go countWords(pending, counted)

    return fillStats(counted)
}
```

---

## Выходной канал

Вот функция, к которой мы пришли:

```go
// countDigitsInWords считает количество цифр в словах,
// получая следующее слово через next().
func countDigitsInWords(next func() string) counter {
    pending := make(chan string)
    go submitWords(next, pending)

    counted := make(chan pair)
    go countWords(pending, counted)

    return fillStats(counted)
}
```

Она выглядит хорошо, но есть ещё одна вещь, которую мы могли бы изменить.

Канал `pending` создаётся в родительской функции только для передачи в дочернюю функцию `submitWords`. Лучше было бы создать канал в `submitWords` и вернуть его родителю, чтобы `submitWords` полностью владела им. То же самое относится к каналу `counted` и функции `countWords`.

Тогда `countDigitsInWords` будет выглядеть так:

```go
func countDigitsInWords(next func() string) counter {
    pending := submitWords(next)
    counted := countWords(pending)
    return fillStats(counted)
}
```

Теперь владение ясно, и программу легче понимать. Но куда делись все goroutines? Мы запускаем их внутри `submitWords` и `countWords` так:

```go
// submitWords отправляет слова для подсчёта.
func submitWords(next func() string) chan string {
    out := make(chan string)
    go func() {
        for {
            word := next()
            out <- word
            if word == "" {
                break
            }
        }
    }()
    return out
}

// countWords считает цифры в словах.
func countWords(in chan string) chan pair {
    out := make(chan pair)
    go func() {
        for {
            word := <-in
            count := countDigits(word)
            out <- pair{word, count}
            if word == "" {
                break
            }
        }
    }()
    return out
}
```

Убедимся, что программа всё ещё работает как ожидается:

```go
func main() {
    phrase := "0ne 1wo thr33 4068"
    next := wordGenerator(phrase)
    stats := countDigitsInWords(next)
    printStats(stats)
}
```

У этого подхода есть недостаток: `submitWords` и `countWords` теперь более сложные и запускают goroutines. С другой стороны, `countDigitsInWords` проще и надёжнее (особенно если мы сделаем каналы направленными, что мы обсудим позже). Какой вариант выбрать, зависит от ваших предпочтений, но определённо не стоит смешивать два метода.

Возврат выходного канала из функции и заполнение его внутри внутренней goroutine — это распространённый паттерн в Go.

---

## Резюме

* **Goroutines** — это легковесные потоки выполнения в Go, которые позволяют писать конкурентные программы.
    
* **WaitGroup** используется для синхронизации и ожидания завершения нескольких goroutines.
    
* **Channels** обеспечивают безопасную передачу данных между goroutines и их синхронизацию.
    
* Разделение логики на **reader** и **worker** делает код более понятным и масштабируемым.
    
* Паттерн **выходного канала** упрощает композицию конкурентных функций.
