Разберите следующий Go-пример и укажите все проблемные места в обработке ошибок и ресурсном управлении: func readFile(name string) ([]byte, error) { f, _ := os.Open(name); defer f.Close(); return ioutil.ReadAll(f) } ; когда уместно возвращать ошибку, а когда вызывать panic
Код func readFilenamestringname stringnamestring[]byte,error[]byte, error[]byte,error { f, _ := os.Opennamenamename
defer f.Close
return ioutil.ReadAllfff
} содержит несколько явных и скрытых проблем в обработке ошибок и управлении ресурсами.
Проблемные места
Игнорирование ошибки от os.Openf,:=os.Open(name)f, _ := os.Open(name)f,:=os.Open(name). Если Open вернул ошибку файлненайден,нетправит.п.файл не найден, нет прав и т. п.файлненайден,нетправит.п., вы теряете причину отказа и продолжаете работу с некорректным f.Вызов defer f.Close до проверки ошибки открытия. Вызывая методы на f без проверки err, вы рискуете вызвать метод на nil runtimepanicruntime panicruntimepanic. Всегда проверяйте err перед использованием f.Не проверяется ошибка от ioutil.ReadAll онавозвращается,новывозвращаетерезультатпрямо—этонормальнотолькоеслиReadAllвозвращает(data,err)ивыпростовозвращаетеих;вданномпримеревыдействительновозвращаетерезультатReadAll,ноэтоскрываетдругуюпроблемусCloseона возвращается, но вы возвращаете результат прямо — это нормально только если ReadAll возвращает (data, err) и вы просто возвращаете их; в данном примере вы действительно возвращаете результат ReadAll, но это скрывает другую проблему с Closeонавозвращается,новывозвращаетерезультатпрямо—этонормальнотолькоеслиReadAllвозвращает(data,err)ивыпростовозвращаетеих;вданномпримеревыдействительновозвращаетерезультатReadAll,ноэтоскрываетдругуюпроблемусClose. В простом варианте ReadAll возвращает data,errdata, errdata,err — вы их возвращаете, так что прямой проблемой это не является; но см. ниже про Close.Ошибка от f.Close игнорируется. Если ReadAll прошёл успешно, но Close вернул ошибку например,призаписинасетевойFSнапример, при записи на сетевой FSнапример,призаписинасетевойFS, вы её теряете. Лучше вернуть ошибку Close, если нет другой илиобъединитьошибкиили объединить ошибкиилиобъединитьошибки.Использование ioutil.ReadAll вболееновыхверсияхGoчастьioutilустарела—лучшеиспользоватьio.ReadAllилиos.ReadFileдлячтениявсегофайлав более новых версиях Go часть ioutil устарела — лучше использовать io.ReadAll или os.ReadFile для чтения всего файлавболееновыхверсияхGoчастьioutilустарела—лучшеиспользоватьio.ReadAllилиos.ReadFileдлячтениявсегофайла.Чтение всего файла в память может быть неподходящим для больших файлов — возможно нужен потоковый подход.
Исправленные варианты
1) Простейший и рекомендуемый Go1.16+Go 1.16+Go1.16+: func readFilenamestringname stringnamestring[]byte,error[]byte, error[]byte,error { return os.ReadFilenamenamename // открывает, читает и закрывает за вас, возвращает []byte,error[]byte, error[]byte,error
}
2) Если нужно вручную корректнаяобработкаошибокCloseкорректная обработка ошибок CloseкорректнаяобработкаошибокClose: func readFilenamestringname stringnamestring[]byte,error[]byte, error[]byte,error { f, err := os.Opennamenamename
if err != nil { return nil, err }
3) Вариант с defer, чтобы гарантировать Close и вернуть ошибку Close только если нет ошибок чтения: func readFilenamestringname stringnamestring[]byte,error[]byte, error[]byte,error { f, err := os.Opennamenamename
if err != nil { return nil, err }
var readErr error defer func { if cerr := f.Close; cerr != nil && readErr == nil { readErr = cerr } } var data byte data, readErr = io.ReadAllfff
return data, readErr
} тутиспользуетсяименованныйreadErr,чтобыdeferмогмодифицироватьвозвращаемуюошибкутут используется именованный readErr, чтобы defer мог модифицировать возвращаемую ошибкутутиспользуетсяименованныйreadErr,чтобыdeferмогмодифицироватьвозвращаемуюошибку
Когда возвращать ошибку, а когда вызывать panic
Всегда возвращайте ошибки в коде библиотек и везде, где ошибка ожидаема и её можно обработать отсутствиефайла,сетевыетаймауты,некорректныевходныеданныеотсутствие файла, сетевые таймауты, некорректные входные данныеотсутствиефайла,сетевыетаймауты,некорректныевходныеданные.panic следует использовать очень осторожно — только для ситуаций, которые: являются внутренними ошибками/нарушением инвариантов программныеошибки,которыенельзявосстановитьпрограммные ошибки, которые нельзя восстановитьпрограммныеошибки,которыенельзявосстановить, илиявно свидетельствуют о баге в программе например,невернонастроеннаяструктура,которойникогданедолжнобытьвнормальномсостояниинапример, неверно настроенная структура, которой никогда не должно быть в нормальном состояниинапример,невернонастроеннаяструктура,которойникогданедолжнобытьвнормальномсостоянии.Для CLI-приложений в main обычно возвращают ошибку/логируют её и завершают программу log.Fatalилиos.Exitlog.Fatal или os.Exitlog.Fatalилиos.Exit, а не бросают panic — это даёт более контролируемый вывод.Библиотеки не должны вызывать panic для обычных ошибок — это лишает пользователя библиотеки возможности обработать ошибку.
Примеры:
Возвращать ошибку: файл не найден, неверный параметр, сетевой таймаут — это нормальные runtime-ошибки.panic: обнаружен внутренний противоречивый state например,nilуказательтам,гдеAPIгарантируетненулевоезначениенапример, nil указатель там, где API гарантирует ненулевое значениенапример,nilуказательтам,гдеAPIгарантируетненулевоезначение, или неверно написанный тест/инициализация, где дальнейшая работа бессмысленна.
Итого: ваш исходный код нужно исправить так, чтобы проверять ошибки от os.Open, не вызывать методы на потенциально nil, корректно закрывать файл и не терять ошибки от Close. В большинстве случаев лучше вернуть ошибку, а не вызывать panic; panic — для неизлечимых ошибок и нарушений инвариантов.
Код
func readFilenamestringname stringnamestring []byte,error[]byte, error[]byte,error {
f, _ := os.Opennamenamename defer f.Close return ioutil.ReadAllfff }
содержит несколько явных и скрытых проблем в обработке ошибок и управлении ресурсами.
Проблемные места
Игнорирование ошибки от os.Open f,:=os.Open(name)f, _ := os.Open(name)f,: =os.Open(name).Если Open вернул ошибку файлненайден,нетправит.п.файл не найден, нет прав и т. п.файлненайден,нетправит.п., вы теряете причину отказа и продолжаете работу с некорректным f.Вызов defer f.Close до проверки ошибки открытия.
Вызывая методы на f без проверки err, вы рискуете вызвать метод на nil runtimepanicruntime panicruntimepanic. Всегда проверяйте err перед использованием f.Не проверяется ошибка от ioutil.ReadAll онавозвращается,новывозвращаетерезультатпрямо—этонормальнотолькоеслиReadAllвозвращает(data,err)ивыпростовозвращаетеих;вданномпримеревыдействительновозвращаетерезультатReadAll,ноэтоскрываетдругуюпроблемусCloseона возвращается, но вы возвращаете результат прямо — это нормально только если ReadAll возвращает (data, err) и вы просто возвращаете их; в данном примере вы действительно возвращаете результат ReadAll, но это скрывает другую проблему с Closeонавозвращается,новывозвращаетерезультатпрямо—этонормальнотолькоеслиReadAllвозвращает(data,err)ивыпростовозвращаетеих;вданномпримеревыдействительновозвращаетерезультатReadAll,ноэтоскрываетдругуюпроблемусClose.
В простом варианте ReadAll возвращает data,errdata, errdata,err — вы их возвращаете, так что прямой проблемой это не является; но см. ниже про Close.Ошибка от f.Close игнорируется.
Если ReadAll прошёл успешно, но Close вернул ошибку например,призаписинасетевойFSнапример, при записи на сетевой FSнапример,призаписинасетевойFS, вы её теряете. Лучше вернуть ошибку Close, если нет другой илиобъединитьошибкиили объединить ошибкиилиобъединитьошибки.Использование ioutil.ReadAll вболееновыхверсияхGoчастьioutilустарела—лучшеиспользоватьio.ReadAllилиos.ReadFileдлячтениявсегофайлав более новых версиях Go часть ioutil устарела — лучше использовать io.ReadAll или os.ReadFile для чтения всего файлавболееновыхверсияхGoчастьioutilустарела—лучшеиспользоватьio.ReadAllилиos.ReadFileдлячтениявсегофайла.Чтение всего файла в память может быть неподходящим для больших файлов — возможно нужен потоковый подход.
Исправленные варианты
1) Простейший и рекомендуемый Go1.16+Go 1.16+Go1.16+:
func readFilenamestringname stringnamestring []byte,error[]byte, error[]byte,error {
return os.ReadFilenamenamename // открывает, читает и закрывает за вас, возвращает []byte,error[]byte, error[]byte,error }
2) Если нужно вручную корректнаяобработкаошибокCloseкорректная обработка ошибок CloseкорректнаяобработкаошибокClose:
data, readErr := io.ReadAllfff closeErr := f.Closefunc readFilenamestringname stringnamestring []byte,error[]byte, error[]byte,error {
f, err := os.Opennamenamename if err != nil {
return nil, err
}
if readErr != nil {
return nil, readErr
}
if closeErr != nil {
return nil, closeErr
}
return data, nil
}
3) Вариант с defer, чтобы гарантировать Close и вернуть ошибку Close только если нет ошибок чтения:
var readErr errorfunc readFilenamestringname stringnamestring []byte,error[]byte, error[]byte,error {
f, err := os.Opennamenamename if err != nil {
return nil, err
}
defer func {
if cerr := f.Close; cerr != nil && readErr == nil {
readErr = cerr
}
}
var data byte
data, readErr = io.ReadAllfff return data, readErr
}
тутиспользуетсяименованныйreadErr,чтобыdeferмогмодифицироватьвозвращаемуюошибкутут используется именованный readErr, чтобы defer мог модифицировать возвращаемую ошибкутутиспользуетсяименованныйreadErr,чтобыdeferмогмодифицироватьвозвращаемуюошибку
Когда возвращать ошибку, а когда вызывать panic
Всегда возвращайте ошибки в коде библиотек и везде, где ошибка ожидаема и её можно обработать отсутствиефайла,сетевыетаймауты,некорректныевходныеданныеотсутствие файла, сетевые таймауты, некорректные входные данныеотсутствиефайла,сетевыетаймауты,некорректныевходныеданные.panic следует использовать очень осторожно — только для ситуаций, которые:являются внутренними ошибками/нарушением инвариантов программныеошибки,которыенельзявосстановитьпрограммные ошибки, которые нельзя восстановитьпрограммныеошибки,которыенельзявосстановить, илиявно свидетельствуют о баге в программе например,невернонастроеннаяструктура,которойникогданедолжнобытьвнормальномсостояниинапример, неверно настроенная структура, которой никогда не должно быть в нормальном состояниинапример,невернонастроеннаяструктура,которойникогданедолжнобытьвнормальномсостоянии.Для CLI-приложений в main обычно возвращают ошибку/логируют её и завершают программу log.Fatalилиos.Exitlog.Fatal или os.Exitlog.Fatalилиos.Exit, а не бросают panic — это даёт более контролируемый вывод.Библиотеки не должны вызывать panic для обычных ошибок — это лишает пользователя библиотеки возможности обработать ошибку.
Примеры:
Возвращать ошибку: файл не найден, неверный параметр, сетевой таймаут — это нормальные runtime-ошибки.panic: обнаружен внутренний противоречивый state например,nilуказательтам,гдеAPIгарантируетненулевоезначениенапример, nil указатель там, где API гарантирует ненулевое значениенапример,nilуказательтам,гдеAPIгарантируетненулевоезначение, или неверно написанный тест/инициализация, где дальнейшая работа бессмысленна.Итого: ваш исходный код нужно исправить так, чтобы проверять ошибки от os.Open, не вызывать методы на потенциально nil, корректно закрывать файл и не терять ошибки от Close. В большинстве случаев лучше вернуть ошибку, а не вызывать panic; panic — для неизлечимых ошибок и нарушений инвариантов.