Допустимо ли возвращать один экземпляр пользовательской ошибки из нескольких горутин, если он содержит изменяемые поля?
Нет, без синхронизации это небезопасно. Общий экземпляр пользовательской ошибки допустимо переиспользовать только если его состояние неизменяемо после создания либо весь доступ к изменяемым данным защищён синхронизацией.
В Go ошибка является обычным значением, реализующим интерфейс error. Язык не требует, чтобы ошибки были неизменяемыми или потокобезопасными, поэтому безопасность их повторного использования определяется общими правилами конкурентного доступа к памяти.
Такой подход сохраняет простоту модели ошибок: библиотека может возвращать структуры с контекстом, а вызывающий код — анализировать их через errors.Is и errors.As. Однако ответственность за жизненный цикл и изменение полей остаётся у разработчика.
Если несколько горутин используют один объект ошибки, а одна из них изменяет его поля, возникает состояние гонки. Это касается не только явного присваивания, но и изменения срезов, карт, вложенных указателей и других ссылочных данных.
Опасность есть даже тогда, когда ошибка только форматируется: метод Error может читать поля одновременно с их изменением. В результате возможны гонка данных, непредсказательное сообщение, повреждение логируемого контекста или обнаружение проблемы только при запуске с детектором гонок.
Предпочтительный вариант — создавать отдельный экземпляр ошибки для каждой операции и полностью заполнять его до передачи в другую горутину. После публикации такой экземпляр не следует изменять; это делает методы Error, Is, As и Unwrap безопасными при параллельном вызове, если они сами не используют изменяемое общее состояние.
Если общий экземпляр действительно нужен, изменяемые данные необходимо защищать, например mutex-ом. Защищать следует весь согласованный набор операций чтения и записи, включая форматирование и пользовательские методы сопоставления, а не только отдельное присваивание.
Копирование структуры не всегда решает проблему. Поверхностная копия по-прежнему может разделять backing-массив среза, карту или вложенный указатель, поэтому для независимого экземпляра требуется глубокая копия соответствующих данных.
Практический компромисс таков: неизменяемые ошибки проще, дешевле и предсказуемее; синхронизированные ошибки позволяют обновлять состояние, но усложняют код, увеличивают стоимость операций и требуют аккуратного контроля блокировок. Ошибка не должна менять своё смысловое содержимое после того, как её начали анализировать или передавать между слоями.
Сервис возвращает общий объект ошибки квоты, в котором перед каждым запросом обновляется идентификатор клиента. При параллельной нагрузке один запрос может залогировать идентификатор другого, а детектор гонок сообщит о конкурентном доступе.
Рассматривались три варианта. Общий изменяемый объект без защиты прост, но некорректен; добавление mutex устраняет гонку, однако делает даже чтение сообщения зависимым от синхронизации; создание новой ошибки с копией идентификатора не разделяет состояние и не требует блокировок.
Выбран третий вариант: ошибка создаётся для конкретного запроса и после создания не изменяется. Это устраняет гонки, сохраняет точный диагностический контекст и делает поведение методов ошибки независимым от порядка выполнения горутин.
Нет. Приватность ограничивает доступ из других пакетов, но не запрещает конкурентное изменение внутри самого пакета. Кроме того, приватное поле может содержать ссылочный объект, который изменяется через внутренний код или переданный наружу метод.
Не обязательно. Если общий буфер изменяется при каждом вызове, параллельные вызовы метода могут конфликтовать. Безопасным считается либо построение результата из неизменяемых данных без общего изменяемого буфера, либо явная синхронизация доступа к этому буферу.
Обычно нет, если ошибка уже доступна другим горутинам или слоям. Такое дополнение меняет наблюдаемое состояние и может конфликтовать с чтением; лучше собрать весь контекст до публикации ошибки или создать новую ошибку, оборачивающую исходную через %w.