One place for hosting & domains

      Criando erros personalizados em Go


      Introdução

      O Go oferece dois métodos para criar erros na biblioteca padrão, errors.New e fmt.Errorf. Ao comunicar informações de erros mais complicados aos seus usuários, ou para si mesmo – num momento futuro, por vezes esses dois mecanismos não são o suficiente para capturar e relatar adequadamente o que aconteceu. Para transmitir essa informação de erro mais complexo e obter mais funcionalidade, podemos implementar o tipo de interface da biblioteca padrão, error.

      A sintaxe para isso seria a seguinte:

      type error interface {
        Error() string
      }
      

      O pacote builtin define error como uma interface com um método único Error() que retorna uma mensagem de erro como uma string. Ao implementar esse método, podemos transformar qualquer tipo que definirmos em um erro nosso.

      Vamos tentar executar o exemplo a seguir para ver uma implementação da interface error:

      package main
      
      import (
          "fmt"
          "os"
      )
      
      type MyError struct{}
      
      func (m *MyError) Error() string {
          return "boom"
      }
      
      func sayHello() (string, error) {
          return "", &MyError{}
      }
      
      func main() {
          s, err := sayHello()
          if err != nil {
              fmt.Println("unexpected error: err:", err)
              os.Exit(1)
          }
          fmt.Println("The string:", s)
      }
      

      Veremos o seguinte resultado:

      Output

      unexpected error: err: boom exit status 1

      Aqui, criamos um novo tipo de struct vazio, MyError e definimos o método Error() nele. O método Error() retorna a string "boom".

      Dentro do main(), chamamos a função sayHello que retorna uma string vazia e uma nova instância do MyError. Como o sayHello​​​ sempre retornará um erro, a chamada fmt.Println dentro do corpo da instrução if no main() sempre irá ser executada. Então, usamos o fmt.Println para imprimir a curta string do prefixo "unexpected error:" junto com a instância do MyError mantida dentro da variável err.

      Note que não precisamos chamar diretamente o Error(), uma vez que o pacote fmt consegue detectar automaticamente que se trata de uma implementação de error. Ele chama o Error() de maneira transparente para obter a string "boom“ e a concatena com a string do prefixo "unexpected error: err:".

      Coletando informações detalhadas em um erro personalizado

      Às vezes, um erro personalizado é a maneira mais limpa de capturar informações detalhadas de erro. Por exemplo, vamos supor que queremos capturar o código de status de erros produzidos por um pedido do HTTP; execute o programa a seguir para ver uma implementação do error que nos permite capturar de modo correto tais informações:

      package main
      
      import (
          "errors"
          "fmt"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              os.Exit(1)
          }
          fmt.Println("success!")
      }
      

      Veremos o seguinte resultado:

      Output

      status 503: err unavailable exit status 1

      Neste exemplo, criamos uma nova instância do RequestError e fornecemos o código de status e um erro usando a função errors.New da biblioteca padrão. Então, imprimimos isso usando o fmt.Println como em exemplos anteriores.

      Dentro do método Error() do RequestError, usamos a função fmt.Sprintf para construir uma string usando as informações fornecidas quando o erro foi criado.

      Declarações de tipo e erros personalizados

      A interface error mostra somente um método, mas podemos precisar acessar os outros métodos de implementações de error para lidar com um erro de maneira correta. Por exemplo, podemos ter várias implementações personalizadas do error que são temporárias e podem ser repetidas—indicadas pela presença de um método Temporary().

      As interfaces fornecem uma visualização limitada do conjunto mais amplo de métodos fornecidos por tipos. Assim, devemos usar uma asserção de tipo para alterar os métodos que a visualização está exibindo ou removê-la totalmente.

      O exemplo a seguir amplia o RequestError mostrado anteriormente com um método Temporary() que indicará se os chamadores devem ou não realizar o pedido novamente:

      package main
      
      import (
          "errors"
          "fmt"
          "net/http"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return r.Err.Error()
      }
      
      func (r *RequestError) Temporary() bool {
          return r.StatusCode == http.StatusServiceUnavailable // 503
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              re, ok := err.(*RequestError)
              if ok {
                  if re.Temporary() {
                      fmt.Println("This request can be tried again")
                  } else {
                      fmt.Println("This request cannot be tried again")
                  }
              }
              os.Exit(1)
          }
      
          fmt.Println("success!")
      }
      

      Veremos o seguinte resultado:

      Output

      unavailable This request can be tried again exit status 1

      Dentro do main(), chamamos o doRequest() que retorna uma interface error para nós. Primeiro, imprimimos a mensagem de erro que o método Error() retornou. Em seguida, tentamos expor todos os métodos do RequestError usando a asserção de tipo re, ok := err.(​​ *RequestError)​​​. Se o tipo declarado foi bem sucedido, usamos o método Temporary() para ver se este erro é um erro temporário. Como o StatusCode definido pelo doRequest() é o 503, que corresponde ao http.StatusServiceUnavailable, ele retorna true e faz com que a mensagem "This request can be tried again" (Esta solicitação pode ser repetida) seja impressa. Na prática, faríamos outro pedido em vez de imprimir uma mensagem.

      Erros de empacotamento

      Normalmente, um erro será gerado a partir de algo fora do seu programa, como: um banco de dados, uma conexão de rede etc. As mensagens de erro fornecidas a partir desses erros não ajudam ninguém a encontrar a origem do erro. O uso de erros de empacotamento com informações extra no início de uma mensagem de erro forneceria o contexto necessário para uma depuração bem-sucedida.

      O exemplo a seguir demonstra como podemos anexar informações contextuais a um error – que, de outro modo, seria criptografado – retornado de alguma outra função:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      type WrappedError struct {
          Context string
          Err     error
      }
      
      func (w *WrappedError) Error() string {
          return fmt.Sprintf("%s: %v", w.Context, w.Err)
      }
      
      func Wrap(err error, info string) *WrappedError {
          return &WrappedError{
              Context: info,
              Err:     err,
          }
      }
      
      func main() {
          err := errors.New("boom!")
          err = Wrap(err, "main")
      
          fmt.Println(err)
      }
      

      Veremos o seguinte resultado:

      Output

      main: boom!

      WrappedError é uma struct com dois campos: uma mensagem de contexto em forma de string e um error sobre o qual o WrappedError fornece mais informações. Quando o método Error() for chamado, usaremos o fmt.Sprintf novamente para imprimir a mensagem de contexto e, em seguida, o error (fmt.Sprintf sabe chamar implicitamente o método Error() também).

      Dentro do main(), criamos um erro usando o errors.New e, em seguida, empacotamos aquele erro usando a função Wrap que definimos. Isso nos permite indicar que esse error foi gerado no "main". Além disso, já que o nosso WrappedError também é um error, podemos empacotar outros WrappedErrors — isso nos permitiria ver uma cadeia que que nos ajudaria a rastrear a fonte do erro. Com um pouco de ajuda da biblioteca padrão, podemos até incorporar traços de pilha completos em nossos erros.

      Conclusão

      Como a interface error é apenas um método único, vimos que temos grande flexibilidade na oferta de diferentes tipos de erros para situações diferentes. Isso pode abranger tudo – desde a comunicação de vários fragmentos de informação como parte de um erro até a implementação de uma retirada exponencial. Embora os mecanismos de gerenciamento de erros no Go, em princípio, possam parecer simplistas, podemos chegar a um gerenciamento bastante detalhado usando esses erros personalizados para lidar com situações comuns e incomuns.

      O Go tem outro mecanismo para comunicar comportamentos inesperados, o panics (pânicos). No nosso próximo artigo na série de tratamento de erros, vamos examinar o panics — o que são e como lidar com eles.



      Source link

      Tratamento de erros em Go


      Um código robusto precisa reagir de forma correta a circunstâncias inesperadas, como erros de entrada do usuário, problemas de conexões de rede e falhas de discos. Tratamento de erro é o processo de identificar quando seu programa se encontra em um estado inesperado e adotar medidas para registrar as informações de diagnóstico para depuração posterior.

      Diferente do que ocorre com outras linguagens de progragramação – nas quais os desenvolvedores precisam lidar com os erros usado uma sintaxe especializada – os errors em Go são valores com o tipo error sendo retornado das funções, como ocorre com qualquer outro valor. Para tratar erros em Go, precisamos examinar os erros que as funções poderiam retornar, decidir se ocorreu um erro e adotar as medidas adequadas para proteger os dados e informar os usuários ou operadores de que o erro ocorreu.

      Criando erros

      Antes que possamos tratar os erros, primeiro precisamos criar alguns. A biblioteca padrão oferece duas funções integradas para criar erros: errors.New e fmt.Errorf. Ambas as funções permitem que você especifique uma mensagem de erro personalizada que, posteriormente, você poderá apresentar para os seus usuários.

      A errors.New usa um único argumento — uma mensagem de erro como uma string que você pode personalizar para alertar seus usuários sobre o que deu errado.

      Tente executar o exemplo a seguir para ver um erro criado pela função errors.New, impressa para a saída padrão:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func main() {
          err := errors.New("barnacles")
          fmt.Println("Sammy says:", err)
      }
      

      Output

      Sammy says: barnacles

      Usamos a função errors.New da biblioteca padrão para criar uma nova mensagem de erro com a string "barnacles" como mensagem de erro. Aqui, seguimos a convenção, usando letras minúsculas para a mensagem de erro, conforme sugerido pelo Guia de estilo da linguagem de programação Go.

      Por fim, usamos a função fmt.Println para combinar nossa mensagem de erro com "Sammy says:".

      A função fmt.Errorf permite que você crie uma mensagem de erro de modo dinâmico. Seu primeiro argumento é uma string que contém a mensagem de erro com os valores do espaço reservado como %s de uma string e %d para um número inteiro. A função fmt.Errorf interpola os argumentos que seguem essa string de formatação nesses espaços reservados, na ordem:

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          err := fmt.Errorf("error occurred at: %v", time.Now())
          fmt.Println("An error happened:", err)
      }
      

      Output

      An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

      Usamos a função fmt.Errorf para criar uma mensagem de erro que incluiria a hora atual. A string de formatação que fornecemos à fmt.Errorf contém a diretiva de formação %v que diz à fmt.Errorf para usar a formatação padrão como o primeiro argumento fornecido após a string de formatação. Esse argumento será a hora atual, fornecido pela função time.Now, da biblioteca padrão. Assim como no exemplo anterior, combinamos nossa mensagem de erro com um prefixo curto e imprimimos o resultado na saída padrão, usando a função fmt.PrintIn.

      Tratando os erros

      Normalmente, você não se depararia com um erro como esse, criado para ser usado imediatamente para uma única finalidade – como no exemplo anterior. Na prática, é muito mais comum criar um erro e retorná-lo de uma função quando algum erro ocorre. Chamadores dessa função usarão, então, uma instrução if para ver se o erro estava presente ou nil — um valor não inicializado.

      O próximo exemplo inclui uma função que sempre retorna um erro. Observe que, ao executar o programa, ele produz a mesma saída que a do exemplo anterior, embora uma função esteja retornando o erro desta vez. Declarar um erro em um local diferente não altera a mensagem de erro.

      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!")
      }
      

      Output

      An error occurred: barnacles

      Aqui, definimos uma função chamada boom(), que retorna um único error que criamos usando a função errors.New. Na sequência, chamamos essa função e capturamos o erro com a linha err := boom(). Depois de atribuir esse erro, fazemos uma verificação para descobrir se ele estava presente, usando a condicional if err ! condicional = nil. Aqui, a condicional sempre avaliará se é true, já que estamos sempre retornando um error com a função boom().

      Esse nem sempre será o caso, de modo que é recomendado ter casos de tratamento de lógica onde um erro não estiver presente (nil) e casos onde o erro estiver presente. Quando o erro estiver presente, usamos fmt.PrintIn para imprimir o erro juntamente com um prefixo, assim como fizemos em exemplos anteriores. Por fim, usamos uma instrução de return para ignorar a execução de fmt.PrintIn("Anchors away!"), já que ela só deve ser executada quando não ocorrer um erro.

      Nota: a instrução if err ! A criação de = nil, mostrada no último exemplo, é o pilar do tratamento de erros na linguagem de programação Go. Sempre que uma função puder produzir um erro, é importante usar uma instrução if para verificar se ocorreu um erro. Dessa forma, o código Go idiomático tem, naturalmente, sua lógica de “happy path”(caminho ideal) no primeiro nível de recuo e toda a lógica de “sad path”(caminho incorreto) no segundo de nível de recuo.

      Instruções if possuem uma cláusula de atribuição opcional que pode ser usada para ajudar a condensar a chamada de uma função e o tratamento de seus erros.

      Execute o próximo programa para ver a mesma saída que a do exemplo anterior, mas, desta vez, usando uma instrução if composta, visando reduzir um pouco textos clichê:

      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!")
      }
      

      Output

      An error occurred: barnacles

      Como anteriormente, temos uma função, boom(), que sempre retorna um erro. Atribuímos o erro retornado da função boom() para err como a primeira parte da instrução if. Na segunda parte da instrução if, após o ponto e vírgula, a variável err estará disponível. Fazemoz uma verificação para descobrir se o erro estava presente e imprimimos o erro com uma string de prefixo curto – como fizemos anteriormente.

      Nesta seção, aprendemos a lidar com as funções que apenas retornam um erro. Essas funções são comuns, mas também é importante conseguir tratar os erros das funções que podem retornar valores múltiplos.

      As funções que retornam um único valor de erro são, com frequência, aquelas que afetam alguma alteração de estado, como inserir linhas em um banco de dados. Também é comum escrever funções que retornam um valor se forem concluídas corretamente junto com um possível erro caso a função falhe. O Go permite que as funções retornem mais de um resultado, o que pode ser usado para retornar simultaneamente um valor e um tipo de erro.

      Para criar uma função que retorne mais de um valor, listamos os tipos de cada valor retornado dentro de parênteses, na assinatura da função. Por exemplo, uma função de capitalize que retorne uma string e um error seria declarada usando func capitalize(name string) (string error) {}. A parte (string, error) diz ao compilador do Go que essa função retornará uma string e um error, exatamente nessa ordem.

      Execute o programa a seguir para ver a saída de uma função que retorne uma string e um 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("sammy")
          if err != nil {
              fmt.Println("Could not capitalize:", err)
              return
          }
      
          fmt.Println("Capitalized name:", name)
      }
      

      Output

      Capitalized name: SAMMY

      Definimos capitalize() como uma função que recebe uma string (o nome a ser capitalizado) e retorna uma string e um valor de erro. Em main(), chamamos a função capitalize() e atribuimos os dois valores retornados da função para as variáveis name e err, separando-as com vírgulas à esquerda do operador :=. Depois disso, executamos nossa if err ! Verificação com = nil, como nos exemplos anteriores, imprimindo o erro na saída padrão usando fmt.Println se o erro estava presente. Se nenhum erro estava presente, imprimimos Capitalized name: SAMMY.

      Tente alterar a string "sammy" em name, err := capitalize("sammy") para a string vazia ("") e você receberá o erro Could not capitalize: no name provided.

      A função capitalize retornará um erro quando os chamadores da função fornecerem uma string vazia para o parâmetro name. Quando o parâmetro name não é a string vazia, capitalize() usa strings.ToTitle para capitalizar o parâmetro name e retorna nil para o valor de erro.

      Há certas convenções sutis que este exemplo segue e que são típicas do código Go, embora ainda não impostas pelo compilador Go. Quando uma função retorna vários valores, incluindo um erro, a convenção solicita que retornemos o valor error como o último item. Ao retornar um error de uma função com múltiplos valores de retorno, o código Go idiomático também definirá cada valor sem erro como valor zero. Os valores zero são, por exemplo, uma string vazia para strings, 0 para números inteiros, um struct vazio para tipos de struct e nil para interface e tipos de ponteiros, para citar alguns. Falamos dos valores de zero mais detalhadamente em nosso tutorial sobre variáveis e constantes.

      Reduzindo o clichê

      Seguir essas convenções pode se tornar maçante em situações nas quais há muitos valores para retornar de uma função. Podemos usar uma função anônima para ajudar a reduzir o clichê. As funções anônimas são procedimentos atribuídos às variáveis. Diferentemente das funções que definimos nos exemplos anteriores, elas só estão disponíveis dentro das funções nas quais você as declara, o que as torna perfeitas para atuarem como pequenas partes reutilizáveis de lógica auxiliar.

      O programa a seguir modifica o último exemplo para incluir o tamanho do nome que estamos capitalizando. Uma vez que ele possui três valores para retornar, tratar os erros pode se tornar complicado sem uma função anônima para nos ajudar:

      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("sammy")
          if err != nil {
              fmt.Println("An error occurred:", err)
          }
      
          fmt.Printf("Capitalized name: %s, length: %d", name, size)
      }
      

      Output

      Capitalized name: SAMMY, length: 5

      Em main(), capturamos agora os três argumentos retornados de capitalize como name, size e err, respectivamente. Verificamos, então, para descobrir se capitalize retornou um error, verificando se a variável err não era igual a nil. É importante fazer isso antes de tentar usar qualquer um dos outros valores retornados por capitalize, uma vez que a função anônima handle pode definir esses valores como zero. Uma vez que nenhum erro ocorreu porque fornecemos a string "sammy", imprimimos o nome em maiúsculas e seu tamanho.

      Novamente, você pode tentar alterar "sammy" para a string vazia ("") para ver o caso de erro impresso (An error occurred: no name provided).

      Em capitalize, definimos a variável handle como uma função anônima. Ela parte de um único erro e retorna valores idênticos na mesma ordem que retorna os valores de capitalize. A handle define esses valores para zero e encaminha o error transmitido como seu argumento como o valor final retornado. Usando isso, podemos então retornar quaisquer erros encontrados em capitalize, usando a instrução return antes da chamada para handle, tendo o error como seu parâmetro.

      Lembre-se que capitalize precisa sempre retornar três valores, já que é assim que definimos a função. Às vezes, não queremos lidar com todos os valores que uma função pode retornar. Felizmente, temos certa flexibilidade em como podemos usar esses valores na parte da atribuição.

      Quando uma função retorna muitos valores, o Go exige que você atribua cada um a uma variável. No último exemplo, fazemos isso fornecendo nomes para os dois valores retornados da função capitalize. Esses nomes devem ser separados por vírgulas e aparecer à esquerda do operador :=. O primeiro valor retornado de capitalize receberá a variável name e o segundo valor (o error) receberá a variável err. Às vezes, estamos interessados apenas no valor de erro. Você pode descartar quaisquer valores indesejados que as funções retornarem, usando o nome de variável _ especial.

      No programa a seguir, modificamos nosso primeiro exemplo envolvendo a função capitalize para produzir um erro, transmitindo a string vazia (""). Tente executar este programa para ver como podemos examinar apenas o erro, descartando o primeiro valor retornado com a variável _:

      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!")
      }
      

      Output

      Could not capitalize: no name provided

      Desta vez, na função main(), atribuimos o nome em maiúscula (a primeira string retornada) para a variável de sublinhado (_). Ao mesmo tempo, atribuímos o error retornado por capitalize para a variável err. Depois, verificamos se o erro estava presente na condicional if err ! condicional = nil. Uma vez que embutimos uma string vazia em código como um argumento para capitalize, na linha _, err := capitalize(""), essa condicional avaliará sempre quanto a se é true. Isso faz com que a saída "Could not capitalize: no name provided"(Não foi possível colocar em maiúscula: nenhum nome foi fornecido) seja impressa pela chamada para a função fmt.PrintIn no corpo da instrução if. O return depois disso ignorará a fmt.Println("Success!").

      Conclusão

      Aprendemos muitas formas de criar erros usando a biblioteca padrão e como criar funções que retornam erros de forma idiomática. Neste tutorial, conseguimos criar vários erros usando as funções errors.New e fmt.Errorf da biblioteca padrão. Nos próximos tutoriais, avaliaremos como criar nossos próprios tipos de erros personalizados para transmitir informações mais completas para os usuários.



      Source link