Hola, queridos lectores de Habrahabr. Mientras discutimos un posible nuevo diseño para el manejo de errores y discutimos sobre los beneficios del manejo explícito de errores, propongo considerar algunas de las características de los errores, el pánico y su recuperación en Go que serán útiles en la práctica.

error
El error es una interfaz. Y como la mayoría de las interfaces en Go, la definición de error es corta y simple:
type error interface { Error() string }
Resulta que cualquier tipo que tenga el método Error puede usarse como un error. Como Rob Pike enseñó, los errores son valores , y los valores pueden usarse para manipular y programar diversas lógicas.
Hay dos funciones en la biblioteca estándar Go que se utilizan convenientemente para crear errores. Los errores. La nueva función es muy adecuada para crear errores simples. La función fmt.Errorf permite el uso de formato estándar.
err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id)
Por lo general, el tipo de error es suficiente para lidiar con los errores. Pero a veces puede ser necesario transmitir información adicional con un error; en tales casos, puede agregar su propio tipo de error.
Un buen ejemplo es el tipo de PathError del paquete os
El valor de dicho error contendrá la operación, la ruta y el error.
Se inicializan de esta manera:
... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e}
El procesamiento puede tener una forma estándar:
_, err := os.Open("---") if err != nil{ fmt.Println(err) }
Pero si es necesario obtener información adicional, puede descomprimir el error en * os.PathError :
_, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) }
Se puede aplicar el mismo enfoque si la función puede devolver varios tipos diferentes de errores.
jugar
Declaración de varios tipos de errores, cada uno tiene sus propios datos:
codigo type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() }
Una función que puede devolver estos errores:
codigo func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil }
Error al manejar a través de tipos de conversión:
codigo func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } }
En el caso de que los errores no necesiten propiedades especiales, es una buena práctica en Ir crear variables para almacenar errores a nivel de paquete. Un ejemplo son los errores como io.EOF, io.ErrNoProgress, etc.
En el siguiente ejemplo, interrumpimos la lectura y continuamos ejecutando la aplicación cuando el error es io.EOF o cerramos la aplicación por cualquier otro error.
func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } }
Esto es efectivo porque los errores se generan solo una vez y se reutilizan.
rastro de la pila
Lista de funciones llamadas en el momento de la captura de la pila. El seguimiento de pila le ayuda a tener una mejor idea de lo que está sucediendo en el sistema. Guardar el rastro en los registros puede ayudar seriamente al depurar.
Go a menudo carece de esta información por error, pero afortunadamente obtener un volcado en Go no es difícil.
Puede usar debug.PrintStack () para enviar el seguimiento a la salida estándar:
func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() }
Como resultado, se escribirá la siguiente información en Stderr:
apilar goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27
debug.Stack () devuelve una porción de bytes con un volcado de pila, que luego puede registrarse o en otro lugar.
b := debug.Stack() fmt.Printf("Trace:\n %s\n", b)
Hay otro punto si hacemos así:
go bar()
entonces obtenemos la siguiente información en la salida:
main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c
Cada gorutina tiene una pila separada, respectivamente, solo obtenemos su volcado. Por cierto, las gorutinas tienen sus propias pilas, la recuperación todavía está conectada con esto, pero más sobre eso más adelante.
Y así, para ver información sobre todas las goroutinas, puede usar runtime.Stack () y pasar el segundo argumento verdadero.
func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) }
apilar Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27
Agregue esta información al error y, por lo tanto, aumente considerablemente su contenido de información.
Por ejemplo, así:
type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() }
Puede agregar una función para crear este error:
func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} }
Entonces ya puedes trabajar con esto:
func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err }
apilar Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29
En consecuencia, el error y la traza se pueden dividir:
func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } }
Y, por supuesto, ya existe una solución preparada. Uno de ellos es el paquete https://github.com/pkg/errors . Le permite crear un nuevo error, que ya contendrá la pila de rastreo, y puede agregar un rastreo y / o mensaje adicional a un error existente. Más conveniente formato de salida.
import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") }
apilar error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361
% v solo mostrará mensajes
error2: error
pánico / recuperar
El pánico (también conocido como accidente, también conocido como pánico), como regla, indica la presencia de mal funcionamiento, debido a que el sistema (o un subsistema específico) no puede continuar funcionando. Si se llama al pánico, el tiempo de ejecución de Go mira la pila, tratando de encontrar un controlador para ello.
Los pánicos no procesados finalizan la aplicación. Esto los distingue fundamentalmente de los errores que le permiten no procesarse.
Puede pasar cualquier argumento a la llamada de función de pánico.
panic(v interface{})
En caso de pánico, es conveniente pasar un error del tipo que simplifica la recuperación y ayuda a la depuración.
panic(errors.New("error"))
La recuperación ante desastres en Go se basa en una llamada de función diferida, también conocida como diferir . Se garantiza que dicha función se ejecutará al regresar de la función principal. Independientemente de la razón: la declaración de devolución, el final de la función o el pánico.
Y ahora la función de recuperación hace posible obtener información sobre el accidente y detener el desenrollado de la pila de llamadas.
Una llamada de pánico típica y un manejador:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) }
recupera la interfaz devuelve {} (la que pasamos al pánico) o nula si no hubo una llamada al pánico.
Considere otro ejemplo de manejo de emergencia. Tenemos alguna función a la que transferimos, por ejemplo, un recurso y que, en teoría, puede causar pánico.
func bar(f *os.File) { panic(errors.New("error")) }
En primer lugar, es posible que deba realizar siempre algunas acciones al final, por ejemplo, limpiar recursos, en nuestro caso, cerrar el archivo.
En segundo lugar, la ejecución incorrecta de dicha función no debería conducir al final de todo el programa.
Este problema se puede resolver con aplazar, recuperar y cerrar:
func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error)
El cierre nos permite pasar a las variables declaradas anteriormente, gracias a esto garantizamos cerrar el archivo y, en caso de accidente, extraer un error y pasarlo al mecanismo habitual de manejo de errores.
Hay situaciones inversas cuando una función con ciertos argumentos siempre debe funcionar correctamente, y si esto no sucede, entonces va realmente mal.
En tales casos, agregue una función de contenedor en la que se llama a la función de destino y, en caso de error, se llama al pánico.
Ir generalmente tiene prefijos obligatorios:
Vale la pena recordar una cosa más relacionada con el pánico y las gorutinas.
Parte de las tesis de lo que se discutió anteriormente:
- Se asigna una pila separada para cada goroutine.
- Al llamar al pánico, se busca recuperar en la pila.
- En el caso de que no se encuentre la recuperación, la aplicación completa finaliza.
El controlador en main no interceptará el pánico de foo y el programa se bloqueará:
func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) }
Esto será un problema si, por ejemplo, se llama a un controlador para conectarse al servidor. En caso de pánico en cualquiera de los controladores, todo el servidor completará la ejecución. Y no puede controlar el manejo de accidentes en estas funciones, por alguna razón.
En un caso simple, la solución podría verse así:
type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) }
manejar / verificar
Quizás en el futuro veremos cambios en el manejo de errores. Puede familiarizarse con ellos en los enlaces:
go2draft
Manejo de errores en Go 2
Eso es todo por hoy. Gracias