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 {
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 {
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 {
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 {
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.
La función
As
comprueba si el error es de un tipo particular.
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 {
Usando los
errors.Is
función, puede escribir esto:
if errors.Is(err, ErrPermission) {
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 {
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"}) {
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")
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 {
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")
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.