Propuesta: prueba - función incorporada de verificación de errores

Resumen


Se propone una nueva construcción de try que está específicamente diseñada para eliminar las expresiones if asociadas comúnmente con el manejo de errores en Go. Este es el único cambio de idioma. Los autores admiten el uso de funciones de biblioteca defer y estándar para enriquecer o ajustar errores. Esta pequeña extensión es adecuada para la mayoría de los escenarios, prácticamente sin complicar el lenguaje.


La construcción de try es fácil de explicar, fácil de implementar, esta funcionalidad es ortogonal a otras construcciones de lenguaje y es totalmente compatible con versiones anteriores. También es extensible si lo queremos en el futuro.


El resto de este documento está organizado de la siguiente manera: después de una breve introducción, damos una definición de la función incorporada y explicamos su uso en la práctica. La sección de discusión revisa sugerencias alternativas y el diseño actual. Al final, se darán las conclusiones y el plan de implementación con ejemplos y una sección de preguntas y respuestas.


Introduccion


En la última conferencia de Gophercon en Denver, los miembros del equipo Go (Russ Cox, Marcel van Lohuizen) presentaron algunas ideas nuevas sobre cómo reducir el tedio del manejo manual de errores en Go ( diseño borrador ). Desde entonces, hemos recibido una gran cantidad de comentarios.


Como Russ Cox explicó en su revisión del problema , nuestro objetivo es hacer que el manejo de errores sea más liviano al reducir la cantidad de código dedicado específicamente a la verificación de errores. También queremos que el código de manejo de errores de escritura sea más conveniente, aumentando la probabilidad de que los desarrolladores aún dediquen tiempo a corregir el manejo de errores. Al mismo tiempo, queremos dejar el código de manejo de errores claramente visible en el código del programa.


Las ideas discutidas en el borrador del borrador se concentran alrededor de la nueva declaración de check unaria, que simplifica la verificación explícita del valor de error obtenido de alguna expresión (generalmente una llamada a función), así como la declaración de manejadores de errores ( handle ) y un conjunto de reglas que conectan estas dos nuevas construcciones de lenguaje.


La mayoría de los comentarios que recibimos se centraron en los detalles y la complejidad del diseño del handle , y la idea de un operador de check resultó ser más atractiva. De hecho, varios miembros de la comunidad tomaron la idea de un operador de check y lo expandieron. Aquí hay algunas publicaciones más similares a nuestra oferta:


  • PeterRK propuso la primera propuesta escrita (conocida por nosotros) para usar la construcción de check lugar del operador en su publicación Partes clave del manejo de errores
  • No hace mucho tiempo, Markus propuso dos nuevas palabras clave, guard y must junto con el uso de defer para envolver errores en # 31442
  • También pjebs sugirió una construcción must en # 32219

La propuesta actual, aunque diferente en detalles, se basó en estos tres y, en general, en los comentarios recibidos sobre el proyecto de diseño propuesto el año pasado.


Para completar la imagen, queremos señalar que se pueden encontrar aún más sugerencias de manejo de errores en esta página wiki . También vale la pena señalar que Liam Breck vino con un amplio conjunto de requisitos para el mecanismo de manejo de errores.


