The Go Blog
Обработка ошибок и Go
Введение
Если вы писали какой-либо код на Go, вы, вероятно, уже сталкивались со встроенным типом error.
Код на Go использует значения error для обозначения ненормального состояния.
Например, функция os.Open возвращает непустое значение error, когда
не может открыть файл.
<code>func Open(name string) (file *File, err error) </code>
Следующий код использует os.Open для открытия файла.
Если возникает ошибка, вызывается log.Fatal для вывода сообщения об ошибке и завершения программы.
<code>f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
</code>
Вы можете достичь большого количества задач в Go, зная только это о типе error,
но в этой статье мы подробнее рассмотрим error и обсудим некоторые
хорошие практики обработки ошибок в Go.
Тип error
Тип error является интерфейсным типом. Переменная типа error представляет любое
значение, которое может описать себя в виде строки.
Вот объявление интерфейса:
<code>type error interface {
Error() string
}
</code>
Тип error, как и все встроенные типы,
предварительно объявлен
в блоке вселенной.
Наиболее часто используемая реализация error — это неэкспортируемый тип errorString из пакета errors.
<code>// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
</code>
Вы можете создать одно из этих значений с помощью функции errors.New.
Она принимает строку, преобразует её в errors.errorString и возвращает
как значение error.
<code>// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
</code>
Вот как можно использовать errors.New:
<code>func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}
</code>
Вызывающий код, передающий отрицательный аргумент в Sqrt, получает непустое значение error
(конкретная реализация — значение типа errors.errorString).
Вызывающий код может получить строку ошибки (“math:
square root of…”) путём вызова метода Error error,
или просто напечатать его:
<code>f, err := Sqrt(-1)
if err != nil {
fmt.Println(err)
}
</code>
Пакет fmt форматирует значение error, вызывая его метод Error() string.
Ответственность за форматирование ошибок лежит на реализации интерфейса error. Ошибка, возвращаемая функцией os.Open, форматируется как «open /etc/passwd: permission denied», а не просто как «permission denied». Ошибка, возвращаемая нашей функцией Sqrt, не содержит информации о недопустимом аргументе.
Чтобы добавить такую информацию, полезной функцией является fmt.Errorf из пакета fmt. Она форматирует строку согласно правилам Printf и возвращает её как error, созданный с помощью errors.New.
<code>if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}
</code>
Во многих случаях fmt.Errorf оказывается достаточным, но поскольку error — это интерфейс, вы можете использовать произвольные структуры данных в качестве значений ошибок, чтобы позволить вызывающим кодам проверять детали ошибки.
Например, наши гипотетические вызывающие функции могут захотеть получить недопустимый аргумент, переданный в Sqrt. Мы можем включить такую возможность, определив новую реализацию ошибки вместо использования errors.errorString:
<code>type NegativeSqrtError float64
func (f NegativeSqrtError) Error() string {
return fmt.Sprintf("math: square root of negative number %g", float64(f))
}
</code>
Сложный вызывающий код может затем использовать утверждение типа для проверки наличия NegativeSqrtError и обработки ошибки специальным образом, тогда как вызывающие функции, передающие ошибку в fmt.Println или log.Fatal, не заметят никаких изменений в поведении.
В качестве другого примера, пакет json определяет тип SyntaxError, который функция json.Decode возвращает при обнаружении синтаксической ошибки при разборе JSON-данных.
<code>type SyntaxError struct {
msg string // description of error
Offset int64 // error occurred after reading Offset bytes
}
func (e *SyntaxError) Error() string { return e.msg }
</code>
Поле Offset даже не отображается в стандартном форматировании ошибки, но вызывающие функции могут использовать его для добавления информации о файле и строке в свои сообщения об ошибках:
<code>if err := dec.Decode(&val); err != nil {
if serr, ok := err.(*json.SyntaxError); ok {
line, col := findLine(f, serr.Offset)
return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
}
return err
}
</code>
(Это немного упрощённая версия некоторых реальных функций из проекта Camlistore.)
Интерфейс error требует только метод Error; конкретные реализации ошибок могут иметь дополнительные методы. Например, пакет net возвращает ошибки типа error, следуя обычной конвенции, но некоторые реализации ошибок имеют дополнительные методы, определённые интерфейсом net.Error:
<code>package net
type Error interface {
error
Timeout() bool // Является ли ошибка таймаутом?
Temporary() bool // Является ли ошибка временной?
}
</code>
Код клиента может проверить наличие net.Error с помощью утверждения типа и затем отличить
временные сетевые ошибки от постоянных.
Например, веб-краулер может заснуть и повторить попытку при возникновении временной
ошибки, а в противном случае — прекратить попытки.
<code>if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
</code>
Упрощение повторяющейся обработки ошибок
В Go обработка ошибок имеет большое значение. Дизайн языка и принятые соглашения поощряют вас явно проверять ошибки там, где они возникают (в отличие от практики в других языках, где используются исключения и иногда перехватываются). В некоторых случаях это делает код на Go громоздким, но к счастью, существуют некоторые приемы, которые можно использовать для минимизации повторяющейся обработки ошибок.
Рассмотрим приложение App Engine с обработчиком HTTP, который извлекает запись из хранилища данных и форматирует её с помощью шаблона.
<code>func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
</code>
Эта функция обрабатывает ошибки, возвращаемые функцией datastore.Get
и методом Execute viewTemplate.
В обоих случаях она выводит простое сообщение об ошибке пользователю с HTTP
кодом состояния 500 ("Внутренняя ошибка сервера").
Кажется, что это небольшой объем кода,
но если добавить еще несколько HTTP-обработчиков, вы быстро получите множество копий
одинакового кода обработки ошибок.
Чтобы уменьшить повторение, можно определить собственный тип HTTP appHandler, который включает возвращаемое значение error:
<code>type appHandler func(http.ResponseWriter, *http.Request) error </code>
Затем мы можем изменить функцию viewRecord так, чтобы она возвращала ошибки:
<code>func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
</code>
Это проще, чем исходная версия,
но пакет http не понимает
функции, возвращающие error.
Чтобы исправить это, мы можем реализовать метод ServeHTTP
интерфейса http.Handler для appHandler:
<code>func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
</code>
Метод ServeHTTP вызывает функцию appHandler и отображает возвращённую ошибку (если таковая имеется) пользователю.
Обратите внимание, что_receiver_ метода, fn, является функцией.
(В Go это возможно!) Метод вызывает функцию, выполняя выражение fn(w, r).
Теперь, когда мы регистрируем viewRecord с помощью пакета http, мы используем функцию Handle
(вместо HandleFunc), поскольку appHandler является http.Handler
(а не http.HandlerFunc).
<code>func init() {
http.Handle("/view", appHandler(viewRecord))
}
</code>
С этой базовой инфраструктурой обработки ошибок мы можем сделать её более удобной для пользователя. Вместо того чтобы просто отображать строку ошибки, было бы лучше предоставить пользователю простое сообщение об ошибке с соответствующим HTTP-кодом состояния, при этом полная ошибка записывается в консоль разработчика App Engine для целей отладки.
Для этого мы создаём структуру appError, содержащую error и некоторые другие поля:
<code>type appError struct {
Error error
Message string
Code int
}
</code>
Далее мы изменяем тип appHandler так, чтобы он возвращал значения типа *appError:
<code>type appHandler func(http.ResponseWriter, *http.Request) *appError </code>
(Обычно является ошибкой возвращать конкретный тип ошибки вместо error,
по причинам, обсуждаемым в Go FAQ,
но в данном случае это правильный подход, потому что ServeHTTP — единственное место,
где видно значение и использует его содержимое.)
И делаем так, чтобы метод ServeHTTP типа appHandler отображал Message из appError
пользователю с корректным HTTP-кодом состояния Code и записывал полную Error
в консоль разработчика:
<code>func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e — это *appError, а не os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
</code>
Наконец, мы обновляем viewRecord до новой сигнатуры функции и заставляем её
возвращать больше контекста при возникновении ошибки:
<code>func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
</code>
Эта версия viewRecord имеет ту же длину, что и оригинальная,
но теперь каждая строка имеет конкретное значение, и мы предоставляем
более дружелюбный пользовательский опыт.
Это ещё не всё; можно дополнительно улучшить обработку ошибок в приложении. Некоторые идеи:
-
обеспечить обработчик ошибок красивым HTML-шаблоном,
-
облегчить отладку, записывая трассировку стека в HTTP-ответ, когда пользователь является администратором,
-
создать функцию-конструктор для
appError, которая сохраняет трассировку стека для более простой отладки, -
восстанавливаться после паник внутри
appHandler, записывая ошибку в консоль как «Critical», в то время как сообщать пользователю «произошла серьёзная ошибка». Это приятный трюк, позволяющий избежать того, чтобы пользователь сталкивался с непонятными сообщениями об ошибках, вызванными программистскими ошибками. См. статью Defer, Panic, and Recover для получения дополнительных сведений.
Заключение
Правильная обработка ошибок — это необходимое условие хорошего программного обеспечения. Применяя описанные в этой статье подходы, вы сможете писать более надёжный и лаконичный код на Go.
Следующая статья: Go для App Engine теперь доступен в общем использовании
Предыдущая статья: Функции первого класса в Go
Индекс блога