Общий счётчик в Go: почему обычный плюс не безопасен между goroutine
Несколько обработчиков увеличивают общий счётчик, и итог иногда меньше ожидаемого. Операция чтения, прибавления и записи не становится безопасной только потому, что выглядит одной строкой. Конкурентный доступ требует выбранной синхронизации.
Как разобрать ситуацию
Для небольшой составной операции можно использовать mutex. Он должен защищать все обращения к одному состоянию, а не только часть записей. Если вместе со счётчиком обновляются другие поля, важно сохранить их общий инвариант. Отдельная атомарная переменная подходит не для любой структуры и не заменяет проектирование состояния.
Пример и ожидаемый результат
Сто goroutine увеличивают защищённую переменную на единицу, затем main ждёт завершения всех работ. Ожидаемый результат — 100. Пример не пытается поймать случайную гонку на удачном запуске: в коде сразу показан согласованный доступ под одним mutex.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
n := 0
for i := 0; i < 100; i++ {
wg.Add(1)
go func() { defer wg.Done(); mu.Lock(); n++; mu.Unlock() }()
}
wg.Wait()
fmt.Println(n)
}
Обычный запуск программы выводит 100. Отдельный запуск с -race является дополнительной диагностикой и здесь не подменяется результатом обычного go run.
Проверка перед применением
- Проверьте, что чтение счётчика во время работы тоже использует согласованную синхронизацию.
- Запустите тесты с race detector в поддерживаемой среде, а не делайте вывод по одному правильному числу.
- Не копируйте структуру с уже используемым mutex: владение состоянием должно оставаться понятным.
В реальном обработчике отдельно проверьте длительность критической секции. Сетевой вызов под общим mutex способен последовательно заблокировать всех клиентов, хотя гонка исчезнет. Хорошее исправление защищает нужное состояние коротко и оставляет внешние операции вне блокировки, когда это допускает бизнес-инвариант.
Границы решения
Правильный счётчик не делает распределённую метрику устойчивой к перезапуску и не гарантирует согласованность между несколькими процессами. Для агрегирования событий сервиса нужен соответствующий внешний механизм.
Если базовый Go уже знаком и хочется связать HTTP, хранение данных и обработку отказов в одну систему, посмотрите наш платный курс
Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker. Перед записью сравните программу со своей задачей и подготовкой.
Техническая справка: pkg.go.dev: документация, go.dev: документация.
Следующий шаг: Go backend: HTTP, конкурентность и устойчивость сервиса. По смежным вопросам: HTTP retry в Go: почему один список кодов не делает повтор безопасным, Как оборачивать ошибки в Go и не потерять машинную проверку причины.
