Конкурентность в Go: channels vs sync — когда что использовать

Подробный разбор конкурентности в Go: когда использовать channels, а когда sync.Mutex. Сравнение производительности, паттерны worker pool, fan-out/fan-in, правильное использование WaitGroup, Once, Pool. Практические примеры и бенчмарки.
- теги
- #Go #Программирование #Конкурентность #Производительность
- категории
- Programming
- опубликовано
Работаешь с Go? Значит, рано или поздно столкнешься с конкурентностью. Go продвигает философию “Don’t communicate by sharing memory, share memory by communicating” — и все сразу бегут использовать channels везде. Но это не всегда правильно.
Недавно профилировал сервис на Go и обнаружил, что замена channels на mutex в критичных местах дала прирост производительности в 3 раза. Расскажу, когда что использовать, и почему слепое следование “Go way” может убить производительность.
Конкурентность vs Параллелизм
Сначала разберемся с терминами, потому что многие путают.
Конкурентность (Concurrency) — структура программы, которая позволяет выполнять несколько задач, переключаясь между ними. Может работать на одном ядре.
Параллелизм (Parallelism) — одновременное выполнение нескольких задач на разных ядрах.
// Конкурентность — 2 горутины на 1 ядре (переключаются)
runtime.GOMAXPROCS(1)
go task1()
go task2()
// Параллелизм — 2 горутины на 2 ядрах (работают одновременно)
runtime.GOMAXPROCS(2)
go task1()
go task2()
Go дает конкурентность из коробки через горутины. Параллелизм зависит от GOMAXPROCS (по умолчанию = количество ядер).
Channels: основы
Channel — это типизированный канал для передачи данных между горутинами. Основной примитив конкурентности в Go.
// Unbuffered channel — блокирует отправителя до получения
ch := make(chan int)
// Buffered channel — блокирует только когда буфер полон
ch := make(chan int, 10)
// Отправка
ch <- 42
// Получение
value := <-ch
// Закрытие
close(ch)
Unbuffered vs Buffered channels
Unbuffered channel — синхронизация. Отправитель блокируется, пока получатель не прочитает.
func main() {
ch := make(chan int) // unbuffered
go func() {
fmt.Println("Sending...")
ch <- 42 // Блокируется здесь
fmt.Println("Sent!")
}()
time.Sleep(time.Second)
fmt.Println("Receiving...")
value := <-ch // Разблокирует отправителя
fmt.Println("Received:", value)
}
// Output:
// Sending...
// (пауза 1 сек)
// Receiving...
// Sent!
// Received: 42
Buffered channel — асинхронность до заполнения буфера.
func main() {
ch := make(chan int, 2) // буфер на 2 элемента
ch <- 1 // Не блокируется
ch <- 2 // Не блокируется
// ch <- 3 // Заблокируется — буфер полон
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
}
Select — мультиплексирование каналов
select позволяет ждать на нескольких каналах одновременно:
func worker(done <-chan struct{}, tasks <-chan Task) {
for {
select {
case <-done:
fmt.Println("Worker stopping")
return
case task := <-tasks:
process(task)
}
}
}
Non-blocking операции с default:
select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message available")
}
Timeout:
select {
case result := <-ch:
return result, nil
case <-time.After(5 * time.Second):
return nil, errors.New("timeout")
}
sync пакет: основы
Пакет sync предоставляет низкоуровневые примитивы синхронизации.
sync.Mutex — взаимное исключение
Защищает shared state от одновременного доступа:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
sync.RWMutex — читатели и писатели
Когда читателей много, а писателей мало — RWMutex эффективнее:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) (string, bool) {
c.mu.RLock() // Несколько читателей могут работать одновременно
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // Только один писатель
defer c.mu.Unlock()
c.data[key] = value
}
sync.WaitGroup — ожидание завершения
Ждем завершения группы горутин:
func processItems(items []Item) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait() // Ждем всех
}
Важно: wg.Add() вызывается ДО запуска горутины, не внутри!
// НЕПРАВИЛЬНО — race condition
for _, item := range items {
go func(item Item) {
wg.Add(1) // Может не успеть выполниться до wg.Wait()
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
// ПРАВИЛЬНО
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
sync.Once — выполнить один раз
Гарантирует однократное выполнение (инициализация, singleton):
type Database struct {
once sync.Once
conn *sql.DB
}
func (d *Database) Connection() *sql.DB {
d.once.Do(func() {
// Выполнится только один раз, даже при конкурентных вызовах
d.conn, _ = sql.Open("postgres", "...")
})
return d.conn
}
sync.Pool — переиспользование объектов
Уменьшает давление на GC для часто создаваемых объектов:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// работаем с buf
}
sync.Map — конкурентная map
Когда ключи стабильны или читателей много больше писателей:
var cache sync.Map
// Запись
cache.Store("key", "value")
// Чтение
if val, ok := cache.Load("key"); ok {
fmt.Println(val.(string))
}
// LoadOrStore — атомарно
actual, loaded := cache.LoadOrStore("key", "default")
// Удаление
cache.Delete("key")
// Итерация
cache.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // продолжить итерацию
})
Важно: sync.Map НЕ всегда быстрее map + RWMutex. Используй когда:
- Ключи стабильны (редко добавляются новые)
- Много читателей, мало писателей
- Горутины работают с разными ключами
sync.Cond — условные переменные
Для сложной координации (редко нужен):
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Push(item int) {
q.mu.Lock()
q.items = append(q.items, item)
q.cond.Signal() // Разбудить одного ожидающего
q.mu.Unlock()
}
func (q *Queue) Pop() int {
q.mu.Lock()
for len(q.items) == 0 {
q.cond.Wait() // Ждать сигнала
}
item := q.items[0]
q.items = q.items[1:]
q.mu.Unlock()
return item
}
Когда использовать channels
Channels хороши для:
1. Передача ownership данных
Когда одна горутина “передает” данные другой и больше не использует их:
func producer(out chan<- Task) {
for {
task := createTask()
out <- task // Передаем ownership
// Больше не трогаем task
}
}
func consumer(in <-chan Task) {
for task := range in {
process(task) // Теперь мы владеем task
}
}
2. Координация и сигнализация
Graceful shutdown, отмена операций:
func server(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("Shutting down...")
return
default:
handleRequest()
}
}
}
func main() {
done := make(chan struct{})
go server(done)
// ... работаем ...
close(done) // Сигнал на завершение
}
3. Pipeline паттерн
Цепочка обработки данных:
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Pipeline: gen -> sq -> print
for n := range sq(gen(1, 2, 3, 4)) {
fmt.Println(n) // 1, 4, 9, 16
}
}
4. Fan-out / Fan-in
Распределение работы и сбор результатов:
// Fan-out: один источник, много обработчиков
func fanOut(in <-chan Task, workers int) []<-chan Result {
outs := make([]<-chan Result, workers)
for i := 0; i < workers; i++ {
outs[i] = worker(in)
}
return outs
}
// Fan-in: много источников, один получатель
func fanIn(channels ...<-chan Result) <-chan Result {
var wg sync.WaitGroup
out := make(chan Result)
for _, ch := range channels {
wg.Add(1)
go func(c <-chan Result) {
defer wg.Done()
for v := range c {
out <- v
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
5. Rate limiting
Ограничение скорости обработки:
func rateLimited(requests <-chan Request) {
// 10 запросов в секунду
limiter := time.NewTicker(100 * time.Millisecond)
defer limiter.Stop()
for req := range requests {
<-limiter.C // Ждем тик
go handle(req)
}
}
// Burst rate limiting
func burstRateLimited(requests <-chan Request) {
// Разрешаем burst до 3, потом 1 в секунду
burstyLimiter := make(chan time.Time, 3)
// Заполняем burst
for i := 0; i < 3; i++ {
burstyLimiter <- time.Now()
}
// Пополняем со скоростью 1/сек
go func() {
for t := range time.Tick(time.Second) {
burstyLimiter <- t
}
}()
for req := range requests {
<-burstyLimiter
go handle(req)
}
}
Когда использовать sync
Mutex и другие примитивы sync лучше для:
1. Защита shared state
Когда нужно просто защитить переменную:
// С mutex — просто и быстро
type Stats struct {
mu sync.Mutex
requests int
errors int
}
func (s *Stats) RecordRequest() {
s.mu.Lock()
s.requests++
s.mu.Unlock()
}
func (s *Stats) RecordError() {
s.mu.Lock()
s.errors++
s.mu.Unlock()
}
// С channels — сложнее и медленнее
type Stats struct {
requests chan int
errors chan int
data struct {
requests int
errors int
}
}
func NewStats() *Stats {
s := &Stats{
requests: make(chan int),
errors: make(chan int),
}
go s.run()
return s
}
func (s *Stats) run() {
for {
select {
case <-s.requests:
s.data.requests++
case <-s.errors:
s.data.errors++
}
}
}
2. Read-heavy workloads
Когда читателей много, писателей мало — RWMutex:
type ConfigStore struct {
mu sync.RWMutex
config Config
}
func (c *ConfigStore) Get() Config {
c.mu.RLock()
defer c.mu.RUnlock()
return c.config
}
func (c *ConfigStore) Update(cfg Config) {
c.mu.Lock()
defer c.mu.Unlock()
c.config = cfg
}
3. Инициализация (sync.Once)
Ленивая инициализация singleton:
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
instance = &Database{}
instance.connect()
})
return instance
}
4. Переиспользование объектов (sync.Pool)
Уменьшение аллокаций:
var jsonPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func MarshalJSON(v interface{}) ([]byte, error) {
buf := jsonPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
jsonPool.Put(buf)
}()
enc := json.NewEncoder(buf)
if err := enc.Encode(v); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
Бенчмарки: channels vs mutex
Давай измерим производительность. Задача: инкрементировать счетчик из нескольких горутин.
// benchmark_test.go
package main
import (
"sync"
"sync/atomic"
"testing"
)
// Вариант 1: Mutex
type MutexCounter struct {
mu sync.Mutex
value int64
}
func (c *MutexCounter) Inc() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
// Вариант 2: Channel
type ChannelCounter struct {
inc chan struct{}
value int64
}
func NewChannelCounter() *ChannelCounter {
c := &ChannelCounter{
inc: make(chan struct{}),
}
go c.run()
return c
}
func (c *ChannelCounter) run() {
for range c.inc {
c.value++
}
}
func (c *ChannelCounter) Inc() {
c.inc <- struct{}{}
}
// Вариант 3: Atomic
type AtomicCounter struct {
value int64
}
func (c *AtomicCounter) Inc() {
atomic.AddInt64(&c.value, 1)
}
// Бенчмарки
func BenchmarkMutexCounter(b *testing.B) {
c := &MutexCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Inc()
}
})
}
func BenchmarkChannelCounter(b *testing.B) {
c := NewChannelCounter()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Inc()
}
})
}
func BenchmarkAtomicCounter(b *testing.B) {
c := &AtomicCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Inc()
}
})
}
Результаты (MacBook Pro M1, 8 cores):
BenchmarkMutexCounter-8 50000000 24.5 ns/op
BenchmarkChannelCounter-8 10000000 145.0 ns/op
BenchmarkAtomicCounter-8 200000000 6.2 ns/op
| Подход | ns/op | Относительно |
|---|---|---|
| Atomic | 6.2 | 1x (эталон) |
| Mutex | 24.5 | 4x медленнее |
| Channel | 145.0 | 23x медленнее |
Вывод: Для простых операций над shared state mutex в 6 раз быстрее channels. Atomic еще в 4 раза быстрее mutex.
Когда channels быстрее?
Channels выигрывают при сложной координации, когда альтернатива — много mutex и условных переменных:
// Pipeline с channels — просто и эффективно
func pipeline(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for v := range in {
out <- v * 2
}
close(out)
}()
return out
}
// То же самое на mutex — ад
type PipelineStage struct {
mu sync.Mutex
cond *sync.Cond
input []int
output []int
closed bool
}
// ... 50+ строк кода ...
Паттерны конкурентности
Worker Pool
Ограниченное число воркеров обрабатывают задачи из очереди:
func workerPool(numWorkers int, tasks <-chan Task, results chan<- Result) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for task := range tasks {
result := process(task)
results <- result
}
}(i)
}
wg.Wait()
close(results)
}
func main() {
tasks := make(chan Task, 100)
results := make(chan Result, 100)
// Запускаем 10 воркеров
go workerPool(10, tasks, results)
// Отправляем задачи
go func() {
for _, t := range getAllTasks() {
tasks <- t
}
close(tasks)
}()
// Собираем результаты
for result := range results {
handleResult(result)
}
}
Semaphore (ограничение конкурентности)
Ограничиваем количество одновременных операций:
// Semaphore через buffered channel
type Semaphore chan struct{}
func NewSemaphore(n int) Semaphore {
return make(chan struct{}, n)
}
func (s Semaphore) Acquire() {
s <- struct{}{}
}
func (s Semaphore) Release() {
<-s
}
// Использование
func processWithLimit(items []Item, maxConcurrency int) {
sem := NewSemaphore(maxConcurrency)
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
sem.Acquire()
go func(item Item) {
defer wg.Done()
defer sem.Release()
process(item)
}(item)
}
wg.Wait()
}
Context для отмены
Используй context для graceful cancellation:
func worker(ctx context.Context, tasks <-chan Task) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case task, ok := <-tasks:
if !ok {
return nil // Канал закрыт
}
if err := process(ctx, task); err != nil {
return err
}
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
tasks := make(chan Task)
// Запускаем воркеры
g, ctx := errgroup.WithContext(ctx)
for i := 0; i < 10; i++ {
g.Go(func() error {
return worker(ctx, tasks)
})
}
// Отправляем задачи
go func() {
defer close(tasks)
for _, t := range getAllTasks() {
select {
case <-ctx.Done():
return
case tasks <- t:
}
}
}()
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
errgroup — координация с ошибками
Пакет golang.org/x/sync/errgroup упрощает работу с группой горутин:
import "golang.org/x/sync/errgroup"
func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]Response, len(urls))
for i, url := range urls {
i, url := i, url // Capture
g.Go(func() error {
resp, err := fetch(ctx, url)
if err != nil {
return err
}
results[i] = resp
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Singleflight — дедупликация запросов
Предотвращаем thundering herd (много одинаковых запросов):
import "golang.org/x/sync/singleflight"
var requestGroup singleflight.Group
func getData(key string) ([]byte, error) {
// Все конкурентные запросы с одним key выполнят только один fetch
v, err, _ := requestGroup.Do(key, func() (interface{}, error) {
return fetchFromDB(key)
})
if err != nil {
return nil, err
}
return v.([]byte), nil
}
Очень полезно для кеша: если 1000 запросов одновременно просят один ключ, в БД пойдет только 1 запрос.
Race Conditions и как их избежать
Детектор рейсов
Go имеет встроенный детектор race conditions:
go test -race ./...
go build -race ./cmd/server
Пример race condition:
// RACE CONDITION
func main() {
counter := 0
for i := 0; i < 1000; i++ {
go func() {
counter++ // Несинхронизированный доступ
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // Неопределенный результат
}
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000a4010 by goroutine 8:
main.main.func1()
/main.go:11 +0x3c
Previous write at 0x00c0000a4010 by goroutine 7:
main.main.func1()
/main.go:11 +0x52
==================
Исправление с mutex
func main() {
var mu sync.Mutex
counter := 0
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // 1000
}
Исправление с atomic
func main() {
var counter int64
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
time.Sleep(time.Second)
fmt.Println(counter) // 1000
}
Исправление с channel
func main() {
counter := 0
inc := make(chan struct{}, 1000)
for i := 0; i < 1000; i++ {
go func() {
inc <- struct{}{}
}()
}
for i := 0; i < 1000; i++ {
<-inc
counter++
}
fmt.Println(counter) // 1000
}
Распространенные ошибки
1. Goroutine leak
// УТЕЧКА — горутина никогда не завершится
func leaky() {
ch := make(chan int)
go func() {
val := <-ch // Блокируется навсегда
fmt.Println(val)
}()
// ch никогда не получит значение и не закроется
}
// ИСПРАВЛЕНИЕ — используй context для отмены
func notLeaky(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return
}
}()
}
2. Закрытие канала несколько раз
// ПАНИКА
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel
// ИСПРАВЛЕНИЕ — закрывай только один раз
type SafeChannel struct {
ch chan int
once sync.Once
closed bool
}
func (s *SafeChannel) Close() {
s.once.Do(func() {
close(s.ch)
s.closed = true
})
}
3. Отправка в закрытый канал
// ПАНИКА
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel
// Закрывать должен только отправитель!
func producer(ch chan<- int) {
defer close(ch) // Закрываем когда закончили отправлять
for i := 0; i < 10; i++ {
ch <- i
}
}
4. Копирование mutex
// НЕПРАВИЛЬНО — mutex копируется
type Counter struct {
mu sync.Mutex
value int
}
func (c Counter) Inc() { // Получатель по значению — копирует mutex!
c.mu.Lock()
c.value++
c.mu.Unlock()
}
// ПРАВИЛЬНО — получатель по указателю
func (c *Counter) Inc() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
5. WaitGroup внутри горутины
// НЕПРАВИЛЬНО — race condition
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1) // Может не выполниться до wg.Wait()
defer wg.Done()
work()
}()
}
wg.Wait()
// ПРАВИЛЬНО — Add до запуска горутины
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
work()
}()
}
wg.Wait()
6. Захват переменной цикла
// НЕПРАВИЛЬНО — все горутины получат последнее значение i
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // Все напечатают 10
}()
}
// ПРАВИЛЬНО — явная передача значения
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i) // 0, 1, 2, ..., 9 (в случайном порядке)
}(i)
}
// Или локальная переменная
for i := 0; i < 10; i++ {
i := i // Shadow
go func() {
fmt.Println(i)
}()
}
Примечание: В Go 1.22+ эта проблема исправлена — каждая итерация создает новую переменную.
Таблица: когда что использовать
| Задача | Решение | Почему |
|---|---|---|
| Защита переменной | sync.Mutex | Просто и быстро |
| Много читателей, мало писателей | sync.RWMutex | Параллельное чтение |
| Счетчик | atomic | Самый быстрый |
| Однократная инициализация | sync.Once | Потокобезопасный singleton |
| Передача данных между горутинами | channel | Ownership transfer |
| Graceful shutdown | channel или context | Сигнализация |
| Pipeline обработки | channel | Естественный паттерн |
| Worker pool | channel + sync.WaitGroup | Координация воркеров |
| Ограничение конкурентности | Buffered channel (semaphore) | Простой semaphore |
| Переиспользование объектов | sync.Pool | Уменьшение аллокаций |
| Конкурентная map | sync.Map или map + RWMutex | Зависит от паттерна доступа |
| Дедупликация запросов | singleflight | Предотвращение thundering herd |
| Группа горутин с ошибками | errgroup | Удобная координация |
Best Practices
1. Начинай с простого
// Сначала mutex — просто и понятно
type Cache struct {
mu sync.RWMutex
data map[string]string
}
// Переходи на channels только если нужна сложная координация
2. Используй -race в тестах и CI
# .github/workflows/test.yml
- name: Test with race detector
run: go test -race -v ./...
3. Документируй правила синхронизации
// Counter is safe for concurrent use.
// All methods may be called from multiple goroutines.
type Counter struct {
mu sync.Mutex
value int
}
4. Держи критическую секцию короткой
// ПЛОХО — долгая операция под локом
func (c *Cache) Process(key string) {
c.mu.Lock()
defer c.mu.Unlock()
data := c.data[key]
result := expensiveOperation(data) // Долго!
c.data[key] = result
}
// ХОРОШО — лок только для доступа к данным
func (c *Cache) Process(key string) {
c.mu.RLock()
data := c.data[key]
c.mu.RUnlock()
result := expensiveOperation(data) // Вне лока
c.mu.Lock()
c.data[key] = result
c.mu.Unlock()
}
5. Используй context для отмены
func worker(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := doWork(ctx); err != nil {
return err
}
}
}
}
6. Закрывай каналы на стороне отправителя
func producer(out chan<- int) {
defer close(out) // Отправитель закрывает
for i := 0; i < 10; i++ {
out <- i
}
}
func consumer(in <-chan int) {
for v := range in { // Получатель итерирует до закрытия
fmt.Println(v)
}
}
Заключение
Конкурентность в Go — мощный инструмент, но важно выбирать правильный примитив:
Используй channels когда:
- Передаешь ownership данных
- Нужна координация и сигнализация
- Строишь pipeline
- Реализуешь graceful shutdown
Используй sync когда:
- Защищаешь shared state
- Нужна максимальная производительность
- Много читателей, мало писателей
- Инициализируешь singleton
Правило большого пальца:
Если сомневаешься — начни с mutex. Переходи на channels только когда код становится сложнее, чем мог бы быть с channels.
Помни: “Don’t communicate by sharing memory” — это guideline, а не закон. Иногда mutex — правильный выбор.
Если хочешь глубже разобраться в Go, почитай статью про антипаттерны в Go коде — там реальные примеры плохого кода из production. А для работы с микросервисами пригодится статья про gRPC в Go .
P.S. Всегда запускай тесты с -race. Лучше найти race condition в CI, чем в production в 3 часа ночи.