Вывод ошибки golang

  1. Documentation

  2. Tutorials

  3. Return and handle an error

Handling errors is an essential feature of solid code. In this section, you’ll
add a bit of code to return an error from the greetings module, then handle it
in the caller.

  1. In greetings/greetings.go, add the code highlighted below.

    There’s no sense sending a greeting back if you don’t know who to greet.
    Return an error to the caller if the name is empty. Copy the following
    code into greetings.go and save the file.

    package greetings
    
    import (
        "errors"
        "fmt"
    )
    
    // Hello returns a greeting for the named person.
    func Hello(name string) (string, error) {
        // If no name was given, return an error with a message.
        if name == "" {
            return "", errors.New("empty name")
        }
    
        // If a name was received, return a value that embeds the name
        // in a greeting message.
        message := fmt.Sprintf("Hi, %v. Welcome!", name)
        return message, nil
    }
    

    In this code, you:

    • Change the function so that it returns two values: a
      string and an error. Your caller will check
      the second value to see if an error occurred. (Any Go function can
      return multiple values. For more, see
      Effective Go.)
    • Import the Go standard library errors package so you can
      use its
      errors.New function.
    • Add an if statement to check for an invalid request (an
      empty string where the name should be) and return an error if the
      request is invalid. The errors.New function returns an
      error with your message inside.
    • Add nil (meaning no error) as a second value in the
      successful return. That way, the caller can see that the function
      succeeded.
  2. In your hello/hello.go file, handle the error now returned by the
    Hello function, along with the non-error value.

    Paste the following code into hello.go.

    package main
    
    import (
        "fmt"
        "log"
    
        "example.com/greetings"
    )
    
    func main() {
        // Set properties of the predefined Logger, including
        // the log entry prefix and a flag to disable printing
        // the time, source file, and line number.
        log.SetPrefix("greetings: ")
        log.SetFlags(0)
    
        // Request a greeting message.
        message, err := greetings.Hello("")
        // If an error was returned, print it to the console and
        // exit the program.
        if err != nil {
            log.Fatal(err)
        }
    
        // If no error was returned, print the returned message
        // to the console.
        fmt.Println(message)
    }
    

    In this code, you:

    • Configure the
      log package to
      print the command name («greetings: «) at the start of its log messages,
      without a time stamp or source file information.
    • Assign both of the Hello return values, including the
      error, to variables.
    • Change the Hello argument from Gladys’s name to an empty
      string, so you can try out your error-handling code.
    • Look for a non-nil error value. There’s no sense continuing
      in this case.
    • Use the functions in the standard library’s log package to
      output error information. If you get an error, you use the
      log package’s
      Fatal function
      to print the error and stop the program.
  3. At the command line in the hello directory, run hello.go to
    confirm that the code works.

    Now that you’re passing in an empty name, you’ll get an error.

    $ go run .
    greetings: empty name
    exit status 1
    

That’s common error handling in Go: Return an error as a value so the caller
can check for it.

Next, you’ll use a Go slice to return a randomly-selected greeting.

< Call your code from another module
Return a random greeting >

Привет, хабровчане! Уже сегодня в ОТУС стартует курс «Разработчик Golang» и мы считаем это отличным поводом, чтобы поделиться еще одной полезной публикацией по теме. Сегодня поговорим о подходе Go к ошибкам. Начнем!

Освоение прагматической обработки ошибок в вашем Go-коде

Этот пост является частью серии «Перед тем как приступать к Go», где мы исследуем мир Golang, делимся советами и идеями, которые вы должны знать при написании кода на Go, чтобы вам не пришлось набивать собственные шишки.

Я предполагаю, что у вас уже имеется хотя бы базовый опыт работы с Go, но если вы чувствуете, что в какой-то момент вы столкнулись с незнакомым обсуждаемым материалом, не стесняйтесь делать паузу, исследовать тему и возвращаться.

Теперь, когда мы расчистили себе путь, поехали!

Подход Go к обработке ошибок — одна из самых спорных и неправильно используемых фич. В этой статье вы узнаете подход Go к ошибкам, и поймете, как они работают “под капотом”. Вы изучите несколько различных подходов, рассмотрите исходный код Go и стандартную библиотеку, чтобы узнать, как обрабатываются ошибки и как с ними работать. Вы узнаете, почему утверждения типа (Type Assertions) играют важную роль в их обработке, и увидите предстоящие изменения в обработке ошибок, которые планируется ввести в Go 2.

Вступление

Сперва-наперво: ошибки в Go не являются исключениями. Дэйв Чейни написал эпический блогпост об этом, поэтому я отсылаю вас к нему и резюмирую: на других языках вы не можете быть уверены, может ли функция вызвать исключение или нет. Вместо генерации исключений функции Go поддерживают множественные возвращаемые значения, и по соглашению эта возможность обычно используется для возврата результата функции наряду с переменной ошибки.

Если по какой-то причине ваша функция может дать сбой, вам, вероятно, следует вернуть из нее предварительно объявленный error-тип. По соглашению, возврат ошибки сигнализирует вызывающей стороне о проблеме, а возврат nil не считается ошибкой. Таким образом, вы дадите вызывающему понять, что возникла проблема, и ему нужно разобраться с ней: кто бы ни вызвал вашу функцию, он знает, что не должен полагаться на результат до проверки на наличие ошибки. Если ошибка не nil, он обязан проверить ее и обработать (логировать, возвращать, обслуживать, вызвать какой-либо механизм повторной попытки/очистки и т. д.).


