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

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

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

package main

import (
 "errors"
 "fmt"
)

func main() {
 someReturnValue, err := canReturnErrorA()
 if err != nil {
  fmt.Println("ERROR: ", err)
  return
 }
 fmt.Println("Returned from Method A: ", someReturnValue)

}

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

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

package main

import (
 "errors"
 "fmt"
)

func main() {
 func() {
  someReturnValueA, err := canReturnErrorA()
  if err != nil {
   fmt.Println("ERROR: ", err)
   return
  }
  fmt.Println("Returned from Method A: ", someReturnValueA)

  otherIntegerValue, err := canReturnErrorB(someReturnValueA)
  if err != nil {
   fmt.Println("ERROR: ", err)
   return
  }
  fmt.Println("Integer value returned from B: ", otherIntegerValue)
 }()
}

func canReturnErrorA() (string, error) {
 //Perform some function
 //By default we are returning an error
 return "A", nil
}

func canReturnErrorB(returnValueFromA string) (int, error) {
 if returnValueFromA == "A" {
  return -1, errors.New("some error from B")
 }
 return 1, nil
}

func canReturnErrorC(returnValueFromA string, returnValueFromB int) (string, error) {
 //Perform some function
 //By default we are returning an error
 return "", errors.New("some error from C")
}

Это, вероятно, проблема новичка-разработчика, но я уверен, что это распространенная проблема, поэтому я не буду беспокоиться, если вы осудите меня за то, что у меня есть полная статья для этого варианта использования.

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

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

Итак, каково решение? Что ж, один из подходов состоит в том, чтобы абстрагировать логику обработки ошибок в отдельную функцию или модуль, а затем вызывать эту функцию всякий раз, когда возникает ошибка. Это позволит сохранить логику обработки ошибок СУХОЙ (не повторяйтесь) и упростить ее обслуживание и масштабирование по мере роста кодовой базы.

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

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

package main

import (
 "errors"
 "fmt"
)

func main() {
 func() {
  someReturnValueA, err := canReturnErrorA()
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   return
  }
  fmt.Println("Returned from Method A: ", someReturnValueA)

  otherIntegerValue, err := canReturnErrorB(someReturnValueA)
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   return
  }
  fmt.Println("Integer value returned from B: ", otherIntegerValue)

  val, err := canReturnErrorC(someReturnValueA, otherIntegerValue)
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   return
  }
  fmt.Println("Integer value returned from B: ", val)
 }()
}

func canReturnErrorA() (string, error) {
 //Perform some function
 //By default we are returning an error
 return "A", nil
}

func canReturnErrorB(returnValueFromA string) (int, error) {
 if returnValueFromA == "A" {
  return -1, errors.New("some error from B")
 }
 return 1, nil
}

func canReturnErrorC(returnValueFromA string, returnValueFromB int) (string, error) {
 //Perform some function
 //By default we are returning an error
 return "", errors.New("some error from C")
}

Теперь предположим, что мы хотим добавить несколько трассировщиков. Теперь код будет выглядеть так:

package main

import (
 "errors"
 "fmt"
)

func main() {
 func() {
  someReturnValueA, err := canReturnErrorA()
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   Tracer.trace("++Adding Tracer wrapper for the err++", err)
   return
  }
  fmt.Println("Returned from Method A: ", someReturnValueA)

  otherIntegerValue, err := canReturnErrorB(someReturnValueA)
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   Tracer.trace("++Adding Tracer wrapper for the err++", err)
   return
  }
  fmt.Println("Integer value returned from B: ", otherIntegerValue)

  val, err := canReturnErrorC(someReturnValueA, otherIntegerValue)
  if err != nil {
   fmt.Println("ERROR: ", err)
   LOGGER.info("++Adding Logger wrapper for the err++", err)
   Tracer.trace("++Adding Tracer wrapper for the err++", err)
   return
  }
  fmt.Println("Integer value returned from B: ", val)
 }()
}

func canReturnErrorA() (string, error) {
 //Perform some function
 //By default we are returning an error
 return "A", nil
}

func canReturnErrorB(returnValueFromA string) (int, error) {
 if returnValueFromA == "A" {
  return -1, errors.New("some error from B")
 }
 return 1, nil
}

func canReturnErrorC(returnValueFromA string, returnValueFromB int) (string, error) {
 //Perform some function
 //By default we are returning an error
 return "", errors.New("some error from C")
}

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

package main

import (
 "errors"
 "fmt"
)

func main() {
 func() (err error) {
         defer func() {
            if err != nil {
              // Do all the common error handling here
              // Error variable will be updated each time
              LOGGER.info("++Adding Logger wrapper for the err++", err)
              Tracer.trace("++Adding Tracer wrapper for the err++", err)        
            }
         }
  someReturnValueA, err = canReturnErrorA()
  if err != nil {
   fmt.Println("ERROR: ", err)
   return
  }
  fmt.Println("Returned from Method A: ", someReturnValueA)

  otherIntegerValue, err = canReturnErrorB(someReturnValueA)
  if err != nil {
   fmt.Println("ERROR: ", err)
   return
  }
  fmt.Println("Integer value returned from B: ", otherIntegerValue)
  return nil
 }()
}

func canReturnErrorA() (string, error) {
 //Perform some function
 //By default we are returning an error
 return "A", nil
}

func canReturnErrorB(returnValueFromA string) (int, error) {
 if returnValueFromA == "A" {
  return -1, errors.New("some error from B")
 }
 return 1, nil
}

func canReturnErrorC(returnValueFromA string, returnValueFromB int) (string, error) {
 //Perform some function
 //By default we are returning an error
 return "", errors.New("some error from C")
}

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

Надеюсь это поможет!