Сравните стратегии обработки ошибок: исключения, возвращаемые коды ошибок, типы Result/Either (Rust/Haskell), и подход Go с явным возвращением ошибки; разберите пример на Go, где программист проигнорировал возвращённую ошибку при работе с файлом, и опишите последствия и правильное исправление
1) Исключения exceptions—Java,Python,C++ит.п.exceptions — Java, Python, C++ и т.п.exceptions—Java,Python,C++ит.п.
Механика: при ошибке бросается исключение; если не перехвачено — стек разворачивается.Плюсы: удобны для «выхода» из глубоко вложенного кода; обработчики можно поставить в одном месте; меньше шумного кода по проверке ошибок.Минусы: контроль потока не явный в сигнатуре функции; возможны неожиданные исключения runtimeruntimeruntime; труднее понять, какие ошибки может бросать функция; производительность/стоимость при частых исключениях; нужен аккуратный механизм очистки finally/RAIIfinally/RAIIfinally/RAII.Проверяемые исключения JavaJavaJava дают явность в сигнатуре, но делают API жирным и часто раздражают разработчиков.
2) Возвращаемые коды ошибок C−style:int,errnoC-style: int, errnoC−style:int,errno
Механика: функция возвращает код 0/не−00/не-00/не−0 или записывает errno.Плюсы: минимальные накладные расходы; предсказуемо.Минусы: легко забыть проверить; малая выразительность толькочислатолько числатолькочисла; плохая композиция; глобальные errno — источник ошибок в многопоточной среде, если неправильно использовать.
3) Типы Result/Either Rust,Haskellит.д.Rust, Haskell и т.д.Rust,Haskellит.д.
Механика: функции возвращают тип вроде Result<T, E> или Either e a; компилятор/типовая система вынуждает обработать оба варианта либо явно пропагировать ошибку.Плюсы: безопасность — невозможно «случайно» игнорировать ошибку; очень выразительно; легко комбинировать/цепочить; в Rust — ? оператор для краткой пропагации с явной типовой безопасности.Минусы: немного больше шума в сигнатурах; иногда нужна обёртка/конвертация ошибок; новые пользователи привыкают к шаблонам обработки.
4) Подход Go явноевозвращениеошибкикакотдельногозначенияявное возвращение ошибки как отдельного значенияявноевозвращениеошибкикакотдельногозначения
Механика: func Foo.........T,errorT, errorT,error. Код явно возвращает err; вызов должен проверять err iferr!=nil...if err != nil { ... }iferr!=nil....Плюсы: явность, простота, читабельность; нет скрытого контроля потока; легко понимать код по месту вызова; хорош для простых случаев и concurrence.Минусы: компилятор не заставляет проверять возвращённую ошибку — её можно проигнорировать; шаблон «if err != nil { return err }» многословен; нужна дисциплина и инструменты staticcheck,errcheck,govetstaticcheck, errcheck, go vetstaticcheck,errcheck,govet для поиска игнорируемых ошибок. Для композиции используются явные проверки и обёртки ошибок fmt.Errorf(...fmt.Errorf(... %w ...), errors.Is/Asfmt.Errorf(....
Типичная дилемма: Result/Either дают статическое требование к обработке ошибок; exceptions и Go дают больше гибкости иошибокввидезабывчивостии ошибок в виде забывчивостииошибокввидезабывчивости. Выбор зависит от требований: безопасность + композиция → Result; удобство и меньше boilerplate для «эпичных» ошибок → exceptions; простота и явность API → Go-style.
Разбор конкретного примера на Go: проигнорирована ошибка при работе с файлом
Плохой код примерпримерпример: func savedata[]bytedata []bytedata[]byte error { f, _ := os.Create("out.txt") // ошибка проигнорирована defer f.Close // если f == nil — panic f.Writedatadatadata // возвращаемая ошибка от Write проигнорирована return nil }
Последствия такого кода:
Если os.Create вернёт ошибку например,нетправ,нехватаетместа,путьнедопустимнапример, нет прав, не хватает места, путь недопустимнапример,нетправ,нехватаетместа,путьнедопустим, f будет nil, следующая defer f.Close приведёт к panic, или попытка записи — к panic. Программа крашнется вместо аккуратной обработки ошибки.Даже если Create прошёл, запись WriteWriteWrite может провалиться дискполон,сбойI/Oдиск полон, сбой I/Oдискполон,сбойI/O. Поскольку ошибка Write проигнорирована, программа «думает», что всё успешно, данные могут быть потеряны.Ошибка при Close котораяможетсигнализироватьобошибкахзаписиприбуферизированнойзаписикоторая может сигнализировать об ошибках записи при буферизированной записикотораяможетсигнализироватьобошибкахзаписиприбуферизированнойзаписи тоже может быть потеряна, если Close не проверяется.Это приводит к скрытым потерям данных, непредсказуемым крахам и усложнению отладки.
Правильное исправление — явно проверять ошибки и корректно обрабатывать ошибку от Close. Пример хорошего кода идиоматичноидиоматичноидиоматично:
// гарантируем проверку ошибки от Close; используем именованный return err defer func { if cerr := f.Close; cerr != nil && err == nil { // если до этого не было ошибки — возвращаем ошибку от Close err = fmt.Errorf"closeout.txt:"close out.txt: %w", cerr"closeout.txt:
} else if cerr != nil { // если уже была ошибка — можно логировать или обернуть обе // log.Printf"closeerror:"close error: %v (previous error: %v)", cerr, err"closeerror:
} } if _, err = f.Writedatadatadata; err != nil { return fmt.Errorf"writeout.txt:"write out.txt: %w", err"writeout.txt:
} // для уверенности можно сделать Sync и проверить её ошибку if err = f.Sync; err != nil { return fmt.Errorf"syncout.txt:"sync out.txt: %w", err"syncout.txt:
} return nil
}
Пояснения:
Мы проверяем ошибку при создании файла и прекращаем операцию, если Create упал.Проверяем результат Write и возвращаем подробную обёрнутую ошибку используемиспользуем %w для последующей обработки через errors.Is/Asиспользуем.Особая осторожность с Close: многие ошибки записи проявляются во время Close особенноприбуферизированныхзаписяхособенно при буферизированных записяхособенноприбуферизированныхзаписях. Поэтому лучше проверять cerr и корректно передавать её наружу. Использование именованного возвращаемого значения err и отложенной функции — общепринятый паттерн для этого.Для поиска забытых проверок ошибок используйте инструменты: staticcheck, errcheck, go vet.
Рекомендации
В Go: всегда проверяйте возвращаемую ошибку, особенно от операций I/O и Close; оборачивайте ошибки контекстом; используйте errors.Is/As для сопоставления.В командах включите статический анализ staticcheck,errcheckstaticcheck, errcheckstaticcheck,errcheck в CI, чтобы ловить игнорируемые ошибки.Выбирайте стратегию обработки ошибок, исходя из требований проекта: нужны ли строгие гарантийные проверки на этапе компиляции тогдаResult−likeвалидацияболеебезопаснатогда Result-like валидация более безопаснатогдаResult−likeвалидацияболеебезопасна, или удобство и простота API exceptions/Go−styleexceptions/Go-styleexceptions/Go−style с дополнительными инструментами контроля.
Если хотите, могу:
показать ещё несколько типичных антипаттернов с ошибками в Go;показать аналогичный пример на Rust с Result и ? для сравнения.
Кратко — четыре подхода и их свойства
1) Исключения exceptions—Java,Python,C++ит.п.exceptions — Java, Python, C++ и т.п.exceptions—Java,Python,C++ит.п.
Механика: при ошибке бросается исключение; если не перехвачено — стек разворачивается.Плюсы: удобны для «выхода» из глубоко вложенного кода; обработчики можно поставить в одном месте; меньше шумного кода по проверке ошибок.Минусы: контроль потока не явный в сигнатуре функции; возможны неожиданные исключения runtimeruntimeruntime; труднее понять, какие ошибки может бросать функция; производительность/стоимость при частых исключениях; нужен аккуратный механизм очистки finally/RAIIfinally/RAIIfinally/RAII.Проверяемые исключения JavaJavaJava дают явность в сигнатуре, но делают API жирным и часто раздражают разработчиков.2) Возвращаемые коды ошибок C−style:int,errnoC-style: int, errnoC−style:int,errno
Механика: функция возвращает код 0/не−00/не-00/не−0 или записывает errno.Плюсы: минимальные накладные расходы; предсказуемо.Минусы: легко забыть проверить; малая выразительность толькочислатолько числатолькочисла; плохая композиция; глобальные errno — источник ошибок в многопоточной среде, если неправильно использовать.3) Типы Result/Either Rust,Haskellит.д.Rust, Haskell и т.д.Rust,Haskellит.д.
Механика: функции возвращают тип вроде Result<T, E> или Either e a; компилятор/типовая система вынуждает обработать оба варианта либо явно пропагировать ошибку.Плюсы: безопасность — невозможно «случайно» игнорировать ошибку; очень выразительно; легко комбинировать/цепочить; в Rust — ? оператор для краткой пропагации с явной типовой безопасности.Минусы: немного больше шума в сигнатурах; иногда нужна обёртка/конвертация ошибок; новые пользователи привыкают к шаблонам обработки.4) Подход Go явноевозвращениеошибкикакотдельногозначенияявное возвращение ошибки как отдельного значенияявноевозвращениеошибкикакотдельногозначения
Механика: func Foo......... T,errorT, errorT,error. Код явно возвращает err; вызов должен проверять err iferr!=nil...if err != nil { ... }iferr!=nil....Плюсы: явность, простота, читабельность; нет скрытого контроля потока; легко понимать код по месту вызова; хорош для простых случаев и concurrence.Минусы: компилятор не заставляет проверять возвращённую ошибку — её можно проигнорировать; шаблон «if err != nil { return err }» многословен; нужна дисциплина и инструменты staticcheck,errcheck,govetstaticcheck, errcheck, go vetstaticcheck,errcheck,govet для поиска игнорируемых ошибок. Для композиции используются явные проверки и обёртки ошибок fmt.Errorf(...fmt.Errorf(... %w ...), errors.Is/Asfmt.Errorf(....Типичная дилемма: Result/Either дают статическое требование к обработке ошибок; exceptions и Go дают больше гибкости иошибокввидезабывчивостии ошибок в виде забывчивостииошибокввидезабывчивости. Выбор зависит от требований: безопасность + композиция → Result; удобство и меньше boilerplate для «эпичных» ошибок → exceptions; простота и явность API → Go-style.
Разбор конкретного примера на Go: проигнорирована ошибка при работе с файлом
Плохой код примерпримерпример:
func savedata[]bytedata []bytedata[]byte error {
f, _ := os.Create("out.txt") // ошибка проигнорирована
defer f.Close // если f == nil — panic
f.Writedatadatadata // возвращаемая ошибка от Write проигнорирована
return nil
}
Последствия такого кода:
Если os.Create вернёт ошибку например,нетправ,нехватаетместа,путьнедопустимнапример, нет прав, не хватает места, путь недопустимнапример,нетправ,нехватаетместа,путьнедопустим, f будет nil, следующая defer f.Close приведёт к panic, или попытка записи — к panic. Программа крашнется вместо аккуратной обработки ошибки.Даже если Create прошёл, запись WriteWriteWrite может провалиться дискполон,сбойI/Oдиск полон, сбой I/Oдискполон,сбойI/O. Поскольку ошибка Write проигнорирована, программа «думает», что всё успешно, данные могут быть потеряны.Ошибка при Close котораяможетсигнализироватьобошибкахзаписиприбуферизированнойзаписикоторая может сигнализировать об ошибках записи при буферизированной записикотораяможетсигнализироватьобошибкахзаписиприбуферизированнойзаписи тоже может быть потеряна, если Close не проверяется.Это приводит к скрытым потерям данных, непредсказуемым крахам и усложнению отладки.Правильное исправление — явно проверять ошибки и корректно обрабатывать ошибку от Close. Пример хорошего кода идиоматичноидиоматичноидиоматично:
import (
"fmt"
"os"
)
func savedata[]bytedata []bytedata[]byte errerrorerr errorerrerror {
// гарантируем проверку ошибки от Close; используем именованный return errf, err := os.Create("out.txt")
if err != nil {
return fmt.Errorf("create out.txt: %w", err)
}
defer func {
if cerr := f.Close; cerr != nil && err == nil {
// если до этого не было ошибки — возвращаем ошибку от Close
err = fmt.Errorf"closeout.txt:"close out.txt: %w", cerr"closeout.txt: } else if cerr != nil {
// если уже была ошибка — можно логировать или обернуть обе
// log.Printf"closeerror:"close error: %v (previous error: %v)", cerr, err"closeerror: }
}
if _, err = f.Writedatadatadata; err != nil {
return fmt.Errorf"writeout.txt:"write out.txt: %w", err"writeout.txt: }
// для уверенности можно сделать Sync и проверить её ошибку
if err = f.Sync; err != nil {
return fmt.Errorf"syncout.txt:"sync out.txt: %w", err"syncout.txt: }
return nil
}
Пояснения:
Мы проверяем ошибку при создании файла и прекращаем операцию, если Create упал.Проверяем результат Write и возвращаем подробную обёрнутую ошибку используемиспользуем %w для последующей обработки через errors.Is/Asиспользуем.Особая осторожность с Close: многие ошибки записи проявляются во время Close особенноприбуферизированныхзаписяхособенно при буферизированных записяхособенноприбуферизированныхзаписях. Поэтому лучше проверять cerr и корректно передавать её наружу. Использование именованного возвращаемого значения err и отложенной функции — общепринятый паттерн для этого.Для поиска забытых проверок ошибок используйте инструменты: staticcheck, errcheck, go vet.Рекомендации
В Go: всегда проверяйте возвращаемую ошибку, особенно от операций I/O и Close; оборачивайте ошибки контекстом; используйте errors.Is/As для сопоставления.В командах включите статический анализ staticcheck,errcheckstaticcheck, errcheckstaticcheck,errcheck в CI, чтобы ловить игнорируемые ошибки.Выбирайте стратегию обработки ошибок, исходя из требований проекта: нужны ли строгие гарантийные проверки на этапе компиляции тогдаResult−likeвалидацияболеебезопаснатогда Result-like валидация более безопаснатогдаResult−likeвалидацияболеебезопасна, или удобство и простота API exceptions/Go−styleexceptions/Go-styleexceptions/Go−style с дополнительными инструментами контроля.Если хотите, могу:
показать ещё несколько типичных антипаттернов с ошибками в Go;показать аналогичный пример на Rust с Result и ? для сравнения.