
Введение
Как программисты, мы все сталкивались с ужасным оператором if err != nil, который, кажется, переполняется повсюду в нашей кодовой базе, что приводит к структурам, подобным спагетти, и бесчисленным жалобам программистов :-(. Обработка ошибок имеет свой собственный набор проблем, включая возможность игнорировать ошибки. генерируются функциональными процессами или бизнес-логикой (многие разработчики делают это намеренно) и неуверенностью в том, какие действия следует предпринять при возникновении ошибки.В этой статье мы рассмотрим философию обработки ошибок, важность обработки ошибок в программировании и как Go предлагает элегантные решения этих проблем.
Философия обработки ошибок
Обработка ошибок служит нескольким важным целям в программировании, и понимание ее философии имеет решающее значение для написания чистого, надежного и поддерживаемого кода. Давайте углубимся в три фундаментальных принципа обработки ошибок.
- Четкий сигнал: обработка ошибок должна давать разработчику четкий сигнал о том, что что-то пошло не так и что пошло не так. Явно проверяя наличие ошибок, мы гарантируем, что знаем о потенциальных проблемах и можем предпринять соответствующие действия.
- Разделение логики: логика обработки ошибок должна быть отделена от обычного потока кода. Это разделение позволяет нам сосредоточиться на основной логике, обрабатывая ошибки отдельно, что приводит к более чистому и читабельному коду.
- Надлежащее ведение журнала и освобождение ресурсов: содержательное ведение журнала ошибок предоставляет ценную информацию для отладки и устранения неполадок. Кроме того, при возникновении ошибок крайне важно рассмотреть вопрос об освобождении всех полученных ресурсов должным образом, чтобы предотвратить утечку ресурсов и сохранить стабильность системы.
Вдохновение блока «Попробуй поймать наконец»
Одной из наиболее распространенных конструкций обработки ошибок в других языках программирования является блок try catch finally (наверняка вы знаете, мои друзья Java). Эта конструкция обеспечивает четкий и лаконичный синтаксис для обработки ошибок. Давайте рассмотрим его преимущества и то, как его можно использовать в Go.
1. Очистить синтаксис. Блок try catch finally обеспечивает четко определенный синтаксис для обработки ошибок, что делает код более читабельным и понятным.
2. Разделение логики. Отделяя обычную логику от логики обработки ошибок, разработчики могут сосредоточиться на основных функциях, не отвлекаясь на обработку ошибок, загромождающую их код.
3, осведомленность об ошибках: с блоком try catch finally ошибки нельзя игнорировать. Разработчики вынуждены думать о том, как явно обрабатывать ошибки, продвигая более надежные методы обработки ошибок.
4. Объединение функций. Блок try catch finally позволяет разработчикам объединять несколько функций в цепочку, повышая читаемость кода и уменьшая избыточность. например
Car car = CarBuilder().BuildWheels(wheels).BuildEngine(engine)...;
Подход Go к обработке ошибок
Go использует уникальный подход к обработке ошибок, рассматривая ошибки как значения, а не как исключения. Это дизайнерское решение согласуется с общей философией Go, заключающейся в простоте и удобочитаемости. Давайте рассмотрим некоторые ключевые аспекты подхода Go к обработке ошибок и то, как они способствуют написанию чистого и эффективного кода.
- Ошибка — это ценность: это образ мышления, который должен быть у программиста. В Go ошибки — это значения, как и любой другой входной параметр. Эта точка зрения побуждает программистов явно обрабатывать ошибки и плавно интегрировать обработку ошибок в процесс программирования. Вы должны относиться к ошибкам так же ценно, как и к любым другим данным.
- Простота: дизайн Go отличается простотой и лаконичностью. Если вы обнаружите, что пишете многословный или утомительный код обработки ошибок, скорее всего, вы неправильно к нему подходите (привет, мир, новичок!). Оцените простоту механизма обработки ошибок Go.
- Множественные возвраты: поддержка Go для множественных возвращаемых значений обеспечивает четкое разделение между ожидаемым результатом и потенциальной ошибкой. Такое разделение повышает читаемость кода и упрощает обработку ошибок.
- Интерфейс ошибок. Интерфейс ошибок Go позволяет использовать гибкие подходы к обработке ошибок. Разработчики могут обрабатывать ошибки в зависимости от их конкретных типов, обеспечивая целевую логику обработки ошибок. например
if err != nil {
switch err.(type) {
case *ThisError:
...
case *ThatError:
...
case *MyLifeSucksError:
...
default:
...
}
}
- Отложить освобождение ресурсов: оператор defer в Go предоставляет удобный способ гарантировать, что ресурсы будут освобождены должным образом, даже при наличии ошибок. Это повышает ясность кода и снижает риск утечки ресурсов. например
func ConnectDatabase() error {
db, err := sql.Open("postgres", "my_connection_string")
if err != nil {
return err
}
defer db.Close() // closes the database connection on function exit
}
Улучшение обработки ошибок в Go
Теперь давайте рассмотрим несколько практических примеров того, как вы можете улучшить свой код обработки ошибок в Go, чтобы получить более чистый и удобный для сопровождения код.
Функциональное программирование:
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&p.attr1)
read(&p.attr2)
read(&p.attr3)
read(&p.attr4)
read(&p.attr5)
if err != nil {
return nil, err
}
return &p, nil
}
Мы инкапсулировали обработку ошибок в функцию read, и любые ошибки чтения, возникшие ранее, остальные не будут читаться из-за
if err != nil {
return
}
Используя методы функционального программирования, мы можем получить более чистый код, о котором легче рассуждать. Функция чтения инкапсулирует повторяющуюся логику проверки ошибок, уменьшая избыточность и улучшая читаемость кода. Но это недостаточно чисто.
Структура пакета:
Если ошибки являются значениями, почему они не могут быть частью структуры?
e.g.
type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.attr1)
r.read(&p.attr2)
r.read(&p.attr3)
r.read(&p.attr4)
r.read(&p.attr5)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
Опять же, если какие-либо ошибки чтения произошли ранее, остальные не будут читаться, так как func read проверяет, произошла ли ошибка перед следующим чтением.
Структурируя наш код с помощью пакетов и инкапсулируя соответствующую функциональность, мы можем добиться лучшей организации и разделения задач. Структура Reader и связанные с ней методы обеспечивают более понятный интерфейс для чтения данных, уменьшая дублирование кода и повышая удобство сопровождения кода :)
Свободный интерфейс:
После Package Structuring покажу тебе кое-что покруче:
import (
"errors"
"fmt"
)
type ShoppingCart struct {
Items []string
err error
}
func NewShoppingCart() *ShoppingCart {
return &ShoppingCart{}
}
func (sc *ShoppingCart) AddItem(item string) *ShoppingCart {
if sc.err == nil {
sc.Items = append(sc.Items, item)
}
return sc
}
func (sc *ShoppingCart) RemoveItem(item string) *ShoppingCart {
if sc.err == nil {
index := sc.findIndex(item)
if index == -1 {
sc.err = errors.New("item not found in the cart")
} else {
sc.Items = append(sc.Items[:index], sc.Items[index+1:]...)
}
}
return sc
}
func (sc *ShoppingCart) Clear() *ShoppingCart {
if sc.err == nil {
sc.Items = nil
}
return sc
}
func (sc *ShoppingCart) findIndex(item string) int {
for i, cartItem := range sc.Items {
if cartItem == item {
return i
}
}
return -1
}
func (sc *ShoppingCart) Print() *ShoppingCart {
if sc.err == nil {
fmt.Println("Shopping Cart:")
for _, item := range sc.Items {
fmt.Println("-", item)
}
}
return sc
}
func main() {
cart := NewShoppingCart()
cart.AddItem("Shirt").AddItem("Pants").AddItem("Shoes").RemoveItem("Pants").Print()
if cart.err != nil {
fmt.Println("Error:", cart.err)
}
}
Используя подход с плавным интерфейсом, мы можем объединять вызовы методов, что приводит к более выразительному и читабельному коду. Этот метод улучшает ясность кода и снижает потребность в повторяющихся проверках на наличие ошибок.
Ошибки упаковки и выявление причин:
Будет хреново, если вы напрямую вернете необработанную ошибку на верхний уровень, не будет ли лучше, если вы добавите битовый контекст?
type authorizationError struct {
operation string
err error // original error
}
func (e *authorizationError) Error() string {
return fmt.Sprintf("authorization failed when %s: %v", e.operation, e.err)
}
Чтобы предоставить лучший контекст и информацию об ошибках, Go позволяет нам оборачивать ошибки с помощью функции fmt.Errorf. Этот метод упаковки помогает создавать более информативные сообщения об ошибках. Кроме того, определяя пользовательские типы ошибок, такие как структура authorError, мы можем добавить к ошибке дополнительный контекст, упрощая понимание и обработку конкретных сценариев ошибок.
Кроме того, пакет github.com/pkg/errors можно использовать для переноса ошибок и извлечения исходной ошибки с помощью функции errors.Cause(err). Эта возможность позволяет нам идентифицировать конкретный тип ошибки и обрабатывать ее соответствующим образом, повышая точность обработки ошибок. например
import "github.com/pkg/errors"
// wrap errors
if err != nil {
return errors.Wrap(err, "my life sucks")
}
// Cause interface
switch err := errors.Cause(err).(type) {
case *MyLifeSucksError:
// handle my life sucks
default:
// unknown error
}
Заключение:
В этой статье мы рассмотрели философию обработки ошибок и ее важность в программировании. Мы обсудили проблемы, возникающие при обработке ошибок, и то, как Go предлагает элегантные решения этих проблем. Используя подход Go к обработке ошибок, мы можем писать более чистый и удобный для сопровождения код. Такие методы, как функциональное программирование, структурирование пакетов, плавные интерфейсы и обработка ошибок, способствуют ясности и эффективности кода.
Помните, опять же, обработка ошибок — это не только обработка исключений; это неотъемлемая часть построения надежных программных систем. Благодаря механизмам обработки ошибок и лучшим практикам Go вы можете превратить свой код, подверженный ошибкам, в техническое искусство. Удачной обработки ошибок!