Популярный совет в Go звучит разумно. Оборачивайте каждую ошибку, пока она всплывает по стеку вызовов, чтобы всегда знать, откуда она пришла:
func getUser(id string) (*User, error) {
u, err := db.QueryUser(id)
if err != nil {
return nil, fmt.Errorf("getUser: query failed: %w", err)
}
return u, nil
}В маленьком проекте это работает. Но в системе, где один запрос проходит через шесть сервисов, вы получаете строки ошибок вот такого вида:
processPayment: validateAccount: fetchBalance: getUser: query failed: context deadline exceeded
Это не наблюдаемость, а стектрейс, переодетый в сообщение об ошибке. Вы дублируете то, что структурированное логирование уже даёт (имя функции, файл, строку), и платите за аллокацию
fmt.Errorf на каждом несчастливом пути.➡️ Что делать вместо
Оборачивать ошибки на границах доверия. Это вызов другого сервиса, слой базы данных или внешняя зависимость. Внутри одного пакета пусть ошибка летит как есть, а работу делает логгер:
// На границе оборачиваем
func (s *PaymentService) Process(ctx context.Context, req *ProcessRequest) error {
if err := s.ledger.Debit(ctx, req.AccountID, req.Amount); err != nil {
return fmt.Errorf("debit account %s: %w", req.AccountID, err)
}
return nil
}
// Внутри пакета просто возвращаем
func validate(req *ProcessRequest) error {
if req.Amount <= 0 {
return ErrInvalidAmount
}
return nil
}
Оборачивание уместно на стыках, где теряется контекст. Внутри пакета оно лишь раздувает сообщения и аллокации. Применяйте его как эвристику под конкретную границу, а не как правило для каждой функции.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека Go-разработчика
#GoToProduction