(3 // обработка ошибки
5 // продолжение)

Эти фрагменты очень распространены в Go, и некоторые рассматривают их в качестве шаблонного кода. Компилятор рассматривает неиспользуемые переменные как ошибки компиляции, поэтому, если вы не собираетесь проверять наличие ошибок, вы должны назначить их пустому идентификатору. Но как бы удобно это ни было, ошибки не следует игнорировать.


( 4 // игнорирование ошибок небезопасно, и вы не должны полагаться на результат прежде, чем проверите наличие ошибок)
результату нельзя доверять до проверки на наличие ошибок

Возврат ошибки вместе с результатами, наряду со строгой системой типов Go, значительно усложняет написание забагованного кода. Вы всегда должны полагать, что значение функции повреждено, если только вы не проверили ошибку, которую она возвратила, и, присваивая ошибку пустому идентификатору, вы явно игнорируете, что значение вашей функции может быть повреждено.


Пустой идентификатор темен и полон ужасов.

У Go действительно есть panic и recover механизмы, которые также описаны в другом подробном посте в блоге Go. Но они не предназначены для имитации исключений. По словам Дейва, «Когда вы паникуете в Go — вы действительно паникуете: это не проблема кого-то другого, это уже геймовер». Они фатальны и приводят к сбою в вашей программе. Роб Пайк придумал поговорку «Не паникуйте», которая говорит сама за себя: вам, вероятно, следует избегать эти механизмы и вместо них возвращать ошибки.

«Ошибки — значения».
«Не просто проверяйте наличие ошибок, а элегантно их обрабатывайте»
«Не паникуйте»
все поговорки Роба Пайка

Под капотом

Интерфейс ошибки

Под капотом тип error — это простой интерфейс с одним методом, и если вы с ним не знакомы, я настоятельно рекомендую просмотреть этот пост в официальном блоге Go.


интерфейс error из исходного кода

Свои собственные ошибки реализовать не сложно. Существуют различные подходы к пользовательским структурам, реализующим метод Error() string . Любая структура, реализующая этот единственный метод, считается допустимым значением ошибки и может быть возвращена как таковая.

Давайте рассмотрим несколько таких подходов.

Встроенная структура errorString

Наиболее часто используемая и широко распространенная реализация интерфейса ошибок — это встроенная структура errorString . Это самая простая реализация, о которой вы только можете подумать.


Источник: исходный код Go

Вы можете лицезреть ее упрощенную реализацию здесь. Все, что она делает, это содержит string, и эта строка возвращается методом Error. Эта стринговая ошибка может быть нами отформатирована на основе некоторых данных, скажем, с помощью fmt.Sprintf. Но кроме этого, она не содержит никаких других возможностей. Если вы применили errors.New или fmt.Errorf, значит вы уже использовали ее.


(13// вывод:)

попробуйте

github.com/pkg/errors

Другой простой пример — пакет pkg/errors. Не путать со встроенным пакетом errors, о котором вы узнали ранее, этот пакет предоставляет дополнительные важные возможности, такие как обертка ошибок, развертка, форматирование и запись стек-трейса. Вы можете установить пакет, запустив go get github.com/pkg/errors.

В тех случаях, когда вам нужно прикрепить стек-трейс или необходимую информацию об отладке к вашим ошибкам, использование функций New или Errorf этого пакета предоставляет ошибки, которые уже записываются в ваш стек-трейс, и вы так же можете прикрепить простые метаданные, используя его возможности форматирования. Errorf реализует интерфейс fmt.Formatter, то есть вы можете отформатировать его, используя руны пакета fmt ( %s, %v, %+v и т. д.).


(//6 или альтернатива)

Этот пакет также представляет функции errors.Wrap и errors.Wrapf. Эти функции добавляют контекст к ошибке с помощью сообщения и стек-трейса в том месте, где они были вызваны. Таким образом, вместо простого возврата ошибки, вы можете обернуть ее контекстом и важными отладочными данными.

Обертки ошибок другими ошибками поддерживают Cause() error метод, который возвращает их внутреннюю ошибку. Кроме того, они могут использоваться сerrors.Cause(err error) error функцией, которая извлекает основную внутреннюю ошибку в оборачивающей ошибке.

Работа с ошибками

Утверждение типа

Утверждения типа (Type Assertion) играют важную роль при работе с ошибками. Вы будете использовать их для извлечения информации из интерфейсного значения, а поскольку обработка ошибок связана с пользовательскими реализациями интерфейса error, реализация утверждений на ошибках является очень удобным инструментом.

Его синтаксис одинаков для всех его целей —  x.(T), если  x имеет тип интерфейса.  x.(T) утверждает, что  x не равен nil и что значение, хранящееся в x, относится к типу T. В следующих нескольких разделах мы рассмотрим два способа использования утверждений типа — с конкретным типом T и с интерфейсом типа T.


(2//сокращенный синтаксис, пропускающий логическую переменную ok
3//паника: преобразование интерфейса: интерфейс {} равен nil, а не string
6//удлиненный синтаксис с логической переменной ok
8//не паникует, вместо этого присваивает ok false, когда утверждение ложно
9// теперь мы можем безопасно использовать s как строку)

песочница: panic при укороченном синтаксисе, безопасный удлинённый синтаксис

Дополнительное примечание, касающееся синтаксиса: утверждение типа может использоваться как с укороченным синтаксисом (который паникует при неудачном утверждении), так и с удлиненным синтаксисом (который использует логическое значение OK для указания успеха или неудачи). Я всегда рекомендую брать удлиненный вместо укороченного, так как я предпочитаю проверять переменную OK, а не разбираться с паникой.

Утверждение с интерфейсом типа T

Выполнение утверждения типа x.(T) с интерфейсом типа T подтверждает, что x реализует интерфейс T. Таким образом, вы можете гарантировать, что интерфейсное значение реализует интерфейс, и только если это так, вы сможете использовать его методы.


(5…// утверждаем, что x реализует интерфейс resolver
6…// здесь мы уже можем безопасно использовать этот метод)

Чтобы понять, как это можно использовать, давайте снова взглянем на pkg/errors. Вы уже знаете этот пакет ошибок, так что давайте углубимся в errors.Cause(err error) error функцию.

Эта функция получает ошибку и извлекает самую внутреннюю ошибку, которую та переносит (ту, которая уже не служит оберткой для другой ошибки). Это может показаться примитивным, но есть много замечательных вещей, которые вы можете извлечь из этой реализации:


источник: pkg/errors

Функция получает значение ошибки, и она не может предполагать, что получаемый ею err аргумент является ошибкой-оберткой (поддерживаемой Cause методом). Поэтому перед вызовом метода Cause необходимо убедиться, что вы имеете дело с ошибкой, которая реализует этот метод. Выполняя утверждение типа в каждой итерации цикла for, вы можете убедиться, что cause переменная поддерживает метод Cause, и может продолжать извлекать из него внутренние ошибки до тех пор, пока не найдете ошибку, у которой нет Cause.

Создавая простой локальный интерфейс, содержащий только те методы, которые вам нужны, и применяя на нем утверждение, ваш код отделен от других зависимостей. Полученный вами аргумент не обязательно должен быть известной структурой, он просто должен быть ошибкой. Любой тип, реализующий методы Error и Cause, подойдет. Таким образом, если вы реализуете Cause метод в своем типе ошибки, вы можете использовать эту функцию с ним без замедлений.

Однако следует помнить об одном небольшом недостатке: интерфейсы могут изменяться, поэтому вам следует тщательно поддерживать код, чтобы ваши утверждения не нарушались. Не забудьте определить свои интерфейсы там, где вы их используете, поддерживать их стройными и аккуратными, и у вас все будет хорошо.

Наконец, если вам нужен только один метод, иногда удобнее сделать утверждение на анонимном интерфейсе, содержащем только метод, на который вы полагаетесь, т. е. v, ok := x.(interface{ F() (int, error) }). Использование анонимных интерфейсов может помочь отделить ваш код от возможных зависимостей и защитить его от возможных изменений в интерфейсах.

Утверждение с конкретным типом T и Type Switch

Я предваряю этот раздел введением двух похожих шаблонов обработки ошибок, которые страдают от нескольких недостатков и ловушек. Это не значит, что они не распространенные. Оба они могут быть удобными инструментами в небольших проектах, но они плохо масштабируются.

Первый — это второй вариант утверждения типа: выполняется утверждение типа x.(T) с конкретным типом T. Он утверждает, что значение x имеет тип T, или оно может быть преобразовано в тип T.


(2//мы можем использовать v как mypkg.SomeErrorType)

Другой — это шаблон Type Switch. Type Switch объединяют оператор switch с утверждением типа, используя зарезервированное ключевое слово type. Они особенно распространены в обработке ошибок, где знание основного типа переменной ошибки может быть очень полезным.


(3// обработка…
5// обработка…)

Большим недостатком обоих подходов является то, что оба они приводят к связыванию кода со своими зависимостями. Оба примера должны быть знакомы со структурой SomeErrorType (которая, очевидно, должна быть экспортирована) и должны импортировать пакет mypkg.
В обоих подходах при обработке ваших ошибок вы должны быть знакомы с типом и импортировать его пакет. Ситуация усугубляется, когда вы имеете дело с ошибками в обертках, где причиной ошибки может быть ошибка, возникшая из-за внутренней зависимости, о которой вы не знаете и не должны знать.


(7// обработка…
9// обработка…)

Type Switch различают *MyStruct и MyStruct. Поэтому, если вы не уверены, имеете ли вы дело с указателем или фактическим экземпляром структуры, вам придется предоставить оба варианта. Более того, как и в случае с обычными switch, кейсы в Type Switch не проваливаются, но в отличие от обычных Type Switch, использование fallthrough запрещено в Type Switch, поэтому вам придется использовать запятую и предоставлять обе опции, что легко забыть.

Подытожим

Вот и все! Теперь вы знакомы с ошибками и должны быть готовы к устранению любых ошибок, которые ваше приложение Go может выбросить (или фактически вернуть) на ваш путь!
Оба пакета errors представляют простые, но важные подходы к ошибкам в Go, и, если они удовлетворяют вашим потребностям, они являются отличным выбором. Вы можете легко реализовать свои собственные структуры ошибок и пользоваться преимуществами обработки ошибок Go, комбинируя их с pkg/errors.

Когда вы масштабируете простые ошибки, правильное использование утверждений типа может быть отличным инструментом для обработки различных ошибок. Либо с помощью Type Switch, либо путем утверждения поведения ошибки и проверки интерфейсов, которые она реализует.

Что дальше?

Обработка ошибок в Go сейчас очень актуальна. Теперь, когда вы получили основы, вам может быть интересно, что ждет нас в будущем для обработки ошибок Go!

В следующей версии Go 2 этому уделяется много внимания, и вы уже можете взглянуть на черновой вариант. Кроме того, во время dotGo 2019 Марсель ван Лохуизен провел отличную беседу на тему, которую я просто не могу не рекомендовать — «Значения ошибок GO 2 уже сегодня».

Очевидно, есть еще множество подходов, советов и хитростей, и я не могу включить их все в один пост! Несмотря на это, я надеюсь, что вам он понравился, и я увижу вас в следующем выпуске серии «Перед тем как приступать к Go»!

А теперь традиционно ждем ваши комментарии.

2 сентября, 2019 12:13 пп
1 272 views
| Комментариев нет

Cloud Server

Устойчивый и надежный код должен уметь правильно реагировать на неожиданные обстоятельства: например, на неправильный пользовательский ввод, сетевые сбои и ошибки диска. Обработка ошибок – это процесс обнаружения непредсказуемых состояний и записи диагностической информации для дальнейшего устранения неполадок.

В отличие от других языков, где ошибки обрабатываются разработчиками с помощью специального синтаксиса, ошибки в Go – это значения, которые относятся к типу данных error. Они используются функциями, как и остальные типы данных. Чтобы обработать ошибки в Go, нужно изучить ошибки, которые могут возвращать функции, решить, произошла ли ошибка, принять надлежащие меры для защиты данных и сообщить пользователям или операторам, что произошла ошибка.

Создание ошибок

Прежде чем мы сможем обрабатывать ошибки, нужно сначала их создать. В Go  есть стандартная библиотека, которая предоставляет две встроенные функции для создания ошибок: errors.New и fmt.Errorf. Обе эти функции позволяют указывать пользовательское сообщение об ошибке, которое вы можете позже представить своим пользователям.

Функция errors.New принимает один аргумент – сообщение об ошибке в виде строки, оно позволяет вам предупредить своих пользователей о том, что пошло не так.

Попробуйте запустить следующий пример кода, чтобы увидеть в стандартном выводе ошибку, созданную errors.New:

package main
import (
"errors"
"fmt"
)
func main() {
err := errors.New("barnacles")
fmt.Println("An error occurred:", err)
}
An error occurred: barnacles

Мы использовали функцию errors.New из стандартной библиотеки, чтобы создать новое сообщение об ошибке со строкой “barnacles” в качестве сообщения об ошибке. При этом мы следовали соглашению, используя строчные буквы, как предлагает руководство по стилю Go.

Затем мы использовали функцию fmt.Println, чтобы объединить наше сообщение об ошибке со строкой «An error occurred:».

Функция fmt.Errorf позволяет динамически создавать сообщения об ошибке. Ее первым аргументом является строка, содержащая ваше сообщение об ошибке со значениями-заполнителями: %s для строки и %d для целого числа. fmt.Errorf интерполирует аргументы, следующие за этой строкой, в указанные заполнители в таком порядке:

package main
import (
"fmt"
"time"
)
func main() {
err := fmt.Errorf("error occurred at: %v", time.Now())
fmt.Println("An error happened:", err)
}
An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

Мы использовали функцию fmt.Errorf для создания сообщения об ошибке, которое будет указывать текущее время. Строка форматирования, которую мы предоставили fmt.Errorf, содержит директиву %v, которая задает форматирование по умолчанию для первого аргумента после строки. Этим аргументом будет текущее время, предоставленное функцией time.Now из стандартной библиотеки. Как и в предыдущем примере, мы объединяем сообщение об ошибке с коротким префиксом и выводим результат в стандартный вывод, используя функцию fmt.Println.

Обработка ошибок

Как правило, ошибки, созданные так, как в предыдущем примере – без причины – не используются. На практике гораздо чаще ошибка создается и возвращается из функции, когда что-то идет не так. Вызывающие эту функцию затем используют оператор if, чтобы узнать, была создана ошибка или ноль – неинициализированное значение.

Следующий пример включает функцию, которая всегда возвращает ошибку. Обратите внимание, что при запуске программы она выдает тот же результат, что и в предыдущем примере, хотя функция на этот раз возвращает ошибку. Объявление ошибки в другом месте не изменяет сообщения об ошибке.

package main
import (
"errors"
"fmt"
)
func boom() error {
return errors.New("barnacles")
}
func main() {
err := boom()
if err != nil {
fmt.Println("An error occurred:", err)
return
}
fmt.Println("Anchors away!")
}
An error occurred: barnacles

Здесь мы определили функцию boom(), которая возвращает единичную ошибку, созданную с помощью error.New. Затем мы вызвали эту функцию и зафиксировали ошибку с помощью строки err := boom().

Как только мы присваиваем эту ошибку, мы проверяем, присутствовала ли она с условием if err != nil . Здесь условное выражение всегда будет иметь значение true, поскольку мы всегда возвращаем error из boom().

Но это не всегда так работает, поэтому рекомендуем использовать логику для обработки случаев, когда ошибка отсутствует (nil), и случаев, когда ошибка присутствует. Когда ошибка есть, мы используем fmt.Println, чтобы вывести ее вместе с префиксом, как в предыдущих примерах. В конце оператор return позволяет пропустить выполнение fmt.Println(“Anchors away!”) – эта функция должна выполняться только тогда, когда ошибок нет.

Примечание: Конструкция if err != nil , показанная в последнем примере, является основным компонентом обработки ошибок на Go. Везде, где функция может вызвать ошибку, важно использовать оператор if, чтобы проверить, произошла ли она. Таким образом идиоматический код Go поддерживает логику “happy path” на первом уровне отступа и логику “sad path” на втором уровне.

Операторы if имеют опциональный оператор присваивания, который можно использовать для упрощения вызова функции и обработки ошибок.

Запустите следующую программу, и вы получите тот же вывод, что и в предыдущем примере. Но на этот раз  мы используем составной оператор if, чтобы сократить шаблон:

package main
import (
"errors"
"fmt"
)
func boom() error {
return errors.New("barnacles")
}
func main() {
if err := boom(); err != nil {
fmt.Println("An error occurred:", err)
return
}
fmt.Println("Anchors away!")
}
An error occurred: barnacles

Как и раньше, у нас есть функция boom(), которая всегда возвращает ошибку. Мы присваиваем ошибку из boom(), в качестве первой части оператора if. Во второй части оператора после точки с запятой переменная err становится доступной. Мы проверяем, присутствует ли ошибка, и выводим ошибку с короткой префиксной строкой, как и раньше.

Возврат ошибок вместе со значениями

Функции, которые возвращают единичную ошибку, часто влияют на некоторые stateful изменения (такие как вставка строк в базу данных). Также часто используются функции, которые возвращают значение в случае успешного выполнения и потенциальную ошибку в обратном случае. Go позволяет функциям возвращать более одного результата, который можно использовать для одновременного возврата значения и ошибки.

Чтобы создать функцию, которая возвращает более одного значения, нужно перечислить все типы возвращаемых значений в скобках в сигнатуре функции. Например, функция capitalize, которая возвращает string и error, будет объявлена ​​с помощью func capitalize(name string) (string, error) {}. Часть (string, error) сообщает компилятору Go, что эта функция выводит string и error в указанном порядке.

Запустите эту программу, чтобы получит вывод функции, которая может выводить string и error.

package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
name, err := capitalize("myname")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Capitalized name:", name)
}
Capitalized name: MYNAME

Мы определяем capitalize() как функцию, которая принимает строку (имя, которое нужно записать заглавными буквами) и возвращает значение строки и ошибки. В main() мы вызываем capitalize() и присваиваем переменным name и err два значения, возвращаемых функцией,  разделяя их запятыми в левой части оператора :=. После этого мы выполняем проверку if err != nil , как в предыдущих примерах, и выводим ошибку в стандартный вывод, используя fmt.Println, если ошибка есть. Если ошибки не было, Программа выведет Capitalized name: MYNAME.

Попробуйте заменить строку “myname” в name, err := capitalize(“myname”) на пустую строку (“”), и вы получите сообщение об ошибке.

Could not capitalize: no name provided

Функция capitalize вернет ошибку, если вызывающие функции задают для параметра name пустую строку. Когда параметр name не является пустой строкой, capitalize() использует strings.ToTitle, чтобы записать заглавными буквами параметр name, и возвращает nil в качестве значения ошибки.

Есть несколько соглашений, которым следует этот пример. Они типичны для кода Go, но не соблюдаются компилятором. Когда функция возвращает несколько значений, включая ошибку, соглашение требует, чтобы мы вернули error как последний элемент. При возврате error из функции с несколькими значениями идиоматический код Go также установит нулевое значение для каждого значения, не являющегося ошибкой. Нулевыми значениями являются, например, пустая строка для строчных типов, 0 для целых чисел, пустая структура для структурных типов и nil для интерфейсов.

Читайте также: Переменные и константы в Go

 Уменьшение шаблона

Соблюдать эти соглашения сложно в ситуациях, когда из функции нужно вернуть много значений. Мы можем использовать анонимную функцию, чтобы уменьшить шаблон. Анонимные функции – это процедуры, присваиваемые переменным. В отличие от функций, которые мы определили в предыдущих примерах, они доступны только в функциях, в которых вы их объявляете – потому их очень удобно использовать в качестве коротких фрагментов многократно используемой вспомогательной логики.

Следующая программа модифицирует последний пример, задавая длину имени, которое нужно написать заглавными буквами. Так как пример выводит три значения, без анонимной функции обработка ошибок может стать сложной:

package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, int, error) {
handle := func(err error) (string, int, error) {
return "", 0, err
}
if name == "" {
return handle(errors.New("no name provided"))
}
return strings.ToTitle(name), len(name), nil
}
func main() {
name, size, err := capitalize("myname")
if err != nil {
fmt.Println("An error occurred:", err)
}
fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
Capitalized name: MYNAME, length: 6

В main()теперь собрано три возвращаемых аргумента из capitalize: name, size и err соответственно. Затем мы проверяем, вернула ли capitalize ошибку – смотрим, была ли err равна nil. Это важно сделать, прежде чем пытаться использовать какие-либо другие значения, возвращаемые capitalize, потому что анонимная функция handle может установить для них нулевые значения. Поскольку ошибки не произошло, так как мы указали строку “myname”, мы получаем имя заглавными буквами и его длину.

Тут вы снова можете попробовать изменить “myname” на пустую строку (“”), чтобы увидеть  ошибку:

An error occurred: no name provided

В рамках capitalize мы определяем переменную handle как анонимную функцию. Она принимает единичную ошибку и возвращает идентичные значения в том же порядке, что и capitalize. Переменная handle приравнивает эти значения нулю и пересылает ошибку, переданную как аргумент, в качестве окончательного значения. Используя это, мы можем возвращать любые ошибки capitalize, используя оператор return перед вызовом handle с параметром error.

Помните, что capitalize должна все время возвращать три значения, поскольку именно так мы определили эту функцию. Но иногда нужны не все значения, которые может вернуть функция. К счастью, эти значения можно гибко присваивать.

Обработка ошибок из функций множественного возврата

Когда функция возвращает много значений, Go требует, чтобы мы присвоили каждое из них переменной. В последнем примере мы так и сделали. При этом имена должны быть разделены запятыми и отображаться слева от оператора :=. Первое значение, возвращаемое capitalize, будет присвоено переменной name, а второе значение (error) будет присвоено переменной err. Иногда нам нужно только значение ошибки. В таком случае можно отказаться от любых нежелательных значений, возвращаемых функциями, используя специальное имя _.

В следующей программе мы изменили первый пример функции capitalize, передав пустую строку (“”) – теперь он выдаст ошибку. Попробуйте запустить эту программу, чтобы увидеть, как проверить только значение ошибки, отбросив первое возвращаемое значение с помощью переменной _:

package main
import (
"errors"
"fmt"
"strings"
)
func capitalize(name string) (string, error) {
if name == "" {
return "", errors.New("no name provided")
}
return strings.ToTitle(name), nil
}
func main() {
_, err := capitalize("")
if err != nil {
fmt.Println("Could not capitalize:", err)
return
}
fmt.Println("Success!")
}
Could not capitalize: no name provided

В этот раз в функции main() мы присвоили имя capitalized переменной _. Затем мы присвоили error, возвращаемую capitalize, переменной err. После этого мы проверили, присутствовала ли ошибка в условном выражении if err != nil. Поскольку в качестве аргумента для capitalize мы жестко закодировали пустую строку в строке _, err := capitalize(“”), это условие всегда будет иметь значение true. Это приводит к выводу:

Could not capitalize: no name provided

Его возвращает функция fmt.Println в теле оператора if. После этого return  пропустит fmt.Println(“Success!”).

Заключение

Теперь вы знаете несколько способов создания ошибок через стандартную библиотеку и знаете, как создавать функции, которые возвращают ошибки идиоматическим способом. Также вы знаете, как работают функции errors.New и fmt.Errorf. В следующих уроках мы рассмотрим, как создавать собственные типы ошибок, чтобы предоставить пользователям более подробную информацию.

Tags: Go, Golang

We’re Earthly. We make building software simpler and therefore faster. Earthly is open-source and written in go. So if you’re interested in a simple way to build then check us out.

Error handling in Go is a little different than other mainstream programming languages like Java, JavaScript, or Python. Go’s built-in errors don’t contain stack traces, nor do they support conventional try/catch methods to handle them. Instead, errors in Go are just values returned by functions, and they can be treated in much the same way as any other datatype — leading to a surprisingly lightweight and simple design.

In this article, I’ll demonstrate the basics of handling errors in Go, as well as some simple strategies you can follow in your code to ensure your program is robust and easy to debug.

The Error Type

The error type in Go is implemented as the following interface:

type error interface {
    Error() string
}

So basically, an error is anything that implements the Error() method, which returns an error message as a string. It’s that simple!

Constructing Errors

Errors can be constructed on the fly using Go’s built-in errors or fmt packages. For example, the following function uses the errors package to return a new error with a static error message:

package main

import "errors"

func DoSomething() error {
    return errors.New("something didn't work")
}

Similarly, the fmt package can be used to add dynamic data to the error, such as an int, string, or another error. For example:

package main

import "fmt"

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("can't divide '%d' by zero", a)
    }
    return a / b, nil
}

Note that fmt.Errorf will prove extremely useful when used to wrap another error with the %w format verb — but I’ll get into more detail on that further down in the article.

There are a few other important things to note in the example above.

  • Errors can be returned as nil, and in fact, it’s the default, or “zero”, value of on error in Go. This is important since checking if err != nil is the idiomatic way to determine if an error was encountered (replacing the try/catch statements you may be familiar with in other programming languages).

  • Errors are typically returned as the last argument in a function. Hence in our example above, we return an int and an error, in that order.

  • When we do return an error, the other arguments returned by the function are typically returned as their default “zero” value. A user of a function may expect that if a non-nil error is returned, then the other arguments returned are not relevant.

  • Lastly, error messages are usually written in lower-case and don’t end in punctuation. Exceptions can be made though, for example when including a proper noun, a function name that begins with a capital letter, etc.

Defining Expected Errors

Another important technique in Go is defining expected Errors so they can be checked for explicitly in other parts of the code. This becomes useful when you need to execute a different branch of code if a certain kind of error is encountered.

Defining Sentinel Errors

Building on the Divide function from earlier, we can improve the error signaling by pre-defining a “Sentinel” error. Calling functions can explicitly check for this error using errors.Is:

package main

import (
    "errors"
    "fmt"
)

var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        switch {
        case errors.Is(err, ErrDivideByZero):
            fmt.Println("divide by zero error")
        default:
            fmt.Printf("unexpected division error: %s\n", err)
        }
        return
    }

    fmt.Printf("%d / %d = %d\n", a, b, result)
}

Defining Custom Error Types

Many error-handling use cases can be covered using the strategy above, however, there can be times when you might want a little more functionality. Perhaps you want an error to carry additional data fields, or maybe the error’s message should populate itself with dynamic values when it’s printed.

You can do that in Go by implementing custom errors type.

Below is a slight rework of the previous example. Notice the new type DivisionError, which implements the Error interface. We can make use of errors.As to check and convert from a standard error to our more specific DivisionError.

package main

import (
    "errors"
    "fmt"
)

type DivisionError struct {
    IntA int
    IntB int
    Msg  string
}

func (e *DivisionError) Error() string { 
    return e.Msg
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, &DivisionError{
            Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
            IntA: a, IntB: b,
        }
    }
    return a / b, nil
}

func main() {
    a, b := 10, 0
    result, err := Divide(a, b)
    if err != nil {
        var divErr *DivisionError
        switch {
        case errors.As(err, &divErr):
            fmt.Printf("%d / %d is not mathematically valid: %s\n",
              divErr.IntA, divErr.IntB, divErr.Error())
        default:
            fmt.Printf("unexpected division error: %s\n", err)
        }
        return
    }

    fmt.Printf("%d / %d = %d\n", a, b, result)
}

Note: when necessary, you can also customize the behavior of the errors.Is and errors.As. See this Go.dev blog for an example.

Another note: errors.Is was added in Go 1.13 and is preferable over checking err == .... More on that below.

Wrapping Errors

In these examples so far, the errors have been created, returned, and handled with a single function call. In other words, the stack of functions involved in “bubbling” up the error is only a single level deep.

Often in real-world programs, there can be many more functions involved — from the function where the error is produced, to where it is eventually handled, and any number of additional functions in-between.

In Go 1.13, several new error APIs were introduced, including errors.Wrap and errors.Unwrap, which are useful in applying additional context to an error as it “bubbles up”, as well as checking for particular error types, regardless of how many times the error has been wrapped.

A bit of history: Before Go 1.13 was released in 2019, the standard library didn’t contain many APIs for working with errors — it was basically just errors.New and fmt.Errorf. As such, you may encounter legacy Go programs in the wild that do not implement some of the newer error APIs. Many legacy programs also used 3rd-party error libraries such as pkg/errors. Eventually, a formal proposal was documented in 2018, which suggested many of the features we see today in Go 1.13+.

The Old Way (Before Go 1.13)

It’s easy to see just how useful the new error APIs are in Go 1.13+ by looking at some examples where the old API was limiting.

Let’s consider a simple program that manages a database of users. In this program, we’ll have a few functions involved in the lifecycle of a database error.

For simplicity’s sake, let’s replace what would be a real database with an entirely “fake” database that we import from "example.com/fake/users/db".

Let’s also assume that this fake database already contains some functions for finding and updating user records. And that the user records are defined to be a struct that looks something like:

package db

type User struct {
  ID       string
  Username string
  Age      int
}

func FindUser(username string) (*User, error) { /* ... */ }
func SetUserAge(user *User, age int) error { /* ... */ }

Here’s our example program:

package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    return db.Find(username)
}

func SetUserAge(u *db.User, age int) error {
    return db.SetAge(u, age)
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return err
  }

  if err = SetUserAge(user, age); err != nil {
      return err
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

Now, what happens if one of our database operations fails with some malformed request error?

The error check in the main function should catch that and print something like this:

failed finding or updating user: malformed request

But which of the two database operations produced the error? Unfortunately, we don’t have enough information in our error log to know if it came from FindUser or SetUserAge.

Go 1.13 adds a simple way to add that information.

Errors Are Better Wrapped

The snippet below is refactored so that is uses fmt.Errorf with a %w verb to “wrap” errors as they “bubble up” through the other function calls. This adds the context needed so that it’s possible to deduce which of those database operations failed in the previous example.

package main

import (
    "errors"
    "fmt"

    "example.com/fake/users/db"
)

func FindUser(username string) (*db.User, error) {
    u, err := db.Find(username)
    if err != nil {
        return nil, fmt.Errorf("FindUser: failed executing db query: %w", err)
    }
    return u, nil
}

func SetUserAge(u *db.User, age int) error {
    if err := db.SetAge(u, age); err != nil {
      return fmt.Errorf("SetUserAge: failed executing db update: %w", err)
    }
}

func FindAndSetUserAge(username string, age int) error {
  var user *User
  var err error

  user, err = FindUser(username)
  if err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  if err = SetUserAge(user, age); err != nil {
      return fmt.Errorf("FindAndSetUserAge: %w", err)
  }

  return nil
}

func main() {
    if err := FindAndSetUserAge("bob@example.com", 21); err != nil {
        fmt.Println("failed finding or updating user: %s", err)
        return
    }

    fmt.Println("successfully updated user's age")
}

If we re-run the program and encounter the same error, the log should print the following:

failed finding or updating user: FindAndSetUserAge: SetUserAge: failed executing db update: malformed request

Now our message contains enough information that we can see the problem originated in the db.SetUserAge function. Phew! That definitely saved us some time debugging!

If used correctly, error wrapping can provide additional context about the lineage of an error, in ways similar to a traditional stack-trace.

Wrapping also preserves the original error, which means errors.Is and errors.As continue to work, regardless of how many times an error has been wrapped. We can also call errors.Unwrap to return the previous error in the chain.

When To Wrap

Generally, it’s a good idea to wrap an error with at least the function’s name, every time you “bubble it up” — i.e. every time you receive the error from a function and want to continue returning it back up the function chain.

Wrapping an error adds the gift of context

There are some exceptions to the rule, however, where wrapping an error may not be appropriate.

Since wrapping the error always preserves the original error messages, sometimes exposing those underlying issues might be a security, privacy, or even UX concern. In such situations, it could be worth handling the error and returning a new one, rather than wrapping it. This could be the case if you’re writing an open-source library or a REST API where you don’t want the underlying error message to be returned to the 3rd-party user.

Conclusion

That’s a wrap! In summary, here’s the gist of what was covered here:

  • Errors in Go are just lightweight pieces of data that implement the Error interface
  • Predefined errors will improve signaling, allowing us to check which error occurred
  • Wrap errors to add enough context to trace through function calls (similar to a stack trace)

I hope you found this guide to effective error handling useful. If you’d like to learn more, I’ve attached some related articles I found interesting during my own journey to robust error handling in Go.

Also, checkout Earthly. I work on it and its written in go and is open source.

References

  • Error handling and Go
  • Go 1.13 Errors
  • Go Error Doc
  • Go By Example: Errors
  • Go By Example: Panic

Earthly makes builds simple
Fast, repeatable builds with an instantly familiar syntax – like Dockerfile and Makefile had a baby.

Learn More

Слышатся сирены. Студенты и учителя быстро выскочили из классов и столпились у точки сбора. Никакой опасности в поле зрения нет и ничего не горит. Это очередная учебная пожарная тревога. В случае реальной чрезвычайной ситуации все точно будут знать, что делать.

Премиум 👑 канал по Golang

Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎

Подписаться на канал

Уроки, статьи и Видео

Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.

Go в ВК

ЧАТ в Telegram

Содержание статьи

  • Исправление ошибок в Golang
  • Элегантная обработка ошибок
  • Запись данных в файле
  • Применяем defer — отложенные действия
  • Креативная обработка ошибок
  • Новые ошибки в программе на Golang
  • Причины каждой ошибки в Go
  • Настраиваемые типы ошибок
  • Множество ошибок в Golang
  • Утверждение типа Go
  • Принцип работы panic
  • Есть ли исключения в Golang?
  • Как использовать panic
  • Тонкости работы с panic в Go

Файл не найден, неверный формат, сервер недоступен. Что делает программа, когда что-то идет не так? Возможно, проблему можно решить, и тогда операции будут выполняться должным образом. Иногда лучше всего просто выйти и закрыть двери — или на крайний случай разбить окно и выскочить наружу.

План всегда важен. Рассмотрим возможные ошибки и способы их исправления. Go всегда ответственно подходит к вопросу устранения ошибок, побуждая вас задуматься о причинах ошибки, что поможет ее решить. Как и десятая пожарная тренировка, обработка ошибок может показаться монотонной, однако результат того стоит.

В данном уроке будут рассмотрены способы исправления ошибок и выяснения причины их появления. Под конец сравним стиль исправления ошибок Go и в других языках программирования.

В начала 18 века английский поэт Александр Поуп написал поэму, строчка которой известна по сей день: to err is human, то есть человеку свойственно ошибаться. Подумайте, как данную строку можно сравнить с программированием.

To err is human; to forgive, divine.

Александ Поуп, “An Essay on Criticism: Part 2”

Все делают ошибки. Системы не исключение. Ошибки повсеместны. Они не считаются редким явлением, поэтому лучше быть готовым. Принимайте ошибки, не игнорируйте их. Работайте над исправлением и двигайтесь дальше.

Исправление ошибок в Golang

В языках программирования прошлого лимитация на одно возвращаемое значение делали исправление ошибок не совсем понятным. Функции переполняли бы одинаковое возвращаемое значение для указания как ошибки, так и успешного значения, или запрашивали бы побочный канал для обращения к ошибки вроде глобальной переменной errno. Что еще хуже, механизм сообщения об ошибках было непоследовательным от функции к функции.

У Go есть несколько возвращаемых значений, как упоминалось в уроке о функциях. Хотя это не относится к обработке ошибок, несколько возвращаемых значений обеспечивают простой и последовательный механизм возврата ошибки к вызову функций. Если функция может вернуть ошибку, соглашение состоит в том, чтобы использовать последнее возвращаемое значение для ошибок. Вызывающий элемент должен проверить, произошла ли ошибка сразу после вызова функции. Если ошибок не было, значение ошибки будет равно nil.

Чтобы продемонстрировать обработку ошибок, Листинг 1 вызывает функцию ReadDir. Если возникает ошибка, переменная err не будет равна nil, что заставит программу вывести ошибку и немедленно завершить работу. Ненулевое значение, переданное os.Exit, сообщает операционной системе, что произошла ошибка.

Если ReadDir успешно выполнена, files будет назначен к срезу os.FileInfo, предоставляющий информацию о файлах и каталогах по указанному пути. В данном случае точка уточняет путь, указывающий текущую директорию.

files, err := ioutil.ReadDir(«.»)

if err != nil {

    fmt.Println(err)

    os.Exit(1)

}

for _, file := range files {

    fmt.Println(file.Name())

}

Когда возникает ошибка, не стоит полагаться на другие возвращаемые значения. Они могут быть нулевыми для своего типа, но некоторые функции возвращают данные частично или же совершенно иные значения.

При запуске Листинга 1 на Go Playground в выводе будет список директорий:

Для создания списка содержимого другой директории замените текущую директорию (".") в Листинге 1 названием другой директории вроде "etc". Список может содержать как файлы, так и директории. Вы можете использовать file.IsDir() для того, чтобы различить их.

Вопросы для проверки:

  1. Переделайте Листинг 1 для чтения воображаемой директории под названием "unicorns". Какая ошибка выйдет?
  2. Какое сообщение об ошибке выйдет при использовании ReadDir над файлом "/etc/hosts" вместо директории.

Элегантная обработка ошибок в Golang

Разработчикам Go рекомендуется учитывать и обрабатывать любые ошибки, которые возвращают функции. Количество кода для обработки ошибок увеличивается довольно быстро. К счастью, есть несколько способов уменьшить размер кода обработки ошибок без ущерба надежности.

Некоторые функции выполняют вычисления, преобразования данных и другую логику, где ошибки будут некстати. Есть функции, которые взаимодействуют с файлами, базами данных и серверами. Связь несколько ненадежна и может потерпеть неудачу. Одной из стратегий уменьшения кода обработки ошибок является изоляция безошибочного подмножества программы от изначально подверженного ошибкам кода.

Но как насчет кода, который возвращает ошибки? Мы не можем удалить ошибки, но можем упростить код обработки ошибок. Чтобы продемонстрировать это, напишем небольшую программу для записи в файл следующих английских слоганов Go (Go Proverbs), а затем улучшим обработку ошибок, пока код не станет приемлемым.

Errors are values.
Don’t just check errors, handle them gracefully.
Don’t panic.
Make the zero value useful.
The bigger the interface, the weaker the abstraction.
interface{} says nothing.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.
Documentation is for users.
A little copying is better than a little dependency.
Clear is better than clever.
Concurrency is not parallelism.
Don’t communicate by sharing memory, share memory by communicating.
Channels orchestrate; mutexes serialize.

Rob Pike, Go Proverbs

Запись данных в файле

При записи файла множество вещей может пойти не так. Если путь неправильный, или есть проблемы с разрешением, столкнуться с ошибкой можно еще перед началом записи. По окончании записи на диске устройства может закончится место, или же съемный диск может быть случайно извлечен. В дополнении ко всему в конце файл должен быть закрыт, это позволяет убедиться, что все успешно записано на диск, а также избежать утечки информации.

На заметку: Операционная система ограничивает число открытых файлов, поэтому при открытии каждого нового файла память уменьшается. Когда файл не используется, но при этом остается открытым, расход ресурсов является примером утечки.

Главная функция в Листинге 2 вызывает proverbs для создания файла и обрабатывает любые ошибки, отображая ее и затем выходя. Другая имплементация может обрабатывать ошибки иначе, возможно, подсказывая пользователю другой путь или название файла. Хотя функция proverbs может быть написана так, чтобы она выходила при возникновении ошибок, полезно разрешить вызывающему элементу решать, как обрабатывать ошибки.

err := proverbs(«proverbs.txt»)

if err != nil {

    fmt.Println(err)

    os.Exit(1)

}

Функция proverbs может вернуть error, что является специальным встроенным типом для ошибок. Функция повременит с созданием файла из за ошибки. Если в данный момент возникает ошибка, нет нужды закрывать файл, поэтому процесс обрывается. Оставшаяся часть функции записывает строки в файл и гарантирует, что файл закрыт, независимо от его успешности, как показано в коде ниже.

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    _, err = fmt.Fprintln(f, «Errors are values.»)

    if err != nil {

        f.Close()

        return err

    }

    _, err = fmt.Fprintln(f, «Don’t just check errors, handle them gracefully.»)

    f.Close()

    return err

}

В предыдущем коде много моментов для обработки ошибок — так много, что запись каждого выражения из «Go Proverbs» может стать довольно утомительной.

Плюсом является тот факт, что у кода для обработки ошибок есть отступ, это позволяет легче ориентироваться в коде. Отступы для ошибок являются общим паттерном Go, что во время имплементации можно усовершенствовать.

Вопрос для проверки:

Почему функции должны возвращать ошибку вместо выхода из программы?

Применяем defer — отложенные действия в Golang

Убедиться, что файл правильно закрыт, можно с помощью ключевого слова defer. Go гарантирует, что все отложенные действия будут выполнены до возврата содержащей функции. В следующем листинге каждый возвращаемый оператор, следующий за defer, приведет к вызывающему методу f.Close().

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    defer f.Close()

    _, err = fmt.Fprintln(f, «Errors are values.»)

    if err != nil {

        return err

    }

    _, err = fmt.Fprintln(f, «Don’t just check errors, handle them gracefully.»)

    return err

}

На заметку: Поведение предыдущего кода похоже тому, что в Листинге 3. Изменение кода без изменения его поведения называется рефакторингом. Как и переосмысление первого черновика сочинения, рефакторинг является важным навыком для написания лучшего кода.

Вы можете отложить любую функцию или метод, и как множество возвращаемых значений, отсрочка нужна не для уточнения обработки ошибки. Она улучшает обработку ошибок, избавляясь от необходимости постоянно помнить об очистке. Благодаря defer, код для обработки ошибок может сфокусироваться только на своей задачи и больше ни о чем.

Ключевое слово defer облегчает процесс, однако проверка на наличие ошибок после каждой строки кода очень утомительна. Пришло время для креативности!

Вопрос для проверки:

Когда будет вызвано отсроченное действие?

Креативная обработка ошибок в Golang

15 января 2015 года в блоге Go была опубликована отличная статья об обработке ошибок. В статье описывается простой способ для записи в файл без повторения одинакового кода для обработки ошибок после каждой строчки.

Для применения данной техники вам понадобится объявить новый тип, что вызывается в Листинге 5. Если при записи safeWriter в файл возникает ошибка, он сохраняет ошибку вместо ее возвращения. Следующие попытки записи в одинаковый файл будут пропущены, если writeln видит, что раньше была ошибка.

type safeWriter struct {

    w   io.Writer

    err error // Место для хранения первой ошибки

}

func (sw *safeWriter) writeln(s string) {

    if sw.err != nil {

        return // Пропускает запись, если раньше была ошибка

    }

    _, sw.err = fmt.Fprintln(sw.w, s) // Записывает строку и затем хранить любую ошибку

}

Через использование safeWriter следующий листинг записывает несколько строк без репетативной обработки ошибок, но по-прежнему возвращает все возникшие ошибки.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

func proverbs(name string) error {

    f, err := os.Create(name)

    if err != nil {

        return err

    }

    defer f.Close()

    sw := safeWriter{w: f}

    sw.writeln(«Errors are values.»)

    sw.writeln(«Don’t just check errors, handle them gracefully.»)

    sw.writeln(«Don’t panic.»)

    sw.writeln(«Make the zero value useful.»)

    sw.writeln(«The bigger the interface, the weaker the abstraction.»)

    sw.writeln(«interface{} says nothing.»)

    sw.writeln(«Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.»)

    sw.writeln(«Documentation is for users.»)

    sw.writeln(«A little copying is better than a little dependency.»)

    sw.writeln(«Clear is better than clever.»)

    sw.writeln(«Concurrency is not parallelism.»)

    sw.writeln(«Don’t communicate by sharing memory, share memory by communicating.»)

    sw.writeln(«Channels orchestrate; mutexes serialize.»)

    return sw.err // Возвращает ошибку в случае ее возникновения

}

Это более простой и чистый способ для записи текстового файла, но смысл не в этом. Такая же техника может использоваться для создания zip-файлов или для совершенно разных задач. Крупная идея становится значительнее одной техники:

… ошибки являются значениями, и сила языка программирования Go в том, что он может обработать их.

Роб Пайк, «Ошибки — это значения«

Перед вами элегантный способ обработки ошибок в Go.

Вопрос для проверки:

Если бы сообщение об ошибке в Листинге 6 сообщало файлу “Clear is better than clever.”, какие бы событие последовали за этим?

Новые ошибки в программе на Golang

Если функция получает неверные параметры, или если что-то другое идет не так, вы можете создать и вернуть новые значения ошибок, чтобы оповестить вызывающий элемент о проблеме.

Для демонстрации новых ошибок Листинг 7 создает основу для Судоку, что представляет собой сетку 9 на 9. Каждый квадрат сетки может содержать цифру от 1 до 9. Имплементация использует массив с фиксированным размером, ноль указывает на пустой квадрат.

const rows, columns = 9, 9

// Grid является сеткой Судоку

type Grid [rows][columns]int8

Пакет errors содержит функцию конструктора, что принимает строку для сообщения об ошибке. Используя ее, метод Set в «Листинге 8» может создать и возвратить ошибку "out of bounds".

Проверка параметров в начале метода защищает оставшуюся часть метода от неправильного ввода.

func (g *Grid) Set(row, column int, digit int8) error {

    if !inBounds(row, column) {

        return errors.New(«out of bounds»)

    }

    g[row][column] = digit

    return nil

}

Функция inBounds в следующем листинге помогает убедиться, что row и column находятся в пределах границ сетки. Она не дает методу Set забиться лишними деталями.

func inBounds(row, column int) bool {

    if row < 0 || row >= rows {

        return false

    }

    if column < 0 || column >= columns {

        return false

    }

    return true

}

Наконец функция main в следующем листинге создает сетку и отображает любую ошибку, возникшую в результате неправильной замены.

func main() {

    var g Grid

    err := g.Set(10, 0, 5)

    if err != nil {

        fmt.Printf(«An error occurred: %v.\n», err)

        os.Exit(1)

    }

}

На заметку: Для сообщений ошибок часто используются части предложений, чтобы перед отображением к ним можно было добавить дополнительный текст.

Всегда читайте сообщения об ошибках. Рассматривайте их как часть пользовательского интерфейса программы, будь он для конечных пользователей и разработчиков. Фраза «out of bounds» неплоха, но более точное «outside of grid boundaries» может быть лучше. А сообщение «error 37» вообще ни о чем не говорит.

Вопрос для проверки:

В чем преимущество защит от плохого ввода перед функцией?

Причины каждой ошибки в Go

Многие пакеты Go объявляют и экспортируют переменные для ошибок, которые они могут вернуть. Для использования этого с сеткой Судоку следующий листинг объявляет две переменные для ошибок на уровне пакета.

var (

    ErrBounds = errors.New(«out of bounds»)

    ErrDigit  = errors.New(«invalid digit»)

)

На заметку: Принято присваивать сообщения об ошибках переменным, что начинаются со слова Err.

По объявлении ErrBounds вы можете изменить метод Set для возвращения его вместо создания новой ошибки, как показано в следующем коде.

if !inBounds(row, column) {

    return ErrBounds

}

Если метод Set возвращает ошибку, вызывающая сторона может различить возможные ошибки и обрабатывать определенные ошибки по-разному, как показано в следующем листинге. Вы можете сравнить ошибку, возвращаемую с переменными ошибки, используя == или оператор switch.

var g Grid

err := g.Set(0, 0, 15)

if err != nil {

    switch err {

    case ErrBounds, ErrDigit:

        fmt.Println(«Les erreurs de paramètres hors limites.»)

    default:

        fmt.Println(err)

    }

    os.Exit(1)

}

На заметку: Конструктор errors.New имплементируется через использование указателя, поэтому оператор switch в предыдущем примере сравнивает адреса памяти, текст не содержит сообщения об ошибке.

Задание для проверки:

Напишите функцию validDigit и используйте ее, чтобы убедиться, что метод Set принимает только цифры между 1 и 9.

Настраиваемые типы ошибок в Golang

Каким бы полезным не был errors.New, иногда нужно, чтобы ошибки описывались не просто сообщением. Go достаточно свободен в этом плане.

Тип error является встроенным интерфейсом, как показано в следующем примере. Любой тип, что имплементирует метод Error() для возвращения строки, неявно удовлетворяет интерфейс. В качестве интерфейса возможно создать новые типы ошибок.

type error interface {

    Error() string

}

Множество ошибок в Golang

Есть несколько причин, по которым цифра не может быть помещена в определенное место в Судоку. В предыдущем разделе мы установили два правила: строки и столбцы находятся внутри сеткии, и цифры находятся в промежутке от 1 до 9. Что будет, если вызывающий элемент передаст множество неверных аргументов?

Вместо возвращения одной ошибки за раз, метод Set может сделать несколько проверок и вернуть все ошибки сразу. Тип SudokuError в Листинге 15 является срезом error. Он удовлетворяет интерфейсу error с методом, что соединяет ошибки вместе в одну строку.

На заметку: Принято, что настраиваемые типы ошибок вроде SudokuError заканчиваются словом Error. Иногда это просто слово Error вроде url.Error из пакета url.

type SudokuError []error

// Error возвращает одну или несколько ошибок через запятые.

func (se SudokuError) Error() string {

    var s []string

    for _, err := range se {

        s = append(s, err.Error()) // Конвертирует ошибки в строки

    }

    return strings.Join(s, «, «)

}

Чтобы использовать SudokuError, метод Set можно модифицировать для валидации границ и цифр, возвращая обе ошибки сразу, как показано в следующем примере.

func (g *Grid) Set(row, column int, digit int8) error { // Возвращает тип ошибки

    var errs SudokuError

    if !inBounds(row, column) {

        errs = append(errs, ErrBounds)

    }

    if !validDigit(digit) {

        errs = append(errs, ErrDigit)

    }

    if len(errs) > 0 {

        return errs

    }

    g[row][column] = digit

    return nil // Возвращает nil

}

Если ошибок нет, метод Set возвращает nil. Это не изменилось по сравнению с Листингом 8, но важно отметить, что пустой срез errs здесь не возвращается. Для подробностей можете почитать об интерфейсах nil.

Сигнатура метода для Set также не изменилась по сравнению с Листингом 8. Всегда используйте тип интерфейса error при возвращении ошибок, а не конкретные типы вроде SudokuError.

Вопрос для проверки:

Что произойдет, если метод Set успешно вернет пустой срез errs?

Утверждение типа в Go

Так как Листинг 16 конвертирует SudokuError в тип интерфейса error перед его возвращением, может возникнуть вопрос, как получить доступ к отдельным ошибкам. Решением станет утверждение типа, или type assertion. Используя утверждение типа, вы можете конвертировать интерфейс в конкретный базовый тип.

Утверждение типа в Листинге 17 утверждает err для типа SudokuError через код err.(SudokuError). Это так и есть, то ok будет истинным, а err будет SudokuError, давая доступ к срезам ошибок в данном случае. Помните, что отдельные ошибки для SudokuError являются переменными ErrBounds и ErrDigit, что могут сравниваться в случае необходимости.

var g Grid

err := g.Set(10, 0, 15)

if err != nil {

    if errs, ok := err.(SudokuError); ok {

        fmt.Printf(«%d error(s) occurred:\n», len(errs))

        for _, e := range errs {

            fmt.Printf(«- %v\n», e)

        }

    }

    os.Exit(1)

}

В выводе предыдущего кода будут следующие ошибки:

2 error(s) occurred:

out of bounds

invalid digit

На заметку: Если тип удовлетворяет нескольким интерфейсам, утверждение типа также может конвертировать из одного интерфейса в другой.

Вопрос для проверки:

Что делает утверждение типа err.(SudokuError)?

Принцип работы panic в Golang

При обработке ошибок некоторые языки программирования сильно полагаются на исключения. В Go нет исключений, но в нем есть похожий механизм, который называется panic. При задействовании panic в программе происходит сбой. Это похоже на случаи необработанных исключений в других языках.

Есть ли исключения в Golang?

Исключения в других языках значительно отличаются от значений ошибок в Go. Это заметно как в поведении, так и в имплементации.

Если функция выдает исключение, и никто не собирается его перехватить, это исключение доходит до вызывающей функции, затем до вызывающей ту функцию и так далее, пока достигает вершины стека вызовов (например, функция main).

Исключения — это стиль обработки ошибок, который можно считать включенным. Часто они не занимают код, тогда как выбор обработки исключений может привлекать изрядное количество специализированного кода. Это связано с тем, что вместо использования существующих возможностей языка исключения обычно имеют специальные ключевые слова, такие как try, catch, throw, finally, raise, rescue, except и так далее.

Значения ошибок в Go предоставляют простую, гибкую альтернативу исключениям, которые могут помочь вам создать надежное программное обеспечение. Игнорирование значений ошибок в Go — это сознательное решение, которое становится очевидным каждому, кто читает полученный код.

Вопрос для проверки:

В чем преимущество значений ошибок Go по сравнению с исключениями?

Как использовать panic в Golang

Как упоминалось ранее, в Go есть механизм, похожий на исключения, что называется panic. В то время, как неправильная цифра в Судоку в другом языке может стать причиной исключения, panic в Go является редкостью.

При осознании того, что, отправившись в поездку, вы забыли полотенце,  вы можете запаниковать. Аргумент, переданный panic, может быть любого типа, не только строкой, как показано ниже:

panic(«Я забыл свое полотенце»)

На заметку: Хотя значения ошибок обычно предпочтительнее panic, panic часто лучше, чем os.Exit в том, что panic запустит любую отсроченную функцию, а os.Exit этого делать не станет.

В некоторых ситуациях Go предпочтет panic вместо значений ошибок, это может быть деление на ноль:

var zero int

_ = 42 / zero // Runtime error: integer divide by zero — целое число делится на ноль

Вопрос для проверки:

Как программа может использовать panic?

Тонкости работы с panic в Golang

Чтобы panic не привел к сбою программы, Go предоставляет функцию recover, что показано в Листинге 18.

Отсроченные функции выполняются перед возвращением функции, даже в том случае, если задействуется panic. Если отсроченная функция вызывает recover, panic остановится, и программа продолжит выполняться. В таком случае цель у recover похожа на catch, except и rescue в других языках.

defer func() {

    if e := recover(); e != nil { // Приходит в себя после panic

         fmt.Println(e) // Выводит: Я забыл свое полотенце

    }

}()

panic(«Я забыл свое полотенце») // Приводит к panic

Данный код использует анонимную функцию.

Вопрос для проверки:

Где может использоваться встроенная функция recover?

Заключение

  • Ошибки являются значениями, что внутренне оперируют с несколькими возвращаемыми значениями и другой частью языка Go;
  • Будучи креативным, можно найти множество способов для обработки ошибок;
  • Настраиваемые типы ошибок могут удовлетворить интерфейсу error;
  • Ключевое слово defer помогает выполнить очистку перед возвращением функции;
  • Утверждение типа может конвертировать интерфейс в конкретный тип или другой интерфейс;
  • Не паникуйте — изучите ошибку.

Итоговое задание для проверки:

В стандартной библиотеке Go есть функция для парсинга веб адресов (см. golang.org/pkg/net/url/#Parse). Отобразите ошибку, которая возникает, когда url.Parse используется для неправильного веб адреса вроде того, что содержит пробелы: https://a b.com/.

Используйте специальный символ %#v с Printf для изучения ошибки. Затем выполните утверждение типа *url.Error для получения доступа и вывода полей базовой структуры.

На заметку: URL, или Uniform Resource Locator — адрес страницы в Интернете.

Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.

E-mail: vasile.buldumac@ati.utm.md

Образование

Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники

  • 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
  • 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»

Понравилась статья? Поделить с друзьями:

Интересное по теме:

  • Вывод маток никот ошибки
  • Вы должны туда собственноручно сходить какая ошибка
  • Вывод всех ошибок python
  • Вывод всех ошибок php htaccess
  • Вы можете задать интересующие вас вопросы речевая ошибка

  • Добавить комментарий

    ;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: