One place for hosting & domains

      errores

      Crear errores personalizados en Go


      Introducción

      Go ofrece dos métodos para crear errores en la biblioteca estándar: errors.New y fmt.Errorf. Cuando comunica información de error más complicada a sus usuarios, o a usted mismo al realizar una depuración, a veces estos dos mecanismos no son suficientes para capturar e informar de manera adecuada lo que sucedió. Para expresar esta información de error más compleja, y ampliar la funcionalidad, podemos implementar el tipo de interfaz de biblioteca estándar: error.

      La sintaxis para esto sería la siguiente:

      type error interface {
        Error() string
      }
      

      El paquete builtin define error como una interfaz con un único método Error() que muestra un mensaje de error como una cadena. Al implementar este método, podemos transformar cualquier tipo que definamos en un error propio.

      Intentaremos ejecutar el siguiente ejemplo para ver una implementación de la interfaz 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 el siguiente resultado:

      Output

      unexpected error: err: boom exit status 1

      Aquí, hemos creado un nuevo tipo de estructura vacía, MyError, y hemos definido el método Error() en ella. El método Error() muestra la cadena "boom".

      En main(), invocamos la función sayHello que muestra una cadena vacía y una nueva instancia de MyError. Ya que sayHello siempre mostrará un error, la invocación fmt.PrintIn dentro del cuerpo de la declaración if en main() siempre se ejecutará. Utilizaremos fmt.PrintIn para imprimir la cadena de prefijo corto "unexpected error:" junto con la instancia de MyError que está en la variable err.

      Observe que no tenemos que invocar directamente Error(), ya que el paquete fmt puede detectar automáticamente que esta es una implementación de error. Invoca Error() de forma transparente para obtener la cadena "boom" y la concatena con la cadena de prefijo "unexpected error: err:".

      Recopilar información detallada en un error personalizado

      A veces, un error personalizado es la alternativa más prolija para capturar información detallada de un error. Por ejemplo, supongamos que queremos capturar el código de estado de los errores producidos por una solicitud HTTP. Ejecute el siguiente programa para ver una implementación de error que nos permita capturar de forma prolija esa información:

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

      Verá el siguiente resultado:

      Output

      status 503: err unavailable exit status 1

      En este ejemplo, creamos una nueva instancia de RequestError y proporcionamos el código de estado y un error usando la función errors.New de la biblioteca estándar. A continuación, imprimimos esto usando fmt.PrintIn como en los ejemplos anteriores.

      En el método Error() de RequestError, usamos la función fmt.Sprintf para construir una cadena empleando la información proporcionada cuando se creó el error.

      Aserciones de tipo y errores personalizados

      La interfaz error expone solo un método, pero es posible que necesitemos acceder a otros métodos de implementaciones de error para gestionar un error de forma correcta. Por ejemplo, es posible que tengamos varias implementaciones personalizadas de error que sean temporales y puedan probarse nuevamente, denotadas por la presencia de un método Temporary().

      Las interfaces proporcionan una vista reducida del conjunto, más amplio, de métodos proporcionados por los tipos, de modo que debemos usar una_ aserción de tipo_ para cambiar los métodos que la vista muestra o para eliminarla completamente.

      El siguiente ejemplo aumenta el RequestError previamente mostrado para tener un método Temporary() que indicará si quienes realizan la invocación deberian intentar realizar nuevamente la solicitud o no:

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

      Verá el siguiente resultado:

      Output

      unavailable This request can be tried again exit status 1

      En main(), invocamos doRequest() que muestra una interfaz error. Primero, imprimimos el mensaje de error mostrado por el método Error(). A continuación, intentamos exponer todos los métodos de RequestError usando la afirmación de tipo re, ok := err.( *RequestError). Si la aserción de tipo se realizó correctamente, usaremos el método Temporary() para ver si este error es temporal. Debido a que el StatusCode establecido por doRequest() es 503, que coincide con http.StatusServiceUnavailable, con esto se muestra true y se imprime "This request can be tried again". En la práctica, en vez de eso, realizaríamos otra solicitud en vez de imprimir un mensaje.

      Ajustar errores

      Comúnmente, un error se generará a partir de algo externo a su programa, como una base de datos y una conexión de red, entre otros ejemplos. Los mensajes de error proporcionados a partir de estos errores no ayudan a encontrar el origen del error. Ajustar los errores con información adicional al principio de un mensaje de error proporcionará el contexto necesario para realizar correctamente la depuración.

      En el siguiente ejemplo, se demuestra la forma en que podemos añadir información contextual a un error, mostrado por alguna otra función, que de otra forma sería enigmático.

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

      Verá el siguiente resultado:

      Output

      main: boom!

      WrappedError es una estructura con dos campos: un mensaje de contexto como una string y un error sobre el cual WrappedError proporciona más información. Cuando se invoca el método Error(), de nuevo usamos fmt.Sprintf para imprimir el mensaje de contexto; luego el error (fmt.Sprintf sabe cómo invocar de forma implícita el método Error() también).

      En main(), creamos un error usando errors.New y luego ajustamos ese error usando la función Wrap que definimos. Esto nos permite indicar que este error se generó en "main". Además, ya que nuestro WrappedError es también un error, podríamos ajustar otro WrappedError; esto nos permitiría ver una cadena para poder rastrear el origen del error. Con algo de ayuda de la biblioteca estándar, podemos incluso integrar seguimientos de pila completos en nuestros errores.

      Conclusión

      Debido a que la interfaz error es solo un método único, hemos visto que disponemos de una gran flexibilidad para proporcionar diferentes tipos de error para diferentes situaciones. Esto puede abarcar todo, desde la comunicación de varios fragmentos de información como parte de un error hasta la implementación de un retroceso exponencial. Aunque los mecanismos de gestión de errores en Go pueden parecer, a primera vista, simplistas, podemos lograr un manejo bastante bueno usando estos errores personalizados para gestionar situaciones comunes y atípicas.

      Go tiene otro mecanismo para comunicar el comportamiento inesperado: los panics. En nuestro siguiente artículo de la serie de gestión de errores, veremos los panics, qué son y la forma gestionarlos.



      Source link

      Cómo manejar errores en Go


      Un código sólido debe reaccionar de forma adecuada en circunstancias imprevistas, como entradas incorrectas de los usuarios o conexiones de red o discos defectuosos. El manejo de errores es el proceso de identificar cuando sus programas se encuentran en un estado imprevisto y de tomar medidas para registrar información de diagnóstico para una depuración posterior.

      A diferencia de otros lenguajes que requieren que los desarrolladores manejen los errores con una sintaxis especial, los errores en Go son valores del tipo error que se devuelven de funciones como cualquier otro valor. Para manejar errores en Go, debemos examinar los errores que pueden devolver las funciones, determinar si se produjo un error y tomar las medidas adecuadas para proteger los datos e informarles a los usuarios y los operadores que se produjo un error.

      Creación de errores

      Para poder manejar errores, debemos crear algunos primero. La biblioteca estándar ofrece dos funciones incorporadas para crear errores: errors.New y fmt.Errorf. Estas dos funciones le permiten especificar un mensaje de error personalizado para presentarlo, posteriormente, a sus usuarios.

      errors.New tiene un solo argumento: un mensaje de error con una cadena que puede personalizar para avisarles a sus usuarios cuál fue el problema.

      Intente ejecutar el ejemplo que se indica a continuación para ver un error creado por errors.New incorporado a una salida estándar:

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

      Output

      Sammy says: barnacles

      Usamos la función errors.New de la biblioteca estándar para crear un nuevo mensaje de error con la cadena "barnacles" como mensaje de error. Aquí, seguimos la convención al usar minúsculas para el mensaje de error, tal como se sugiere en la Guía de Estilo del Lenguaje de Programación Go.

      Por último, usamos la función fmt.Println para combinar nuestro mensaje de error con "Sammy says:".

      La función fmt.Errorf le permite crear un mensaje de error de forma dinámica. Su primer argumento es una cadena que contiene su mensaje de error con valores de marcadores de posición, como %s para cadenas y %d para enteros. fmt.Errorf interpola los argumentos que siguen esta cadena de formato en esos marcadores de posición en orden:

      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 la función fmt.Errorf para crear un mensaje de error que incluya la hora actual. La cadena de formato que proporcionamos a fmt.Errorf contiene la directiva de formato %v que le indica a fmt.Errorf que use el formato predeterminado para el primer argumento proporcionado después de la cadena de formato. Ese argumento será la hora actual, que se proporciona mediante la función time.Now de la biblioteca estándar. De manera similar al ejemplo anterior, combinamos nuestro mensaje de error con un prefijo corto y enviamos el resultado a la salida estándar utilizando la función fmt.Println.

      Manejo de errores

      En general, no vería un error creado como este para utilizarlo de inmediato con ningún otro propósito, como en el ejemplo anterior. En la práctica, es mucho más frecuente crear un error y devolverlo de una función cuando ocurre un problema. Entonces, quienes invoquen esa función, utilizarán una instrucción if para ver si el error ocurrió o nil, un valor sin inicializar.

      El ejemplo siguiente incluye una función que siempre devuelve un error. Cuando ejecute el programa, observe que produce la misma salida que el ejemplo anterior, a pesar de que, ahora, el error se está devolviendo de una función. La declaración de un error en una ubicación distinta no modifica el mensaje del error.

      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

      Aquí, definimos una función denominada boom() que devuelve un único error que creamos utilizando errors.New. Luego, llamamos a esta función y captamos el error con la línea err := boom(). Una vez asignemos este error, comprobaremos si estaba presente con el condicional if err ! = nil. Aquí, el condicional siempre evaluará a true, dado que siempre estamos devolviendo un error de boom().

      Esto no siempre será así, por lo que es recomendable contar con casos lógicos de manipulación en los que el error no esté presente (nil) y casos en los que lo esté. Cuando el error está presente, usamos fmt.Println para mostrar nuestro error junto con un prefijo, como hemos hecho en ejemplos anteriores. Por último, usamos una instrucción return para omitir la ejecución de fmt.Println("Anchors away!"), dado que solo se debe ejecutar cuando no se produce ningún error.

      Nota: La construcción if err ! = nil que se muestra en el último ejemplo es el caballo de batalla de la manipulación de errores en el lenguaje de programación Go. Donde sea que una función pueda producir un error, es importante utilizar una instrucción if para determinar su presencia. De esta manera, el código idiomático Go tiene, naturalmente, su lógica “happy path” en el primer nivel de indentación, y toda la lógica “sad path”, en el segundo.

      Las instrucciones if tienen una cláusula opcional de asignación que puede utilizarse para ayudar a resumir la invocación de una función y el manejo de sus errores.

      Ejecute el siguiente programa para ver la misma salida de nuestro ejemplo anterior, pero, esta vez, con una instrucción if compuesta para reducir el texto estándar:

      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

      Al igual que antes, tenemos una función, boom(), que siempre devuelve un error. Asignamos el error devuelto de boom() a err como la primera parte de la instrucción if. De esta manera, esa variable err está disponible en la segunda parte de la instrucción if, después del punto y coma. Comprobamos si el error estaba presente y mostramos nuestro error con una cadena de prefijo corta, como hemos hecho anteriormente.

      En esta sección, aprendimos a manejar funciones que solo devuelven un error. Estas funciones son habituales, pero también es importante poder manejar errores de funciones que pueden devolver varios valores.

      Devolución de errores junto con valores

      Las funciones que devuelven un solo valor de error suelen ser las que producen algún cambio de estado, como la inserción de filas a una base de datos. También es frecuente escribir funciones que devuelven un valor si se completaron con éxito junto con un posible error si la función falló. Go permite que las funciones devuelvan más de un resultado, lo que puede utilizarse para devolver simultáneamente un valor y un tipo de error.

      Para crear una función que devuelva más de un valor, enumeramos los tipos de cada valor devuelto dentro de paréntesis en la firma de la función. Por ejemplo, una función capitalize que devuelve una string y un error se declararía utilizando func capitalize(name string) (string, error) {}. La parte (string, error) le indica al compilador de Go que esta función devolverá una string y un error, en ese orden.

      Ejecute el programa que se indica a continuación para ver la salida de una función que devuelve una string y un 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 una función que toma una cadena (el nombre que se escribirá en mayúsculas), y devuelve una cadena y un valor de error. En main(), invocamos capitalize() y asignamos los dos valores devueltos de la función a las variables name y err al separarlos con comas en el lado izquierdo del operador :=. A continuación, ejecutamos nuestra comprobación if err ! = nil, como en ejemplos anteriores, y mostramos el error en la salida estándar utilizando fmt.Println si el error estaba presente. Si no hubo errores, mostramos Capitalized name: SAMMY.

      Intente cambiar la cadena "sammy" en name, err := capitalize("sammy") por la cadena vacía ("") y recibirá, en su lugar, el error Could not capitalize: no name provided.

      La función capitalize devolverá un error cuando quienes invoquen la función proporcionen una cadena vacía para el parámetro name. Cuando el parámetro name no es la cadena vacía, capitalize() utiliza strings.ToTitle para escribir, en mayúsculas, el parámetro name y devuelve nil como valor de error.

      Hay algunas convenciones sutiles que siguen este ejemplo que son típicas del código Go, pero el compilador de Go no las aplica. Cuando una función devuelve varios valores, con un error incluido, la convención requiere que el error se devuelva como último elemento. Al devolver un error de una función con varios valores de retorno, el código idiomático Go también establecerá un valor de cero para cada valor non-error. Los valores de cero son, por ejemplo, una cadena vacía para cadenas, 0 para enteros, una estructura vacía para tipos de estructura y nil para tipos de punteros e interfaces, por nombrar algunos. Abordaremos los valores de cero con más detalle en nuestro tutorial sobre variables y constantes.

      Reducción del texto estándar

      Respetar estas convenciones puede volverse tedioso en situaciones en las que se devuelven muchos valores de una función. Podemos utilizar una función anónima para ayudar a reducir el texto estándar. Las funciones anónimas son procedimientos que se asignan a variables. A diferencia de las funciones que definimos en ejemplos anteriores, solo están disponibles en las funciones donde se declaran. Esto las hace perfectas para actuar como fragmentos cortos de lógica auxiliar reutilizable.

      El programa que se indica a continuación modifica el último ejemplo para incluir la longitud del nombre que escribiremos en mayúsculas. Dado que tiene tres valores que devolver, el manejo de errores podría tornarse engorroso sin una función anónima de ayuda:

      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

      Ahora, dentro de main(), capturamos los tres argumentos devueltos de capitalize como name, size y err, respectivamente. Luego, comprobamos si capitalize devolvió un error al verificar si la variable err era distinta de nil. Es importante hacerlo antes de intentar utilizar cualquiera de los demás valores que devuelve capitalize, dado que la función anónima, handle, podría establecer esos valores en cero. Dado que no ocurrió ningún error porque proporcionamos la cadena "sammy", mostramos el nombre en mayúsculas y su longitud.

      Una vez más, puede intentar cambiar "sammy" por la cadena vacía ("") para mostrar el caso de error (An error occurred: no name provided).

      Dentro de capitalize, definimos la variable handle como una función anónima. Toma un solo error y devuelve valores idénticos en el mismo orden que los valores de retorno de capitalize. handle establece esos valores en cero y reenvía el error pasado como su argumento, como el valor de retorno final. Al usar esto, podemos devolver cualquier error que se detecte en capitalize al utilizar la instrucción return delante de la invocación a handle con el error como su parámetro.

      Recuerde que capitalize siempre debe devolver tres valores, dado que así es como definimos la función. A veces, no queremos lidiar con todos los valores que una función podría devolver. Afortunadamente, tenemos cierta flexibilidad en la manera en que podemos utilizar estos valores en el lado de la asignación.

      Manejo de errores de funciones que devuelven varios valores

      Cuando una función devuelve muchos valores, Go requiere que asignemos una variable a cada uno de ellos. En el último ejemplo, lo hacemos al proporcionar nombres para los dos valores que devuelve la función capitalize. Estos nombres se deben separar con comas y tienen que aparecer en lado izquierdo del operador :=. El primer valor devuelto de capitalize se asignará a la variable name, y el segundo valor (el error) se asignará a la variable err. A veces, solo nos interesa el valor de error. Puede descartar cualquier valor no deseado que devuelva una función al utilizar el nombre de variable especial _.

      En el programa que se indica a continuación, modificamos nuestro primer ejemplo con la función capitalize para producir un error al pasar la cadena vacía (""). Intente ejecutar este programa para ver cómo podemos examinar solo el error al descartar el primer valor devuelto con la variable _:

      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

      Esta vez, dentro de la función main(), asignamos el nombre en mayúsculas (la string que se devuelve primero) a la variable de guion bajo (_). Al mismo tiempo, asignamos el error devuelto por capitalize a la variable err. A continuación, comprobamos si el error estaba presente en el condicional if err ! = nil. Dado que predefinimos una cadena vacía en el código como argumento de capitalize en la línea _, err := capitalize(""), este condicional siempre evaluará a true. Esto produce la salida "Could not capitalize: no name provided" que se muestra al invocar la función fmt.Println dentro del cuerpo de la instrucción if. Después de esto. el return omitirá fmt.Println("Success!").

      Conclusión

      Hemos visto muchas maneras de crear errores utilizando la biblioteca estándar y cómo crear funciones que devuelvan errores de una manera idiomática. En este tutorial, hemos logrado crear con éxito varios errores utilizando la biblioteca estándar errors.New y funciones fmt.Errorf. En tutoriales futuros, veremos cómo crear nuestros propios tipos de errores personalizados para transmitir más información a los usuarios.



      Source link