Manejo de errores en Go 1.13


Durante la última década, hemos explotado con éxito el hecho de que Go maneja los errores como valores . Aunque la biblioteca estándar tenía un soporte mínimo para los errores: solo los errores. errors.New y fmt.Errorf que generan un error que contiene solo un mensaje: la interfaz integrada permite a los programadores de Go agregar cualquier información. Todo lo que necesitas es un tipo que implemente el método de Error :

 type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() } 

Estos tipos de errores se encuentran en todos los idiomas y almacenan una amplia variedad de información, desde marcas de tiempo hasta nombres de archivos y direcciones de servidores. A menudo se mencionan errores de bajo nivel que proporcionan un contexto adicional.

El patrón, cuando un error contiene otro, se encuentra con tanta frecuencia en Go que después de una discusión acalorada en Go 1.13 se agregó su apoyo explícito. En este artículo, veremos las adiciones a la biblioteca estándar que proporcionan el soporte mencionado: tres nuevas funciones en el paquete de errores y un nuevo comando de formato para fmt.Errorf .

Antes de discutir los cambios en detalle, hablemos sobre cómo se investigaron y construyeron los errores en versiones anteriores del lenguaje.

Errores antes de ir 1.13


Investigación de errores


Los errores en Go son significados. Los programas toman decisiones basadas en estos valores de diferentes maneras. Muy a menudo, el error se compara con cero para ver si la operación falló.

 if err != nil { // something went wrong } 

A veces comparamos el error para encontrar el valor de control y ver si ha ocurrido un error específico.

 var ErrNotFound = errors.New("not found") if err == ErrNotFound { // something wasn't found } 

El valor de error puede ser de cualquier tipo que satisfaga la interfaz de error definida en el idioma. Un programa puede usar una declaración de tipo o un interruptor de tipo para ver el valor de error de un tipo más específico.

 type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 

Agregar información


A menudo, una función pasa un error a la pila de llamadas, agregando información, por ejemplo, una breve descripción de lo que sucedió cuando ocurrió el error. Esto es fácil de hacer, solo construya un nuevo error que incluya el texto del error anterior:

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

Al crear un nuevo error usando fmt.Errorf descartamos todo excepto el texto del error original. Como vimos en el ejemplo de QueryError , a veces es necesario definir un nuevo tipo de error que contenga el error original para guardarlo para el análisis usando el código:

 type QueryError struct { Query string Err error } 

Los programas pueden mirar dentro del *QueryError y tomar una decisión basada en el error original. Esto a veces se llama desenvolver un error.

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

El tipo os.PathError de la biblioteca estándar es otro ejemplo de cómo un error contiene otro.

Errores en Go 1.13


Método de desenvoltura


En Go 1.13, los paquetes de biblioteca estándar de errors y fmt simplificaron el fmt errores que contienen otros errores. Lo más importante es la convención, no el cambio: un error que contiene otro error puede implementar el método Unwrap , que devuelve el error original. Si e1.Unwrap() devuelve e2 , entonces decimos que e1 empaqueta e2 y puede desempaquetar e1 para obtener e2 .

De acuerdo con esta convención, puede QueryError tipo QueryError descrito anteriormente al método QueryError , que devuelve el error que contiene:

 func (e *QueryError) Unwrap() error { return e.Err } 

El resultado de desempaquetar el error también puede contener el método Unwrap . La secuencia de errores obtenidos a través del desempaquetado repetido, la llamamos cadena de errores .

Investigación de error con Is y As


En Go 1.13, el paquete de errors contiene dos nuevas funciones para investigar errores: Is y As .

Los errors.Is . errors.Is función compara un error con un valor.

 // Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } 

La función As comprueba si el error es de un tipo particular.

 // Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

En el caso más simple, los errors.Is . errors.Is función se comporta como una comparación con un error de control, y los errors.As . errors.As función se comporta como una declaración de tipo. Sin embargo, cuando se trabaja con errores empaquetados, estas funciones evalúan todos los errores en la cadena. Veamos el ejemplo de QueryError anterior para examinar el error original:

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

Usando los errors.Is función, puede escribir esto:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

El paquete de errors también contiene una nueva función Unwrap que devuelve el resultado de llamar al método Unwrap del error, o devuelve nil si el error no tiene el método Unwrap . Por lo general, es mejor usar errors.Is , es errors.As , errors.As , ya que le permiten examinar toda la cadena con una sola llamada.

Error al empaquetar con% w


Como mencioné, es una práctica normal usar la función fmt.Errorf para agregar información adicional al error.

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

En Go 1.13, la función fmt.Errorf admite el nuevo comando %w . Si es así, entonces el error devuelto por fmt.Errorf contendrá el método Unwrap que devuelve el argumento %w , que debería ser un error. En todos los demás casos, %w idéntico a %v .

 if err != nil { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

Empaquetar el error con %w hace disponible para errors.Is y errors.As . errors.As :

 err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ... 

¿Cuándo empacar?


Cuando agrega un contexto adicional al error utilizando fmt.Errorf o una implementación de tipo personalizado, debe decidir si el nuevo error contendrá el original. No hay una respuesta única para esto, todo depende del contexto en el que se crea el nuevo error. Empaca para mostrarle a la persona que llama. No empaquete el error si esto lleva a la divulgación de los detalles de implementación.

Por ejemplo, imagine una función Parse que lee una estructura de datos compleja de io.Reader . Si ocurre un error, querremos averiguar el número de la fila y la columna donde ocurrió. Si se produjo un error al leer io.Reader , tendremos que empacarlo para averiguar el motivo. Dado que el llamante recibió la función io.Reader , tiene sentido mostrar el error que generó.

Otro caso: una función que realiza varias llamadas a la base de datos probablemente no debería devolver un error en el que se empaqueta el resultado de una de estas llamadas. Si la base de datos utilizada por esta función es parte de la implementación, la divulgación de estos errores violará la abstracción. Por ejemplo, si la función LookupUser del paquete pkg usa la database/sql Go database/sql paquete database/sql , entonces puede encontrar el error sql.ErrNoRows . Si devuelve un error usando fmt.Errorf("accessing DB: %v", err) , la persona que llama no puede mirar adentro y encontrar sql.ErrNoRows . Pero si la función devuelve fmt.Errorf("accessing DB: %w", err) , la persona que llama podría escribir:

 err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) … 

En este caso, la función siempre debe devolver sql.ErrNoRows si no desea dividir clientes, incluso al cambiar a un paquete con una base de datos diferente. En otras palabras, el empaquetado hace que un error forme parte de su API. Si no desea confirmar la compatibilidad con este error en el futuro como parte de la API, no lo empaque.

Es importante recordar que independientemente de si lo empaca o no, el error permanecerá sin cambios. Una persona que lo entienda tendrá la misma información. Tomar decisiones sobre el empaque depende de si se necesita información adicional para los programas para que puedan tomar decisiones más informadas; o si desea ocultar esta información para mantener el nivel de abstracción.

Configuración de pruebas de error utilizando métodos Is y As


La función errors.Is verifica cada error en la cadena contra el valor objetivo. Por defecto, un error coincide con este valor si son equivalentes. Además, un error en la cadena puede declarar su conformidad con el valor objetivo utilizando la implementación del método Is .

Considere el error causado por el paquete Upspin , que compara el error con la plantilla y evalúa solo los campos distintos de cero:

 type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) { // err's User field is "someuser". } 

Los errors.As La función errors.As también aconseja el método As , si lo hay.

Errores y API de paquetes


Un paquete que devuelve errores (y la mayoría de los paquetes hacen esto) debe describir las propiedades de estos errores en los que un programador puede confiar. Un paquete bien diseñado también evitará la devolución de errores con propiedades en las que no se puede confiar.

Lo más simple es decir si la operación fue exitosa, devolviendo, respectivamente, el valor nil o non-nil. En muchos casos, no se requiere otra información.

Si necesita que la función devuelva un estado de error identificable, por ejemplo, "elemento no encontrado", puede devolver un error en el que se empaqueta el valor de la señal.

 var ErrNotFound = errors.New("not found") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

Existen otros patrones para proporcionar errores que la persona que llama puede examinar semánticamente. Por ejemplo, devuelve directamente un valor de control, un tipo específico o un valor que puede analizarse mediante una función predicativa.

En cualquier caso, no revele los detalles internos al usuario. Como se mencionó en el capítulo "¿Cuándo vale la pena empaquetarlo?", Si devuelve un error de otro paquete, conviértalo para no revelar el error original, a menos que tenga la intención de comprometerse a devolver este error específico en el futuro.

 f, err := os.Open(filename) if err != nil { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

Si una función devuelve un error con un valor o tipo de señal empaquetada, no devuelva directamente el error original.

 var ErrPermission = errors.New("permission denied") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

Conclusión


Aunque discutimos solo tres funciones y un comando de formateo, esperamos que ayuden a mejorar en gran medida el manejo de errores en los programas Go. Esperamos que el empaquetado en aras de proporcionar un contexto adicional se convierta en una práctica normal, ayudando a los programadores a tomar mejores decisiones y encontrar errores más rápido.

Como dijo Russ Cox en su discurso en la GopherCon 2019 , en el camino hacia Go 2 experimentamos, simplificamos y enviamos. Y ahora, después de haber enviado estos cambios, iniciamos nuevos experimentos.

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


All Articles