Введение

Как программисты, мы все сталкивались с ужасным оператором 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 вы можете превратить свой код, подверженный ошибкам, в техническое искусство. Удачной обработки ошибок!