Паттерны конкурентности в Go
Golang был создан с нуля для эпохи многоядерных процессоров и распределённых систем. Его слоган «Do not communicate by sharing memory; instead, share memory by communicating» идеально отражает философию языка
Паттерны конкурентности в Go не просто «возможны» - они встроены в язык. Это делает параллельный код в Go:
- Простым и читаемым
- Масштабируемым (десятки тысяч горутин без overhead’а)
- Производительным (минимальные контекстные переключения)
Примеры паттернов
1. Producer-Consumer (через каналы)
Каналы - это по-сути безопасные очереди
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
fmt.Printf("Произвёл: %d\n", i)
ch <- i
time.Sleep(time.Millisecond * 300)
}
close(ch) // Не забудь закрыть:)
}
func consumer(ch <-chan int) {
for val := range ch {
fmt.Printf("Потребил: %d\n", val)
time.Sleep(time.Millisecond * 500)
}
}
func main() {
ch := make(chan int, 3) // Буферизированный канал
go producer(ch)
go consumer(ch)
time.Sleep(time.Second * 3)
fmt.Println("Готово")
}
Канал автоматически синхронизирует доступ: отправка блокируется при полном буфере, получение - при пустом.
2. Worker Pool
Определенное число горутин обрабатывают задачи из очереди
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Воркер %d обработал задачу %d\n", id, job)
// Симулируем работу
// time.Sleep(time.Second)
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
var wg sync.WaitGroup
// Запускаем воркеров
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// Отправляем задачи
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
fmt.Println("Все задачи выполнены")
}
WaitGroup гарантирует, что main дождётся завершения всех воркеров.
3. Pipeline
Данные проходят через цепочку горутин, каждая выполняет свою стадию обработки.
package main
import "fmt"
func generator(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}
func main() {
in := generator(1, 2, 3, 4, 5)
out := square(in)
for result := range out {
fmt.Println(result) // 1, 4, 9, 16, 25
}
}
Легко масштабировать и добавлять этапы для параллельной обработки.
4. Проверяем ctx
Важно помнить про ctx, так как параллельные операции зачастую применяются в загруженных участках кода, и их лучше завершать при необходимости. Например, клиент дернул http-ручку, но решил отменить запрос. Нет смысла производить дальнейшую обработку. В таком случае нам нужно завершить выполнение всех горутин, и не тратить ресурсы впустую
Ну или если у нас есть определенный timeout для выполнения кода. Например, мы знаем, что обработка не может превышать час, то мы можем добавить context.WithTimeout, чтобы воркер просто завершился при неожиданно появившемся баге, и не простаивал бесконечно
package main
import (
"context"
"fmt"
"time"
)
func longTask(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
fmt.Println("Задача завершена")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := longTask(ctx); err != nil {
fmt.Println("Задача отменена:", err)
}
}
Инструменты для конкурентности в go мне очень нравятся. Они производительные, просто читаются и удобные в использовании. Но тут просто нужно быть осторожным, можно жестко запороть все из-за банальной ошибки, поэтому нужно смотреть в оба👀(Но опять же, это не проблема go, это просто зачастую связанно с большим кол-вом логики во всех этих обработках🫠)