Un ir tan excepcional

Recientemente se han publicado borradores del diseño del nuevo manejo de errores en Go 2. Es muy agradable que el lenguaje no se ubique en un solo lugar: se desarrolla y cada año crece mejor a pasos agigantados.


Solo ahora, mientras que Go 2 solo es visible en el horizonte, es muy doloroso y triste esperar. Por lo tanto, tomamos el asunto en nuestras propias manos. Un poco de generación de código, un poco de trabajo con ast, y con un ligero movimiento del pánico de la mano, el pánico se convierte ... ¡en elegantes excepciones!



E inmediatamente quiero hacer una declaración muy importante y absolutamente seria.
Esta decisión es puramente entretenida y educativa por naturaleza.
Me refiero a solo 4 diversión. Esto es generalmente una prueba de concepto, en verdad. Advertí :)

Entonces que paso


El resultado fue un pequeño generador de código de biblioteca . Y los generadores de códigos, como todos saben, llevan dentro de sí bondad y gracia. En realidad no, pero en el mundo Go son bastante populares.


Configuramos un generador de código de este tipo en go-raw. Lo analiza por la ayuda del módulo estándar go/ast , hace algunos no astutas transformaciones, el resultado se escribe al lado del archivo, agregando el sufijo _jex.go . Los archivos resultantes quieren un pequeño tiempo de ejecución para funcionar.


De esta manera simple, agregamos excepciones a Go.


Nosotros usamos


Conectamos el generador al archivo, en el encabezado (antes del package ) escribimos


 //+build jex //go:generate jex 

Si ahora ejecuta el comando go generate -tags jex , se jex utilidad jex . Toma el nombre del archivo de os.Getenv("GOFILE") , lo come, lo digiere y escribe {file}_jex.go . El archivo recién nacido ya tiene //+build !jex en el encabezado (la etiqueta está invertida), así que go build , y en el compartimento con él los otros comandos, como go test o go install , tengan en cuenta solo los archivos nuevos y correctos. Lepota ...


Ahora dot-import github.com/anjensan/jex .
Sí, mientras que la importación a través de un punto es obligatoria. En el futuro se planea dejar lo mismo.


 import . "github.com/anjensan/jex" 

Genial, ahora puede insertar llamadas a las funciones de código auxiliar TRY , THROW , EX en el código. Por todo esto, el código sigue siendo sintácticamente válido, e incluso se compila en una forma no procesada (simplemente no funciona), por lo que la autocompletar está disponible y las linters realmente no juran. Los editores también mostrarían documentación para estas funciones, si tan solo tuvieran una.


Lanzar una excepción


 THROW(errors.New("error name")) 

