Liberando el manejo de errores eliminando errores



Go2 tiene como objetivo reducir la sobrecarga del manejo de errores, pero ¿sabía qué es mejor que la sintaxis mejorada para el manejo de errores?

No es necesario manejar los errores en absoluto. No digo "elimine su código de manejo de errores", en cambio sugiero cambiar su código para que no tenga muchos errores que manejar.

Este artículo se inspira en el capítulo "Definir errores fuera de existencia" del libro " Una filosofía de diseño de software " de John Ousterhout. Intentaré aplicar sus consejos a Go.

Primer ejemplo


Aquí está la función para contar el número de líneas en un archivo:

func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil } 

Creamos bufio.Reader, luego nos sentamos en un bucle, llamando al método ReadString, aumentando el contador hasta llegar al final del archivo, y luego devolvemos el número de líneas leídas. Este es el código que queríamos escribir, en cambio CountLines es complicado por el manejo de errores.

Por ejemplo, hay una construcción tan extraña:

 _, err = br.ReadString('\n') lines++ if err != nil { break } 

Aumentamos el número de líneas antes de buscar errores, esto parece extraño. La razón por la que debemos escribirlo de esta manera es porque ReadString devolverá un error si encuentra el final del archivo, io.EOF, antes de presionar un carácter de nueva línea. Esto también puede suceder si no hay una nueva línea.

Para resolver este problema, reorganizaremos la lógica para aumentar el número de filas y luego veremos si necesitamos salir del ciclo (esta lógica aún no es correcta, ¿puede encontrar un error?).

Pero no hemos terminado de buscar errores. ReadString devolverá io.EOF cuando llegue al final del archivo. Esto se espera, ReadString necesita alguna forma de decir alto, no hay nada más que leer. Por lo tanto, antes de devolver el error a la persona que llama de CountLine, debemos verificar si hubo un error io.EOF y, en este caso, devolverlo a la persona que llama, de lo contrario, devolveremos cero cuando todo esté bien. Es por eso que la última línea de la función no es fácil.

 return lines, err 

Creo que este es un buen ejemplo de la observación de Russ Cox de que el manejo de errores puede dificultar la función . Veamos la versión mejorada.

 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() } 

Esta versión mejorada pasa del uso de bufio.Reader a bufio.Scanner. Bajo el capó, bufio.Scanner usa bufio.Reader, agregando una capa de abstracción que ayuda a eliminar el manejo de errores, lo que obstaculizó el trabajo de nuestra versión anterior de CountLines (bufio.Scanner puede escanear cualquier plantilla, por defecto busca nuevas líneas).

El método sc.Scan () devuelve verdadero si el escáner encontró una línea de texto y no encontró un error. Por lo tanto, el cuerpo de nuestro bucle for solo se llamará cuando haya una línea de texto en el búfer del escáner. Esto significa que nuestro CountLines rehecho maneja correctamente el caso cuando no hay un carácter de nueva línea final. También ahora se maneja correctamente el caso cuando el archivo está vacío.

En segundo lugar, dado que sc.Scan devuelve falso cuando se produce un error, nuestro bucle for finalizará cuando se llegue al final del archivo o se produzca un error. Escriba bufio.Scanner recuerda el primer error detectado y lo solucionamos después de salir del bucle utilizando el método sc.Err ().

Finalmente, buffo.Scanner se encarga de procesar io.EOF y lo convierte en nulo si se llega al final del archivo sin un error.

Segundo ejemplo


Mi segundo ejemplo está inspirado en los errores de Rob Pikes.

Cuando se trabaja con archivos de apertura, escritura y cierre, el manejo de errores es, pero no muy impresionante, porque las operaciones pueden concluirse en ayudantes como ioutil.ReadFile e ioutil.WriteFile. Sin embargo, cuando se trabaja con protocolos de red de bajo nivel, a menudo es necesario construir una respuesta directamente usando primitivas de E / S, por lo que el manejo de errores puede comenzar a repetirse. Considere este fragmento de un servidor HTTP que crea una respuesta HTTP / 1.1:

 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } 

Primero creamos una barra de estado usando fmt.Fprintf y buscamos un error. Luego, para cada encabezado, registramos la clave y el valor del encabezado, cada vez que buscamos un error. Finalmente, finalizamos la sección del encabezado con un \ r \ n adicional, verificamos el error y copiamos el cuerpo de la respuesta al cliente. Finalmente, aunque no necesitamos verificar el error de io.Copy, necesitamos convertirlo de un formulario con dos valores de retorno, que io.Copy regresa al valor de retorno único que WriteResponse espera.

Esto no es solo un montón de trabajo repetitivo, cada operación, que esencialmente está escribiendo bytes en io.Writer, tiene una forma diferente de manejo de errores. Pero podemos facilitar nuestra tarea introduciendo un pequeño contenedor.

 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } 

errWriter cumple el contrato io.Writer, por lo que puede usarse para migrar un io.Writer existente. errWriter transfiere las grabaciones a su grabadora subyacente hasta que se detecta un error. De ahora en adelante, descarta cualquier entrada y devuelve el error anterior.

 func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err } 

La aplicación de errWriter a WriteResponse mejora enormemente la claridad del código. Cada una de las operaciones ya no necesita limitarse a la verificación de errores. El mensaje de error se mueve al final de la función, verificando el campo ew.err y evitando la molesta traducción de los valores devueltos de io.Copy

Conclusión


Cuando encuentre un manejo excesivo de errores, intente extraer algunas operaciones como un tipo de contenedor auxiliar.

Sobre el autor


El autor de este artículo, Dave Cheney , es autor de muchos paquetes populares para Go, como github.com/pkg/errors y github.com/davecheney/httpstat .

Source: https://habr.com/ru/post/440386/


All Articles