Учащиеся, которые писали на C, знают, что в языке C часто возвращается целочисленный код ошибки (errno), указывающий на ошибку обработки функции.-1чтобы указать на ошибку, используйте0значит правильно.
В то время как в Go мы используемerrortype для представления ошибки, но это уже не целочисленный тип, а тип интерфейса:
type error interface {
Error() string
}
Он представляет ошибки, которые можно объяснить одной строкой.
Наша наиболее часто используемаяerrors.New()функция, очень простая:
// src/errors/errors.go
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
Тип ошибки, созданный с помощью функции New, фактически не экспортируется в пакет ошибок.errorStringтип, который содержит только одно полеs, и реализует единственный метод:Error() string.
Обычно этого достаточно, это отражает «что-то пошло не так» в тот момент, но иногда нам нужна более конкретная информация, например:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
Когда вызывающий абонент находит ошибку, она только знает, что отрицательное число передается, и неясно, какое значение передается. Пойти:
It is the error implementation's responsibility to summarize the context.
Требуется вернуться к этой ошибке, чтобы предоставить конкретную «контекстную» информацию, то естьSqrtВ функции необходимо указать, что это за отрицательное число.
Итак, если вы найдетеfМеньше 0, ошибка должна быть возвращена следующим образом:
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
Это используетсяfmt.Errorfфункция, которая форматирует строку перед вызовомerrors.NewФункция создания ошибок.
Когда мы хотим узнать тип ошибки и распечатать ошибку, напечатайте ошибку напрямую:
fmt.Println(err)
или:
fmt.Println(err.Error)
fmtПакет вызывается автоматическиerr.Error()функция для печати строки.
Обычно мы ставим ошибку в конце возвращаемого значения функции, тут не о чем говорить, все так делают, это условность.
Ссылка [Tony Bai] В этой статье упоминалось, что при построении ошибки первая буква входящей строки должна быть строчной, а в конце не должно быть знаков препинания, потому что мы часто используем возвращаемую ошибку следующим образом:
... err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)
Дилемма ошибки
In Go, error handling is important. The language's design and conventions encourage you to explicitly check for errors where they occur (as distinct from the convention in other languages of throwing exceptions and sometimes catching them).
Обработка ошибок очень важна в Go. Это требует, чтобы мы явно обрабатывали ошибки, возникающие на уровне языка. Вместо того, чтобы использовать другие языки, такие как Java,try-catch- finallyЭта "фишка".
Это приводит к тому, что по всему коду летают «ошибки», что очень утомительно и долго.
И ради надежности кода каждую ошибку, возвращаемую функцией, мы не можем игнорировать. Поскольку при возникновении ошибки он, скорее всего, вернетnilтип объекта. Если ошибка не оценивается, следующая строка является правильной.nil100% операций над объектами приведут кpanic.
Таким образом, наиболее критикуемый язык Go заключается в том, что его метод обработки ошибок, кажется, возвращается к древней эпохе языка C.
rr := doStuff1()
if err != nil {
//handle error...
}
err = doStuff2()
if err != nil {
//handle error...
}
err = doStuff3()
if err != nil {
//handle error...
}
Расс Кокс, один из авторов Go, опроверг это мнение: вместо try-catch был выбран механизм обработки ошибок возвращаемого значения, главным образом учитывая, что первый подходит для крупномасштабного ПО, а второй больше подходит для небольших программ. .
Также упоминается в справочнике [Go FAQ],try-catchЭто может сделать код очень запутанным, и программисты склонны совершать некоторые распространенные ошибки, например:failing to open a file, также попадающие в исключения, что делает обработку ошибок более утомительной и подверженной ошибкам.
Многократно возвращаемое значение языка Go делает возврат ошибки необычным. Для настоящих исключений GO предоставляетpanic-recoverМеханизм, но и делает код выглядит очень просто.
Конечно, Расс Кокс также признает, что механизм обработки ошибок в Go действительно оказывает определенную психологическую нагрузку на разработчиков.
Ссылки [Является ли механизм обработки ошибок языка Go хорошим дизайном? ] — это ответ на Zhihu, объясняющий различные способы обработки ошибок и исключений в Go.Первый использует ошибку, а второй использует панику, что более выгодно, чем подход Java к исключениям из-за ошибок с одним горшком.
[Как изящно обрабатывать ошибки в Golang] дает несколько хороших примеров того, как обрабатывать ошибки в бизнесе.
попробуй сломать игру
Содержание этой части в основном взято из выступления Дэйва Чейни на GoCon 2016, а справочные материалы могут идти прямо к исходному тексту.
Часто можно услышать, что в Го много «мантр», которые очень легко произнести, но не очень легко понять, потому что у большинства из них есть истории. Например, мы часто говорим:
Don't communicating by sharing memory, share memory by communicating.
В газете тоже много цитировалось, очень интересно:
Давайте поговорим о трех «пословицах» об ошибках.
Errors are just values
Errors are just valuesФактический смысл в том, что до тех пор, пока реализацияErrorТипы интерфейсов можно рассматривать какError, важно понять смысл этих «притчей».
Способы обработки ошибок автор делит на три типа:
- Sentinel errors
- Error Types
- Opaque errors
Давайте поговорим один за другим. во-первыхSentinel errors, Sentinel исходит из словаря, обычно используемого в компьютерах, что означает «Sentry» на китайском языке. В прошлом при изучении быстрой взвода было бы «часовая», и другие элементы должны были сравнивать с «Sentry», который нарисовал линию.
здесьSentinel errorsНа самом деле я хочу сказать, что здесь есть ошибка, подразумевающая, что поток обработки больше не может продолжаться и должен здесь остановиться, что также является пределом. Эти ошибки часто оговариваются заранее.
Например,ioсумкаio.EOF, что указывает на ошибку "конец файла". Но этот метод не очень гибкий:
func main() {
r := bytes.NewReader([]byte("0123456789"))
_, err := r.Read(make([]byte, 10))
if err == io.EOF {
log.Fatal("read failed:", err)
}
}
должен судитьerrЭто согласованная ошибка?io.EOFравный.
Другой пример, когда я хочу вернуть ошибку и добавить некоторую контекстную информацию, это проблематично:
func main() {
err := readfile(“.bashrc”)
if strings.Contains(error.Error(), "not found") {
// handle error
}
}
func readfile(path string) error {
err := openfile(path)
if err != nil {
return fmt.Errorf(“cannot open file: %v", err)
}
// ……
}
существуетreadfileЕсли установлено, что err не является пустым в функции, используйте fmt.Errorf, чтобы добавить определенное значение перед errfileинформация, возвращаемая вызывающему абоненту. Возвращаемая ошибка на самом деле является строкой.
Когда возникают последствия, вызывающая сторона должна судить о базовой функции путем сопоставления строк.readfileЕсть ли какая-то ошибка. «Плохой запах» кода возникает, когда вы должны сделать это, чтобы обнаружить какую-то ошибку.
Кстати,err.Error()Методы предназначены для программистов, а не для кода, т. е. когда мы вызываемErrorметод, результат должен быть записан в файл или распечатан для просмотра программистом. В коде мы не можемerr.Error()сделать некоторые суждения, как указано вышеmainНехорошо делать это в функции.
Sentinel errorsСамая большая проблема заключается в том, что он создает зависимость между пакетом, который определяет ошибку, и пакетом, который ее использует. Например, судитьerr == io.EOFВы должны ввести пакет io, конечно, это стандартный пакет библиотеки, и все в порядке. Если многие определяемые пользователем пакеты определяют ошибки, то я должен ввести много пакетов, чтобы судить о различных ошибках. Здесь возникает проблема, которая может вызвать проблемы с циклическими ссылками.
Поэтому мы должны попытаться избежатьSentinel errors, хотя в стандартной библиотеке есть некоторые пакеты, которые это делают, подражать не рекомендуется.
ВторойError Types, что относится к реализацииerrorтакие типы интерфейсов. Важным преимуществом этого является то, что помимо ошибки, к типу могут быть присоединены другие поля, предоставляющие дополнительную информацию, например, количество строк с ошибкой и т. д.
В стандартной библиотеке есть очень хороший пример:
// PathError records an error and the operation and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}
PathErrorПуть к файлу и тип операции на момент возникновения ошибки регистрируются дополнительно.
Как правило, с таким типом ошибки внешний вызывающий объект должен использовать утверждение типа, чтобы определить ошибку:
// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
switch err := err.(type) {
case *PathError:
return err.Err
case *LinkError:
return err.Err
case *SyscallError:
return err.Err
}
return err
}
Но это неизбежно создает зависимость между неправильно определенным пакетом и неправильно используемым пакетом, возвращаясь к предыдущей проблеме.
Несмотря на тоError typesСравниватьSentinel errorsЛучше, потому что он может нести больше контекстной информации, но у него все еще есть проблема введения зависимостей пакетов. Поэтому тоже не рекомендуется. По крайней мере, не ставьтеError typesкак тип экспорта.
Последний,Opaque errors. В переводе это «ошибки черного ящика», потому что вы можете сказать, что произошла ошибка, но не можете увидеть, что внутри нее.
Например, следующий псевдокод:
func fn() error {
x, err := bar.Foo()
if err != nil {
return err
}
// use x
return nil
}
Как звонящий, после звонкаFooПосле функции просто знайтеFooНормально работает или есть проблемы. То есть вам нужно только определить, является ли err пустым, если нет, напрямую вернуть ошибку. В противном случае продолжайте нормальный процесс, который следует, не зная, что такое ошибка.
это обработкаOpaque errorsЭто тип неправильной стратегии.
Конечно, в некоторых случаях этого недостаточно. Например, в сетевом запросе вызывающий абонент должен определить, что тип ошибки, возвращаемой для решения того, чтобы повторить попытку. В этом случае автор дает метод:
In this case rather than asserting the error is a specific type or value, we can assert that the error implements a particular behaviour.
То есть вместо того, чтобы судить о типе ошибки, необходимо судить, имеет ли ошибка определенное поведение или реализует определенный интерфейс.
Вот пример:
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
После получения ошибки, возвращаемой сетевым запросом, вызовитеIsTemporaryфункция, если она возвращает true, попробуйте еще раз.
Преимущество этого заключается в том, что в пакете, выполняющем сетевой запрос, нет необходимостиimportСсылка на неправильно определенный пакет.
handle not just check errors
В этом разделе было бы сказано второе предложение Притчей: «Не просто проверяйте ошибки, обрабатывайте их изящно».
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}
Код в приведенном выше примере проблематичен, и его можно оптимизировать прямо в одно предложение:
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
Есть и другие проблемы, в самом верху цепочки вызовов функций мы получаем ошибку:No such file or directory.
Эта обратная связь об ошибке слишком мало информации, не знаю имя файла, путь, номер строки и так далее.
Попробуйте немного улучшить его, добавив немного контекста:
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return fmt.Errorf("authenticate failed: %v", err)
}
return nil
}
Эта практика на самом деле по ошибке преобразует его в строку, затем объединяет другую строку и, наконец, передаетfmt.ErrorfПревратить в ошибку. Это разрушает обнаружение выравнивания, то есть мы не можем судить, является ли ошибка предопределенной ошибкой.
План состоит в том, чтобы использовать стороннюю библиотеку:github.com/pkg/errors. Предоставляет дружественный интерфейс:
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error
пройти черезWrapОшибка и строка могут быть «обернуты» в новую ошибку.CauseОбратная операция может быть выполнена для восстановления ошибки во внутреннем слое.
С этими двумя функциями гораздо удобнее:
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
Это функция для чтения файла, сначала попробуйте открыть файл, если есть ошибка, она вернет сообщение об ошибке с прикрепленным сообщением «открыть не удалось»; после этого попробуйте прочитать файл, если есть ошибка, он вернет прикрепленное сообщение об ошибке «Ошибка чтения».
При вызове во внешний слойReadFileКогда функция:
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.Wrap(err, "could not read config")
}
Таким образом, мы можем напечатать такое сообщение об ошибке в основной функции:
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
Он многослойный и очень четкий. И если мы используемpkg/errorsФункции печати, предоставляемые библиотекой:
func main() {
_, err := ReadConfig()
if err != nil {
errors.Print(err)
os.Exit(1)
}
}
Вы можете получить более иерархические и подробные ошибки:
readfile.go:27: could not read config
readfile.go:14: open failed
open /Users/dfc/.settings.xml: no such file or directory
ВышеупомянутоеWrapФункция, давайте взглянем на функцию «причины», упомянутую ранее.temporaryИнтерфейс как пример:
type temporary interface {
Temporary() bool
}
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
te, ok := errors.Cause(err).(temporary)
return ok && te.Temporary()
}
использовать перед судомCauseУберите ошибку, сделайте утверждение и, наконец, рекурсивно вызовитеTemporaryфункция. Если ошибка не реализованаtemporaryинтерфейс, утверждение не выполняется и возвращаетсяfalse.
Only handle errors once
Что такое «обработка» ошибка:
Handling an error means inspecting the error value, and making a decision.
Это значит посмотреть на ошибку и принять решение.
Например, если решение не принято, ошибка игнорируется:
func Write(w io.Writer, buf []byte) {
w.Write(buf)
w.Write(buf)
}
w.Write(buf)Будет возвращено два результата: один указывает на количество успешно записанных байтов, а другой — на ошибку.Приведенный выше пример не выполняет никакой обработки для этих двух возвращаемых значений.
В следующем примере ошибка обрабатывается дважды:
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
// annotated error goes to log file
log.Println("unable to write:", err)
// unannotated error returned to caller
return err
return err
}
return nil
}
Первая обработка — запись ошибки в журнал, а вторая обработка — возврат ошибки вызывающей программе верхнего уровня. Вызывающий может также зарегистрировать ошибку или продолжить возврат на верхний уровень.
Таким образом, в файле журнала будет много повторяющихся описаний ошибок, и в глазах вызывающего объекта верхнего уровня (например, основной функции) ошибка, которую он получает, по-прежнему будет ошибкой, возвращаемой функцией нижнего уровня. без какой-либо контекстной информации.
Использование стороннего пакета ошибок может решить проблему идеально:
func Write(w io.Write, buf []byte) error {
_, err := w.Write(buf)
return errors.Wrap(err, "write failed")
}
Возвращаемые ошибки удобны для людей и машин.
резюме
В этой части в основном рассказывается о некоторых принципах обработки ошибок и представлен сторонний пакет ошибок, чтобы сделать обработку ошибок более элегантной.
Напоследок автор делает некоторые выводы:
- К ошибкам, таким как внешние API, нужно относиться серьезно.
- Относитесь к ошибкам как к черному ящику и оценивайте их поведение, а не тип.
- Старайтесь не использовать дозорные ошибки.
- Используйте сторонний пакет ошибок для переноса ошибок (errors.Wrap), чтобы упростить его использование.
- Используйте errors.Cause, чтобы получить основную ошибку.
Мертворожденное предложение попробовать
Были предложения улучшить процесс обработки ошибок с помощью ключевого слова «проверить и обработать» и «встроенной функции try». В настоящее время предложение по встроенной функции try было официально отклонено заранее из-за подавляющего сопротивления. в обществе.
Конкретное содержание этих двух предложений см. в справочных материалах [проверить и обработать] и [попробовать предложение].
Улучшения в версии 1.13
В языке Go есть несколько неудачных попыток, таких как вендор и внутренний, введенные в Go 1.5 для управления пакетами, которые в конечном итоге стали объектом злоупотреблений и вызвали множество проблем. Итак, Go 1.13 просто отбросил его.GOPATHиvendorособенность, используйте вместо этогоmoduleдля управления пакетами.
Чай Да сказал в статье «Язык го существует уже десять лет, Go2 готов к работе»:
Например, недавняя заявка Роберта Гриземера, одного из отцов языка Go, об упрощении обработки ошибок с помощью встроенной функции try была отклонена. Неудачная попытка — хороший знак, это означает, что язык Go все еще пробует себя в некоторых новых областях — язык Go все еще активен.
3 сентября этого года Go выпустила версию 1.13.В дополнение к функции положительного модуля, она также улучшила числовой литерал. Что еще более важно, defer повышает производительность на 30 %, перемещает больше объектов из кучи в стек для повышения производительности и так далее.
Также значительно улучшена стандартная библиотека ошибок. В библиотеку ошибок добавлены три функции Is/As/Unwrap, которые будут использоваться для поддержки повторной упаковки ошибок и обработки идентификации в рамках подготовки к новым улучшениям обработки ошибок в Go 2.
1.13поддерживаетсяerrorУпаковка:
An error e can wrap another error w by providing an Unwrap method that returns w. Both e and w are available to programs, allowing e to provide additional context to w or to reinterpret it while still allowing programs to make decisions based on w.
Для поддержки упаковки,fmt.Errorfвырос%wформате и вerrorПакет добавляет три функции:errors.Unwrap,errors.Is,errors.As.
fmt.Errorf
использоватьfmt.Errorfплюс%wформатер для генерации вложенной ошибки, которая неpkg/errorsИспользование функции Wrap для вложения ошибок таким образом очень лаконично.
Unwrap
func Unwrap(err error) error
Разобрать вложенную ошибку, нужно вызвать многоуровневую вложенностьUnwrapФункция несколько раз, чтобы получить самую внутреннюю ошибку.
Исходный код выглядит следующим образом:
func Unwrap(err error) error {
// 判断是否实现了 Unwrap 方法
u, ok := err.(interface {
Unwrap() error
})
// 如果不是,返回 nil
if !ok {
return nil
}
// 调用 Unwrap 方法返回被嵌套的 error
return u.Unwrap()
}
Утвердить при ошибке, чтобы увидеть, реализует ли он метод Unwrap, и если да, то вызвать его метод Unwrap. В противном случае возвращает ноль.
Is
func Is(err, target error) bool
Определите, имеет ли err тот же тип, что и цель, или вложенная ошибка err имеет тот же тип, что и цель, и если да, верните true.
Исходный код выглядит следующим образом:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
// 无限循环,比较 err 以及嵌套的 error
for {
if isComparable && err == target {
return true
}
// 调用 error 的 Is 方法,这里可以自定义实现
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 返回被嵌套的下一层的 error
if err = Unwrap(err); err == nil {
return false
}
}
}
Через бесконечный цикл используйтеUnwrapПродолжайте распаковывать вложенные ошибки в err, проверьте, реализует ли распакованная ошибка метод Is, и вызовите его метод Is.Когда оба возвращают true, вся функция возвращает true.
As
func As(err error, target interface{}) bool
Найдите эквивалент target из цепочки ошибок err и установите переменную, на которую указывает target.
Исходный код выглядит следующим образом:
func As(err error, target interface{}) bool {
// target 不能为 nil
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
// target 必须是一个非空指针
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
// 保证 target 是一个接口类型或者实现了 Error 接口
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
// 使用反射判断是否可被赋值,如果可以就赋值并且返回true
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// 调用 error 自定义的 As 方法,实现自己的类型断言代码
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 不断地 Unwrap,一层层的获取嵌套的 error
err = Unwrap(err)
}
return false
}
Возвращает true, если err в цепочке ошибок можно присвоить переменной, на которую указывает target; или если err реализуетAs(interface{}) boolметод возвращает истину.
В первом случае err будет присвоено переменной, на которую указывает target, во втором эта функция предоставляется функцией As.
Если target не является ненулевым указателем на «тип, который реализует интерфейс ошибки или другой тип интерфейса», функция вызывает панику.
Содержание этой части лучше написано в статье Feixue Ruthless Big Brother [Feixue Ruthless Analysis 1.13 Errors], и ее рекомендуется прочитать.
Суммировать
Очень хорошо использовать error и panic для обработки ошибок и исключений в языке Go, что относительно понятно. Что касается того, использовать ли ошибку или панику, это зависит от конкретного бизнес-сценария.
Конечно, ошибки в Go слишком просты, чтобы записывать много контекстной информации, и нет лучшего способа оборачивать ошибки. Конечно, они могут быть решены сторонними библиотеками. Официальный также внес улучшения в эту часть в только что выпущенном go 1.13, и я полагаю, что в Go 2 будут дальнейшие оптимизации.
В этой статье также перечислены некоторые примеры обработки ошибок, например, не обрабатывать ошибку дважды, оценивать поведение ошибки, а не ее тип и т. д.
В справочниках есть много примеров обработки ошибок, и эта статья служит введением.
использованная литература
[Предложение об ошибке Go 2]go.Googlesource.com/proposal/+/…
【проверить и обработать】go.Googlesource.com/proposal/+/…
[Неправильно обсуждаемый вопрос]GitHub.com/golang/go/i…
【Часто задаваемые вопросы о значении ошибки】GitHub.com/gowaves/go/me…
【пакет ошибок】golang.org/pkg/errors/
【Беспощадная обработка ошибок блога Blood Snow】woohoo.fly snow.org/2019/01/01/…
【Ошибка безжалостного анализа 1.13 Blood Snow】woohoo.fly snow.org/2019/09/06/…
【Обработка языковых ошибок Тони Бай Го】Тони Пендулум.com/2015/10/30/…
[Перейти к официальному руководству по ошибкам]blog.golang.org/error-handan дорога…
【Перейти к часто задаваемым вопросам】golang.org/doc/initiated#сумма разница…
[обработка ошибок ethancai]Итан Кай.GitHub.IO/2017/12/29/…
【Выступление Дэйва Чейни на GoCon 2016】Dave.Cheney.net/Passat/go con…
【Блог Морсинга Эффективная обработка ошибок в Go】По умолчанию используется машина.Откройте /error-Handan Road…
[Как изящно обрабатывать ошибки в Golang]Woohoo.ITU ring.com.capable/article/508…
[Предложение по обработке ошибок Go 2: попробовать или проверить? 】Headlines.IO/posts/uh9 давай, о…
【попробовать предложение】GitHub.com/golang/go/i…
【Отклонить предложение попробовать】GitHub.com/golang/go/i…
[Является ли механизм обработки ошибок языка Go хорошим дизайном? 】Ууху. Call.com/question/27…