Atrapa la excepción


 if TRY() { //   } else { fmt.Println(EX()) } 

Se genera una función anónima debajo del capó. Y en ello defer . Y tiene una función más. Y en ella recover ... Bueno, todavía hay un poco de magia mágica para manejar el return y defer .


Y sí, por cierto, ¡son compatibles!


Además, hay una macro variable especial ERR . Si le asigna un error, se produce una excepción. Es más fácil llamar a funciones que aún devuelven un error de la manera anterior


 file, ERR := os.Open(filename) 

Además, hay un par de pequeñas bolsas de utilidad ex y must , pero no hay mucho de qué hablar.


Ejemplos


Aquí hay un ejemplo del código Go correcto e idiomático


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

Este código no es tan agradable y elegante. Por cierto, ¡esta no es solo mi opinión!
Pero jex nos ayudará a mejorarlo.


 func CopyFile_(src, dst string) { defer ex.Logf("copy %s %s", src, dst) r, ERR := os.Open(src) defer r.Close() w, ERR := os.Create(dst) if TRY() { ERR := io.Copy(w, r) ERR := w.Close() } else { w.Close() os.Remove(dst) THROW() } } 

Pero por ejemplo, el siguiente programa


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

puede reescribirse como


 func main() { if TRY() { hex, ERR := ioutil.ReadAll(os.Stdin) data, ERR := parseHexdump(string(hex)) os.Stdout.Write(data) } else { log.Fatal(EX()) } } 

Aquí hay otro ejemplo para sentir mejor la idea propuesta. Código original


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

puede reescribirse como


 func printSum_(a, b string) { x, ERR := strconv.Atoi(a) y, ERR := strconv.Atoi(b) fmt.Println("result:", x + y) } 

o incluso eso


 func printSum_(a, b string) { fmt.Println("result:", must.Int_(strconv.Atoi(a)) + must.Int_(strconv.Atoi(b))) } 

Excepción


La conclusión es una estructura de contenedor simple sobre una instancia de error .


 type exception struct { //  ,   err error //  ^W , ,    log []interface{} //      ,    suppress []*exception } 

Un punto importante es que los ataques de pánico ordinarios no se perciben como excepciones. Por lo tanto, todos los errores estándar como runtime.TypeAssertionError no son una excepción. Esto está en línea con las mejores prácticas aceptadas en Go: si tenemos, por ejemplo, una nula desreferencia, entonces abandonamos todo el proceso alegre y alegremente. Fiable y predecible. Aunque no estoy seguro, quizás valga la pena revisar este momento y detectar tales errores. Tal vez opcional?


Y aquí hay un ejemplo de una cadena de excepción.


 func one_() { THROW(errors.New("one")) } func two_() { THROW(errors.New("two") } func three() { if TRY() { one_() } else { two_() } } 

Aquí manejamos con calma la excepción one , como de repente bam ... y two lanza la excepción two . Por lo tanto, la fuente one adjuntará suppress en el campo suppress . Nada se perderá, todo irá a los registros. Por lo tanto, no hay una necesidad particular de insertar toda la cadena de errores directamente en el texto del mensaje utilizando el patrón muy popular fmt.Errorf("blabla: %v", err) . Aunque nadie, por supuesto, no prohíbe su uso aquí, si realmente lo desea.


Cuando olvidé atrapar


Ah, otro punto muy importante. Para aumentar la legibilidad, hay una comprobación adicional: si una función puede generar una excepción, entonces su nombre debe terminar con _ . Un nombre deliberadamente torcido que le dirá al programador "querido señor, aquí en su programa algo puede salir mal, ¡por favor sea cuidadoso y diligente!"


Se inicia automáticamente una comprobación para los archivos transformados, además, también se puede iniciar manualmente en un proyecto utilizando el jex-check . Quizás tenga sentido ejecutarlo como parte del proceso de compilación junto con otros linters.


La comprobación de comentarios está //jex:nocheck . Esto, por cierto, es la única forma de lanzar excepciones desde una función anónima.


Por supuesto, esto no es una panacea para todos los problemas. Checker se perderá esto


 func bad_() { THROW(errors.New("ups")) } func worse() { f := bad_ f() } 

Por otro lado, no es mucho peor que la verificación estándar de err declared and not used , que es muy fácil de eludir.


 func worse() { a, err := foo() if err != nil { return err } b, err := bar() //  ,    ok... go vet, ? } 

En general, esta pregunta es bastante filosófica, qué es mejor hacer cuando olvidó procesar el error: ignórelo silenciosamente o genere pánico ... Por cierto, los mejores resultados de la prueba podrían lograrse implementando un soporte de excepción en el compilador, pero esto está mucho más allá del alcance de este artículo .


Algunos pueden decir que, aunque esta es una solución maravillosa, ya no es una excepción, porque ahora las excepciones significan una implementación muy específica. Bueno, ahí, porque los rastros de la pila no están unidos a las excepciones, o hay un linter separado para verificar los nombres de las funciones, o que la función puede terminar con _ pero no arroja excepciones, o no hay soporte directo en la sintaxis, o que realmente es pánico, y el pánico no es una excepción en absoluto, porque el gladiolo ... Las esporas pueden ser tan calientes como inútiles e inútiles. Por lo tanto, los dejaré detrás de la pizarra del artículo y continuaré llamando a la solución descrita de manera selectiva llamada "excepciones".


Sobre las carreras


A menudo, los desarrolladores, para simplificar la depuración, pegan un seguimiento de pila a implementaciones de error personalizadas. Incluso hay varias bibliotecas populares para esto. Pero, afortunadamente, con excepciones, esto no requiere ninguna acción adicional debido a una característica interesante de Go: durante el pánico, los bloques defer ejecutan en el contexto de pila del código que arrojó el pánico. Por lo tanto aquí


 func foo_() { THROW(errors.New("ups")) } func bar() { if TRY() { foo_() } else { debug.PrintStack() } } 

Se imprimirá un seguimiento completo de la pila, aunque sea un poco detallado (corté los nombres de los archivos)


  runtime/debug.Stack runtime/debug.PrintStack main.bar.func2 github.com/anjensan/jex/runtime.TryCatch.func1 panic main.foo_ main.bar.func1 github.com/anjensan/jex/runtime.TryCatch main.bar main.main 

No está de más hacer su propio ayudante para formatear / imprimir un seguimiento de la pila, teniendo en cuenta las funciones sustitutas, ocultándolas para facilitar su lectura. Creo que una buena idea, escribió.


O puede tomar la pila y adjuntarla a la excepción usando ex.Log() . Entonces, tal excepción se puede transferir a otra horoutin: no se pierden los estrechos.


 func foobar_() { e := make(chan error, 1) go func() { defer close(e) if TRY() { checkZero_() } else { EX().Log(debug.Stack()) //   e <- EX().Wrap() //     } }() ex.Must_(<-e) //  ,  ,  } 

Desafortunadamente


Eh ... por supuesto, algo así se vería mucho mejor


  try { throw io.EOF, "some comment" } catch e { fmt.Printf("exception: %v", e) } 

Pero, ay, ah, la sintaxis de Go no es extensible.
[pensativo] Aunque, probablemente, es para mejor ...


En cualquier caso, tienes que pervertir. Una de las ideas alternativas era hacer


  TRY; { THROW(io.EOF, "some comment") }; CATCH; { fmt.Printf("exception: %v", EX) } 

Pero ese código parece bastante tonto después de go fmt . Y el compilador jura cuando ve return en ambas ramas. No hay tal problema con if-TRY .


Sería genial reemplazar la macro ERR con la función MUST (mejor que solo). Para escribir


  return MUST(strconv.Atoi(a)) + MUST(strconv.Atoi(b)) 

En principio, esto todavía es factible, uno puede derivar el tipo de expresiones durante el análisis de ast, generar una función de envoltura simple para todos los tipos de tipos, como los declarados en el paquete must , y luego reemplazar MUST con el nombre de la función sustituta correspondiente. Esto no es del todo trivial, pero es completamente posible ... Solo los editores / ide no podrán entender dicho código. Después de todo, la firma de la función STUB MUST no es expresable dentro del sistema de tipo Go. Y por lo tanto no autocompletar.


Debajo del capó


Se agrega una nueva importación a todos los archivos procesados.


  import _jex "github.com/anjensan/jex/runtime" 

La llamada THROW reemplaza por panic(_jex.NewException(...)) . EX() también se reemplaza por el nombre de la variable local que contiene la excepción capturada.


Pero if TRY() {..} else {..} procesa un poco más complicado. Primero, se produce un procesamiento especial para todas las return y defer . Luego las ramas procesadas si se colocan en funciones anónimas. Y luego estas funciones se pasan a _jex.TryCatch(..) . Aqui esta


 func test(a int) (int, string) { fmt.Println("before") if TRY() { if a == 0 { THROW(errors.New("a == 0")) } defer fmt.Printf("a = %d\n", a) return a + 1, "ok" } else { fmt.Println("fail") } return 0, "hmm" } 

se convierte en algo como esto (eliminé los //line comentarios de //line ):


 func test(a int) (_jex_r0 int, _jex_r1 string) { var _jex_ret bool fmt.Println("before") var _jex_md2502 _jex.MultiDefer defer _jex_md2502.Run() _jex.TryCatch(func() { if a == 0 { panic(_jex.NewException(errors.New("a == 0"))) } { _f, _p0, _p1 := fmt.Printf, "a = %d\n", a _jex_md2502.Defer(func() { _f(_p0, _p1) }) } _jex_ret, _jex_r0, _jex_r1 = true, a+1, "ok" return }, func(_jex_ex _jex.Exception) { defer _jex.Suppress(_jex_ex) fmt.Println("fail") }) if _jex_ret { return } return 0, "hmm" } 

Mucho, no hermoso, pero funciona. De acuerdo, no todo y no siempre. Por ejemplo, no puede hacer defer-recover diferida dentro de TRY, ya que la llamada a la función se convierte en una lambda adicional.


Además, al mostrar el árbol ast, se indica la opción "guardar comentarios". Entonces, en teoría, go/printer debería imprimirlos ... Lo que honestamente hace, la verdad es muy, muy torcida =) No daré ejemplos, solo torcidos. En principio, este problema es completamente solucionable si especifica cuidadosamente las posiciones para todos los nodos ast (ahora están vacíos), pero esto definitivamente no está incluido en la lista de cosas necesarias para el prototipo.


Prueba


Por curiosidad, escribí un pequeño punto de referencia .


Tenemos una implementación qsort de madera que busca duplicados en la carga. Encontrado: un error. Una versión simplemente lo arroja a través de return err , la otra aclara el error llamando a fmt.Errorf . Y uno más usa excepciones. Clasificamos cortes de diferentes tamaños, ya sea sin duplicados (sin error, el corte está ordenado por completo) o con una repetición (la separación se rompe aproximadamente a la mitad, puede verse por los tiempos).


Resultados
 ~ > cat /proc/cpuinfo | grep 'model name' | head -1 model name : Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz ~ > go version go version go1.11 linux/amd64 ~ > go test -bench=. github.com/anjensan/jex/demo goos: linux goarch: amd64 pkg: github.com/anjensan/jex/demo BenchmarkNoErrors/_____10/exception-8 10000000 236 ns/op BenchmarkNoErrors/_____10/return_err-8 5000000 255 ns/op BenchmarkNoErrors/_____10/fmt.errorf-8 5000000 287 ns/op BenchmarkNoErrors/____100/exception-8 500000 3119 ns/op BenchmarkNoErrors/____100/return_err-8 500000 3194 ns/op BenchmarkNoErrors/____100/fmt.errorf-8 500000 3533 ns/op BenchmarkNoErrors/___1000/exception-8 30000 42356 ns/op BenchmarkNoErrors/___1000/return_err-8 30000 42204 ns/op BenchmarkNoErrors/___1000/fmt.errorf-8 30000 44465 ns/op BenchmarkNoErrors/__10000/exception-8 3000 525864 ns/op BenchmarkNoErrors/__10000/return_err-8 3000 524781 ns/op BenchmarkNoErrors/__10000/fmt.errorf-8 3000 561256 ns/op BenchmarkNoErrors/_100000/exception-8 200 6309181 ns/op BenchmarkNoErrors/_100000/return_err-8 200 6335135 ns/op BenchmarkNoErrors/_100000/fmt.errorf-8 200 6687197 ns/op BenchmarkNoErrors/1000000/exception-8 20 76274341 ns/op BenchmarkNoErrors/1000000/return_err-8 20 77806506 ns/op BenchmarkNoErrors/1000000/fmt.errorf-8 20 78019041 ns/op BenchmarkOneError/_____10/exception-8 2000000 712 ns/op BenchmarkOneError/_____10/return_err-8 5000000 268 ns/op BenchmarkOneError/_____10/fmt.errorf-8 2000000 799 ns/op BenchmarkOneError/____100/exception-8 500000 2296 ns/op BenchmarkOneError/____100/return_err-8 1000000 1809 ns/op BenchmarkOneError/____100/fmt.errorf-8 500000 3529 ns/op BenchmarkOneError/___1000/exception-8 100000 21168 ns/op BenchmarkOneError/___1000/return_err-8 100000 20747 ns/op BenchmarkOneError/___1000/fmt.errorf-8 50000 24560 ns/op BenchmarkOneError/__10000/exception-8 10000 242077 ns/op BenchmarkOneError/__10000/return_err-8 5000 242376 ns/op BenchmarkOneError/__10000/fmt.errorf-8 5000 251043 ns/op BenchmarkOneError/_100000/exception-8 500 2753692 ns/op BenchmarkOneError/_100000/return_err-8 500 2824116 ns/op BenchmarkOneError/_100000/fmt.errorf-8 500 2845701 ns/op BenchmarkOneError/1000000/exception-8 50 33452819 ns/op BenchmarkOneError/1000000/return_err-8 50 33374000 ns/op BenchmarkOneError/1000000/fmt.errorf-8 50 33705994 ns/op PASS ok github.com/anjensan/jex/demo 64.008s 

Si el error no se ha arrojado (el código es estable y concreto reforzado), entonces la garantía con la excepción de lanzamiento es aproximadamente comparable a return err y fmt.Errorf . A veces un poco más rápido. Pero si se arrojó el error, las excepciones van en segundo lugar. Pero todo depende de la proporción de "trabajo útil / error" y la profundidad de la pila. Para cortes pequeños, return err va por delante del espacio; para cortes medianos y grandes, las excepciones ya son iguales al reenvío manual.


En resumen, si los errores ocurren extremadamente raramente, las excepciones pueden incluso acelerar un poco el código. Si como todos los demás, será algo así. Pero si muy a menudo ... entonces las lentas excepciones están lejos del problema más importante, por lo que vale la pena preocuparse.


Como prueba, migré una verdadera biblioteca de gosh para excepciones.


Para mi profundo pesar, no funcionó reescribir 1 en 1

Más precisamente, habría resultado, pero esto debe ser molestado.


Entonces, por ejemplo, la función rpc2XML parece devolver el error ... sí, simplemente nunca lo devuelve. Si intentas serializar un tipo de datos no admitido, no hay error, solo vacía la salida. Tal vez esto es lo que se pretendía ... No, la conciencia no permite dejarlo así. Agregado por


  default: THROW(fmt.Errorf("unsupported type %T", value)) 

Pero resultó que esta función se usa de manera especial


 func rpcParams2XML(rpc interface{}) (string, error) { var err error buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { var xml string buffer += "<param>" xml, err = rpc2XML(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += xml buffer += "</param>" } buffer += "</params>" return buffer, err } 

Aquí revisamos la lista de parámetros, los serializamos todos, pero devolvemos un error solo para este último. Los errores restantes se ignoran. Comportamiento extraño hecho más fácil


 func rpcParams2XML_(rpc interface{}) string { buffer := "<params>" for i := 0; i < reflect.ValueOf(rpc).Elem().NumField(); i++ { buffer += "<param>" buffer += rpc2XML_(reflect.ValueOf(rpc).Elem().Field(i).Interface()) buffer += "</param>" } buffer += "</params>" return buffer } 

Si al menos un campo no funcionó para serializar, es un error. Bueno, eso está mejor. Pero resultó que esta función también se usa de manera especial .


 xmlstr, _ = rpcResponse2XML(response) 

De nuevo, para el código fuente esto no es tan importante, porque hay errores que se ignoran. Estoy empezando a adivinar por qué algunos programadores aman el manejo explícito de errores if err != nil ... Pero con excepciones, aún es más fácil reenviar o procesar que ignorar


 xmlstr = rpcResponse2XML_(response) 

Y no comencé a eliminar la "cadena de errores". Aquí está el código original.


 func DecodeClientResponse(r io.Reader, reply interface{}) error { rawxml, err := ioutil.ReadAll(r) if err != nil { return FaultSystemError } return xml2RPC(string(rawxml), reply) } 

aquí está el reescrito


 func DecodeClientResponse_(r io.Reader, reply interface{}) { var rawxml []byte if TRY() { rawxml, ERR = ioutil.ReadAll(r) } else { THROW(FaultSystemError) } xml2RPC_(string(rawxml), reply) } 

Aquí el error original (que ioutil.ReadAll devolvió) no se perderá, se adjuntará a la excepción en el campo ioutil.ReadAll . De nuevo, se puede hacer como en el original, pero debe confundirse especialmente ...


Reescribí las pruebas, reemplazando if err != nil { log.Error(..) } con un simple lanzamiento de excepción. Hay un punto negativo: las pruebas caen en el primer error y no continúan funcionando "bien, al menos de alguna manera". Según la mente, sería necesario dividirlos en subpruebas ... Lo que, en general, vale la pena hacer en cualquier caso. Pero es muy fácil obtener la pila correcta


 func errorReporter(t testing.TB) func(error) { return func(e error) { t.Log(string(debug.Stack())) t.Fatal(e) } } func TestRPC2XMLConverter_(t *testing.T) { defer ex.Catch(errorReporter(t)) // ... xml := rpcRequest2XML_("Some.Method", req) } 

En general, los errores son muy fáciles de ignorar. En el código original


 func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" xml, _ := rpc2XML(fault) buffer += xml buffer += "</fault></methodResponse>" return buffer } 

aquí el error de rpc2XML nuevamente se ignora silenciosamente. Se ha convertido así


 func fault2XML(fault Fault) string { buffer := "<methodResponse><fault>" if TRY() { buffer += rpc2XML_(fault) } else { fmt.Printf("ERR: %v", EX()) buffer += "<nil/>" } buffer += "</fault></methodResponse>" return buffer } 

Según mis sentimientos personales, es más fácil devolver un resultado "semiacabado" con errores.
Por ejemplo, una respuesta a medio construir. Las excepciones son más complicadas, ya que la función devuelve un resultado exitoso o no devuelve nada. Una especie de atomicidad. Por otro lado, las excepciones son más difíciles de ignorar o perder la causa raíz en la cadena de excepciones. Después de todo, todavía tienes que intentar hacer esto específicamente. Con errores, esto sucede fácil y naturalmente.


En lugar de una conclusión


Al escribir este artículo, ningún gopher resultó herido.


Gracias por la foto del goffer-alcohólico http://migranov.ru


No pude elegir entre los centros de "Programación" y "Programación anormal".
Una elección muy difícil, agregada a ambos.

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


All Articles