Finalmente, después de la publicación de esta propuesta, supimos que Ryan Hileman implementó la try cinco años usando la herramienta de reescritura og y la usó con éxito en proyectos reales. Ver ( https://news.ycombinator.com/item?id=20101417 ).


Función de prueba incorporada


Oferta


Sugerimos agregar un nuevo elemento de lenguaje similar a una función llamado try y llamado con una firma


 func try(expr) (T1, T2, ... Tn) 

donde expr significa una expresión de un parámetro de entrada (generalmente una llamada a función) que devuelve n + 1 valores de los tipos T1, T2, ... Tn y error para el último valor. Si expr es un valor único (n = 0), este valor debe ser de tipo error y try no devuelve un resultado. Al llamar a try con una expresión que no devuelve el último valor de error de tipo, error produce un error de compilación.


La construcción try solo puede usarse en una función que devuelve al menos un valor, y cuyo último valor de retorno es de tipo error . Llamar a try en otros contextos conduce a un error de compilación.


Llama a try con la función f() como en el ejemplo


 x1, x2, … xn = try(f()) 

conduce al siguiente código:


 t1, … tn, te := f() // t1, … tn,  ()   if te != nil { err = te //  te    error return //     } x1, … xn = t1, … tn //     //     

En otras palabras, si el último tipo de error devuelto por expr es nil , try simplemente devuelve los primeros n valores, eliminando el nil final.


Si el último valor devuelto por expr no es nil , entonces:


  • El valor de retorno de error de la función de cierre (en el pseudocódigo mencionado anteriormente err , aunque puede ser cualquier identificador o valor de retorno sin nombre) recibe el valor de error devuelto por expr
  • hay una salida de la función envolvente
  • Si la función de cierre tiene parámetros de retorno adicionales, estos parámetros conservan los valores que contenían antes de la llamada de try .
  • si la función de cierre tiene parámetros de retorno sin nombre adicionales, se les devuelven los valores cero correspondientes (que es idéntico a guardar sus valores cero originales con los que se inicializan).

Si try usa en múltiples asignaciones, como en el ejemplo anterior, y se detecta un error distinto de cero (en adelante, nulo - aprox. Por.), La asignación (por variables de usuario) no se realiza y ninguna de las variables en el lado izquierdo de la asignación cambia. Es decir, try comporta como una llamada a una función: sus resultados están disponibles solo si try devuelve el control al llamante (a diferencia del caso con un retorno de la función de cierre). Como resultado, si las variables en el lado izquierdo de la asignación son parámetros de retorno, el uso de try dará como resultado un comportamiento diferente del código típico que se encuentra ahora. Por ejemplo, si a,b, err se denominan parámetros de retorno de una función de cierre, aquí está este código:


 a, b, err = f() if err != nil { return } 

siempre asignará valores a las variables a, b err , independientemente de si la llamada a f() devolvió un error o no. Desafío contrario


 a, b = try(f()) 

en caso de error, deje a y b sin cambios. A pesar de que este es un matiz sutil, creemos que tales casos son bastante raros. Si se requiere un comportamiento de asignación incondicional, debe continuar utilizando las expresiones if .


Uso


La definición de try explícitamente le dice cómo usarlo: muchas expresiones if que verifican retornos de error pueden reemplazarse con try . Por ejemplo:


 f, err := os.Open(filename) if err != nil { return …, err //       } 

se puede simplificar a


 f := try(os.Open(filename)) 

Si la función de llamada no devuelve un error, try no se puede utilizar (consulte la sección Discusión). En este caso, el error debe procesarse en cualquier caso localmente (ya que no hay retorno de error), y en este caso, if sigue siendo el mecanismo apropiado para verificar errores.


En términos generales, nuestro objetivo no es reemplazar todas las posibles verificaciones de errores con una try . El código que requiere una semántica diferente puede y debe seguir utilizándose if expresiones y variables explícitas con valores de error.


Prueba y prueba


En uno de nuestros intentos anteriores de escribir una especificación (ver la sección de iteración de diseño a continuación), try fue diseñado para entrar en pánico cuando ocurre un error cuando se usa dentro de una función sin un error de retorno. Esto permitió utilizar pruebas unitarias de prueba basadas en el paquete de testing de la biblioteca estándar.


Como una de las opciones, es posible usar funciones de prueba con firmas en el paquete de testing


 func TestXxx(*testing.T) error func BenchmarkXxx(*testing.B) error 

para permitir el uso de try en las pruebas. Una función de prueba que devuelve un error distinto de cero llamará implícitamente a t.Fatal(err) o b.Fatal(err) . Este es un pequeño cambio de biblioteca que evita la necesidad de diferentes comportamientos (retorno o pánico) para try , según el contexto.


Uno de los inconvenientes de este enfoque es que t.Fatal y b.Fatal no podrán devolver el número de línea en el que cayó la prueba. Otra desventaja es que también debemos cambiar las subpruebas de alguna manera. La solución a este problema es una pregunta abierta; No proponemos cambios específicos al paquete de testing en este documento.


Consulte también # 21111 , que sugiere permitir que las funciones de ejemplo devuelvan un error.


Manejo de errores


El diseño del borrador original se refería en gran medida al soporte de idiomas para envolver o aumentar los errores. El borrador propuso un nuevo handle palabras clave y una nueva forma de declarar controladores de errores . Esta nueva construcción del lenguaje atrajo problemas como las moscas debido a la semántica no trivial, especialmente cuando se considera su efecto en el flujo de ejecución. En particular, la funcionalidad del handle cruzó miserablemente con la función de defer , lo que hizo que la nueva característica del lenguaje no fuera ortogonal a todo lo demás.


Esta propuesta reduce el diseño original del borrador a su esencia. Si se requiere enriquecimiento o ajuste de errores, hay dos enfoques: adjuntar a if err != nil { return err} , o "declarar" un controlador de errores dentro de la expresión de defer :


 defer func() { if err != nil { //      -   err = … // /  } }() 

En este ejemplo, err es el nombre del parámetro de retorno de error tipo error función de cierre.


En la práctica, imaginamos funciones de ayuda como


 func HandleErrorf(err *error, format string, args ...interface{}) { if *err != nil { *err = fmt.Errorf(format + ": %v", append(args, *err)...) } } 

o algo similar El paquete fmt puede convertirse en un lugar natural para tales ayudantes (ya proporciona fmt.Errorf ). Con ayudantes, la definición de un controlador de errores en muchos casos se reducirá a una sola línea. Por ejemplo, para enriquecer el error de la función "copiar", puede escribir


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

if fmt.HandleErrorf agrega implícitamente información de error. Dicha construcción es bastante fácil de leer y tiene la ventaja de que puede implementarse sin agregar nuevos elementos de la sintaxis del lenguaje.


La principal desventaja de este enfoque es que se debe nombrar el parámetro de error devuelto, lo que potencialmente conduce a una API menos precisa (consulte las preguntas frecuentes sobre este tema). Creemos que nos acostumbraremos cuando se establezca el estilo apropiado de escritura de código.


La eficiencia difiere


Una consideración importante cuando se usa defer como un controlador de errores es la eficiencia. La expresión defer se considera lenta . No queremos elegir entre un código eficiente y un buen manejo de errores. Independientemente de esta propuesta, los equipos de compilación y tiempo de ejecución de Go discutieron métodos de implementación alternativos, y creemos que podemos hacer formas típicas de usar diferir para manejar errores comparables en eficiencia al código "manual" existente. Esperamos agregar una implementación más rápida de defer en Go 1.14 (ver también el ticket CL 171158 , que es el primer paso en esta dirección).


Casos especiales: go try(f), defer try(f)


La construcción try parece una función, y debido a esto, se espera que pueda usarse en cualquier lugar donde una llamada de función sea aceptable. Sin embargo, si la llamada try se usa en la declaración go , las cosas se complican:


 go try(f()) 

Aquí f() se ejecuta cuando la expresión go se ejecuta en la rutina actual, los resultados de llamar a f se pasan como argumentos para try , que comienza en la nueva rutina. Si f devuelve un error distinto de cero, se espera que try regrese de la función de cierre; sin embargo, no hay función (y no hay parámetro de retorno de error de tipo), porque El código se ejecuta en una gorutina separada. Debido a esto, proponemos deshabilitar try en una expresión go .


Situación con


 defer try(f()) 

parece similar, pero aquí la semántica de defer significa que la ejecución de try se retrasará hasta que regrese de la función de cierre. Como antes, f() evalúa cuando defer , y sus resultados se pasan al try diferido.


try comprueba el error f() devuelto solo en el último momento antes de regresar de la función de cierre. Sin cambiar el comportamiento de try , dicho error puede sobrescribir otro valor de error que la función de cierre intenta devolver. Esto, en el mejor de los casos, confunde, en el peor, provoca errores. Debido a esto, le proponemos que prohíba también el try llamada en la declaración de defer . Siempre podemos reconsiderar esta decisión si existe una aplicación razonable de dicha semántica.


Finalmente, al igual que el resto de las construcciones integradas, try solo se puede usar como una llamada; no se puede usar como una función de valor o en una expresión de asignación variable como en f := try (al igual que f := print y f := new están prohibidos).


La discusión


Iteraciones de diseño


La siguiente es una breve discusión de diseños anteriores que condujeron a la propuesta mínima actual. Esperamos que esto arroje luz sobre las decisiones de diseño seleccionadas.


Nuestra primera iteración de esta oración se inspiró en dos ideas del artículo "Partes clave del manejo de errores", a saber, usar la función incorporada en lugar del operador y la función Go habitual para manejar errores en lugar de la nueva construcción del lenguaje. A diferencia de esa publicación, nuestro controlador de errores tenía un func(error) error firma func(error) error fijo para simplificar las cosas. La función try llamaría a un controlador de errores si hubiera un error antes de que try saliera de la función de cierre. Aquí hay un ejemplo:


 handler := func(err error) error { return fmt.Errorf("foo failed: %v", err) //   } f := try(os.Open(filename), handler) //      

Si bien este enfoque permitió la definición de manejadores de errores efectivos definidos por el usuario, también planteó muchas preguntas que obviamente no tenían las respuestas correctas: ¿Qué debería suceder si se pasa nada al manejador? ¿Deberías try entrar try pánico o considerar esto como la falta de un controlador? ¿Qué sucede si se llama al controlador con un error distinto de cero y luego devuelve un resultado nulo? ¿Esto significa que el error está "cancelado"? ¿O debería una función de cierre devolver un error vacío? También hubo dudas de que la transferencia opcional de un controlador de errores alentaría a los desarrolladores a ignorar los errores en lugar de corregirlos. También sería fácil hacer el manejo correcto de errores en todas partes, pero omita un uso de try . Y similares


En la siguiente iteración, la capacidad de pasar un controlador de errores personalizado se eliminó a favor de usar defer para ajustar los errores. Esto parecía un mejor enfoque porque hacía que los manejadores de errores fueran mucho más notorios en el código fuente. Este paso también eliminó todos los problemas relacionados con la transferencia opcional de las funciones del controlador, pero exigió que se nombraran los parámetros devueltos con el tipo de error si se requería acceso (decidimos que esto era normal). Además, en un intento de hacer que try útil no solo dentro de las funciones que devuelven errores, fue necesario hacer que el comportamiento de try sensible al contexto: si try usó a nivel de paquete, o si se llamó dentro de una función que no devuelve un error, try automáticamente en pánico cuando se detecta un error. (Y como efecto secundario, debido a esta propiedad, la construcción del lenguaje se llamó must lugar de try en esa oración). El comportamiento sensible al contexto de try (o must ) parecía natural y también bastante útil: eliminaría muchas funciones definidas por el usuario utilizadas en expresiones inicializando variables de paquete. También abrió la posibilidad de usar las pruebas unitarias de prueba con el paquete de testing .


Sin embargo, el comportamiento sensible al contexto de try estaba lleno de errores: por ejemplo, el comportamiento de una función que usa try podría cambiar silenciosamente (pánico o no) al agregar o eliminar un error de retorno a la firma de la función. Esto parecía una propiedad demasiado peligrosa. La solución obvia era dividir la funcionalidad de try en dos funciones separadas de try y try , (muy similar a la forma en que se sugirió en # 31442 ). Sin embargo, esto requeriría dos funciones integradas, mientras que solo el try directamente relacionado con un mejor soporte para el manejo de errores.


Por lo tanto, en la iteración actual, en lugar de incluir la segunda función incorporada, decidimos eliminar la semántica dual de try y, por lo tanto, permitir su uso solo en funciones que devuelven un error.


Características del diseño propuesto.


Esta sugerencia es bastante corta y puede parecer un paso atrás en comparación con el borrador del año pasado. Creemos que las soluciones seleccionadas están justificadas:


  • Lo primero es lo primero, try tiene exactamente la misma semántica de la declaración de check propuesta en el original sin handle . Esto confirma la fidelidad del borrador original en uno de los aspectos importantes.


  • Elegir una función integrada en lugar de operadores tiene varias ventajas. No requiere una nueva palabra clave como check , lo que haría que el diseño sea incompatible con los analizadores existentes. Tampoco es necesario expandir la sintaxis de las expresiones con un nuevo operador. Agregar una nueva función incorporada es relativamente trivial y completamente ortogonal a otras características del lenguaje.


  • El uso de una función en línea en lugar de un operador requiere el uso de paréntesis. Deberíamos escribir try(f()) lugar de try f() . Este es el precio (pequeño) que tenemos que pagar por la compatibilidad con los analizadores existentes. Sin embargo, esto también hace que el diseño sea compatible con versiones futuras: si decidimos en el camino que pasar de alguna forma una función de manejo de errores o agregar un parámetro adicional para try para este propósito es una buena idea, agregar un argumento adicional a la llamada de try será trivial.


  • Al final resultó que, la necesidad de escribir corchetes tiene sus ventajas. En expresiones más complejas con múltiples llamadas de try , los paréntesis mejoran la legibilidad al eliminar la necesidad de tratar con la precedencia del operador, como en los siguientes ejemplos:



 info := try(try(os.Open(file)).Stat()) //   try info := try (try os.Open(file)).Stat() //  try   info := try (try (os.Open(file)).Stat()) //  try   

try , : try , .. try (receiver) .Stat ( os.Open ).


try , : os.Open(file) .. try ( , try os , , try try ).


, .. .


  • . , . , , , .

Conclusiones


. , . defer , .


Go - , . , Go append . append , . , . , try .


, , Go : panic recover . error try .


, try , , — — , . Go:


  • , try
  • -

, , . if -.


Implementación


:


  • Go.
  • try . , . .
  • go/types try . .
  • gccgo . ( , ).
  • .

- , . , . .


Robert Griesemer go/types , () cmd/compile . , Go 1.14, 1 2019.


, Ian Lance Taylor gccgo , .


"Go 2, !" , .


1 , , , Go 1.14 .


Ejemplos


CopyFile :


 func CopyFile(src, dst string) (err error) { defer func() { if err != nil { err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r := try(os.Open(src)) defer r.Close() w := try(os.Create(dst)) defer func() { w.Close() if err != nil { os.Remove(dst) //    “try”    } }() try(io.Copy(w, r)) try(w.Close()) return nil } 

, " ", defer :


 defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) 

( defer -), defer , .


printSum


 func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil } 

:


 func printSum(a, b string) error { fmt.Println( "result:", try(strconv.Atoi(a)) + try(strconv.Atoi(b)), ) return nil } 

main :


 func localMain() error { hex := try(ioutil.ReadAll(os.Stdin)) data := try(parseHexdump(string(hex))) try(os.Stdout.Write(data)) return nil } func main() { if err := localMain(); err != nil { log.Fatal(err) } } 

- try , :


 n, err := src.Read(buf) if err == io.EOF { break } try(err) 


, .


: ?


: check handle , . , handle defer , handle .


: try ?


: try Go . - , . , . , " ". try , .. .


: try try?


: , check , must do . try , . try check (, ), - . . must ; try — . , Rust Swift try ( ). .


: ? Rust?


: Go ; , Go ( ; - ). , ? , . , , , (package, interface, if, append, recover, ...), , (struct, var, func, int, len, image, ..). Rust ? try — Go, , ( ) . , ? . , , (, ..) . . , .


: ( error) , defer , go doc. ?


: go doc , - ( _ ) , . , func f() (_ A, _ B, err error) go doc func f() (A, B, error) . , , , . , , . , , , -, (deferred) . Jonathan Geddes try() .


: defer ?


: defer . , , defer "" . . CL 171758 , defer 30%.


: ?


: , . , ( , ), . defer , . defer - https://golang.org/issue/29934 ( Go 2), .


: , try, error. , ?


: error ( ) , , nil . try . ( , . - ).


: Go , try ?


: try , try . super return -, try Go . try . .


: try , . ?


: try ; , . try ( ), . , if .


: , . try, defer . ?


: , . .


: try ( catch )?


: try — ("") , , ( ) . try ; . . "" . , . , try — . , , throw try-catch Go. , (, ), ( ) , . "" try-catch , . , , . Go . panic , .

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


All Articles