¿Qué es Errorx y cómo es útil?
Errorx es una biblioteca para manejar errores en Go. Proporciona herramientas para resolver problemas asociados con el mecanismo de error en proyectos grandes y una sintaxis única para trabajar con ellos.

La mayoría de los componentes del servidor Joom se han escrito en Go desde que se fundó la empresa. Esta elección valió la pena en las etapas iniciales de desarrollo y la vida útil del servicio, y a la luz de los anuncios sobre las perspectivas de Go 2, estamos seguros de que no nos arrepentiremos en el futuro. Una de las principales virtudes de Go es la simplicidad, y el enfoque de los errores demuestra este principio como nada más. No todos los proyectos alcanzan una escala suficiente para que las capacidades de la biblioteca estándar no sean suficientes, lo que le lleva a buscar sus propias soluciones en esta área. Sucedimos que experimentamos cierta evolución en los enfoques para trabajar con errores, y la biblioteca errorx refleja el resultado de esta evolución. Estamos convencidos de que puede ser útil para muchas personas, incluidas aquellas que aún no sienten una gran incomodidad al trabajar con errores en sus proyectos.
Errores en Go
Antes de pasar a la historia sobre errorx, se deben hacer algunas aclaraciones. Al final, ¿qué hay de malo con los errores?
type error interface { Error() string }
Muy simple, verdad? En la práctica, una implementación a menudo no lleva nada más que una descripción de cadena del error. Tal minimalismo está conectado con el enfoque según el cual un error no necesariamente significa algo "excepcional". Los errores más utilizados. New () de la biblioteca estándar es fiel a esta idea:
func New(text string) error { return &errorString{text} }
Si recordamos que los errores en un idioma no tienen un estado especial y son objetos comunes, surge la pregunta: ¿cuál es la peculiaridad de trabajar con ellos?
Los errores no son la excepción . No es ningún secreto que muchos, cuando se familiarizan con Go, encuentran esta diferencia con cierta resistencia. Hay muchas publicaciones, tanto explicativas como de apoyo, y critican el enfoque elegido en Go. De una forma u otra, los errores en Go sirven para muchos propósitos, y al menos uno de ellos es exactamente el mismo que las excepciones en algunos otros idiomas: solución de problemas. Como resultado, es natural esperar de ellos el mismo poder expresivo, incluso si el enfoque y la sintaxis asociados con su uso son muy diferentes.
Que esta mal
Muchos proyectos aprovechan los errores en Go, tal como están, y no tienen la menor dificultad al respecto. Sin embargo, a medida que crece la complejidad del sistema, comienzan a aparecer una serie de problemas que llaman la atención incluso en ausencia de altas expectativas. Una buena ilustración es una línea similar en el registro de su servicio:
Error: duplicate key
Aquí, el primer problema se vuelve obvio de inmediato: si no se ocupa de esto a propósito, en un sistema de alguna manera grande es casi imposible entender qué salió mal, solo por el mensaje inicial. Esta publicación carece de detalles y un contexto más amplio del problema. Este es un error del programador, pero sucede con demasiada frecuencia para descuidarlo. El código dedicado a las ramas "positivas" del gráfico de control siempre merece más atención en la práctica y está mejor cubierto por las pruebas que el código "negativo" asociado con la interrupción de la ejecución o problemas externos. Con qué frecuencia el mantra if err != nil {return err}
repite en los programas Go hace que esta supervisión sea aún más probable.
Como una pequeña digresión, considere este ejemplo:
func (m *Manager) ApplyToUsers(action func(User) (*Data, error), ids []UserID) error { users, err := m.LoadUsers(ids) if err != nil { return err } var actionData []*Data for _, user := range users { data, err := action(user) if err != nil { return err } ok, err := m.validateData(data) if err != nil { return nil } if !ok { log.Error("Validation failed for %v", data) continue } actionData = append(actionData, data) } return m.Apply(actionData) }
¿Qué tan rápido vio el error en este código? Pero se hizo al menos una vez, probablemente por cualquier programador de Go. Sugerencia: error en la expresión if err != nil { return nil }
.
Si volvemos al problema con un mensaje borroso en el registro, entonces, en esta situación, por supuesto, todos también sucedieron. Comenzar a corregir el código de manejo de errores ya en el momento en que ocurre el problema es muy desagradable; Además, de acuerdo con los datos iniciales del registro, no está completamente claro de qué lado comenzar a buscar esa parte del código que, de hecho, debe mejorarse. Esto puede parecer una complejidad exagerada para proyectos que son pequeños en código y en la cantidad de dependencias externas. Sin embargo, para proyectos a gran escala, este es un problema completamente real y doloroso.
Supongamos que un programador de experiencia amarga quiere agregar contexto por adelantado al error que regresa. La manera ingenua de hacer esto es algo como esto:
func InsertUser(u *User) error { err := usersTable.Insert(u) if err != nil { return errors.New(fmt.Sprintf("failed to insert user %s: %v", u.Name, err) } return nil }
Se puso mejor. El contexto más amplio aún no está claro, pero ahora es mucho más fácil encontrar al menos en qué código se produjo el error. Sin embargo, habiendo resuelto un problema, sin querer creamos otro. El error creado aquí mantuvo el mensaje de diagnóstico original, pero todo lo demás, incluido su tipo y contenido adicional, se perdió.
Para ver por qué esto es peligroso, considere un código similar en el controlador de la base de datos:
var ErrDuplicateKey = errors.New("duplicate key") func (t *Table) Insert(entity interface{}) error {
Ahora la comprobación IsDuplicateKeyError()
está destruida, aunque en el momento en que agregamos nuestro texto al error, no teníamos intención de cambiar su semántica. Esto, a su vez, romperá el código que se basa en esta verificación:
func RegisterUser(u *User) error { err := InsertUser(u) if db.IsDuplicateKeyError(err) {
Si queremos hacerlo de manera más inteligente y agregar nuestro propio tipo de error, que almacenará el error original y podrá devolverlo, digamos, a través del método de Cause() error
, entonces también resolveremos el problema solo parcialmente.
- Ahora, en lugar del procesamiento de errores, debe saber que la verdadera razón radica en
Cause()
- No hay forma de enseñar este conocimiento a las bibliotecas externas, y las funciones auxiliares escritas en ellas seguirán siendo inútiles
- Nuestra implementación puede esperar que
Cause()
devuelva la causa inmediata del error (o nula si no es así), mientras que la implementación en otra biblioteca esperará que el método devuelva no nil la causa raíz; la falta de herramientas estándar o un contrato generalmente aceptado amenaza con sorpresas muy desagradables
Sin embargo, esta solución parcial se utiliza en muchas bibliotecas de errores, incluida, en cierta medida, la nuestra. Hay planes en Go 2 para popularizar este enfoque; si esto sucede, será más fácil lidiar con los problemas descritos anteriormente.
Errorx
A continuación hablaremos sobre lo que ofrece errorx, pero primero intente formular las consideraciones que subyacen a la biblioteca.
- Los diagnósticos son más importantes que ahorrar recursos. El rendimiento de crear y mostrar errores es importante. Sin embargo, representan un camino negativo en lugar de positivo, y en la mayoría de los casos sirven como señal de un problema, por lo tanto, la presencia de información de diagnóstico en un error es aún más importante.
- Seguimiento de pila por defecto. Para que el error desaparezca con la totalidad del diagnóstico, no se requiere ningún esfuerzo. Por el contrario, es precisamente para excluir parte de la información (por brevedad o por razones de rendimiento) que se pueden requerir acciones adicionales.
- Semántica de errores. Debe haber una forma simple y confiable de verificar el significado del error: su tipo, variedad, propiedades.
- Facilidad de adición. Agregar información de diagnóstico a un error de aprobación debería ser simple y no debería arruinar la verificación de su semántica.
- Simplicidad El código dedicado a los errores se escribe a menudo y de manera rutinaria, por lo que la sintaxis de las manipulaciones básicas con ellos debe ser simple y concisa. Esto reduce la cantidad de errores y facilita la lectura.
- Menos es más. La comprensibilidad y uniformidad del código es más importante que las características opcionales y las opciones de expansión (que, probablemente, nadie usará).
- La semántica de error es parte de la API. Los errores que requieren un procesamiento separado en el código de llamada son de facto parte del paquete API público. No necesita tratar de ocultarlo o hacerlo menos explícito, pero puede hacer que el procesamiento sea más conveniente y las dependencias externas sean menos frágiles.
- La mayoría de los errores son opacos. Cuantos más tipos de errores para un usuario externo no se puedan distinguir entre sí, mejor. La carga de los tipos de errores API que requieren un manejo especial, así como la carga de los propios errores con los datos necesarios para procesarlos es un defecto de diseño que debe evitarse.
La pregunta más difícil para nosotros fue la extensibilidad: ¿debe errorx proporcionar primitivas para instituir tipos personalizados de errores que sean arbitrariamente diferentes en el comportamiento, o hay una implementación que le permita obtener todo lo que necesita de la caja? Hemos elegido la segunda opción. En primer lugar, errorx resuelve un problema muy práctico, y nuestra experiencia al usarlo muestra que para este propósito es mejor tener una solución, en lugar de piezas de repuesto para crearla. En segundo lugar, la consideración con respecto a la simplicidad es muy significativa: dado que se presta menos atención a los errores, el código debe diseñarse de tal manera que sea difícil trabajar con ellos. La práctica ha demostrado que para esto es importante que todo ese código se vea y funcione igual.
TL; DR por características principales de la biblioteca:
- Pila de ubicaciones de creación de rastreo en todos los errores de forma predeterminada
- Verificaciones de errores, varias variedades.
- La capacidad de agregar información a un error existente sin romper nada
- Escriba control de visibilidad si desea ocultar el motivo original de la persona que llama
- Error al manejar el mecanismo de generalización del código (jerarquía de tipos, rasgos)
- Error de personalización por propiedades dinámicas
- Tipos de error estándar
- Utilidades de sintaxis para mejorar la legibilidad del código de manejo de errores
Introduccion
Si reelaboramos el ejemplo que analizamos anteriormente usando errorx, obtenemos lo siguiente:
var ( DBErrors = errorx.NewNamespace("db") ErrDuplicateKey = DBErrors.NewType("duplicate_key") ) func (t *Table) Insert(entity interface{}) error {
func InsertUser(u *User) error { err := usersTable.Insert(u) if err != nil { return errorx.Decorate(err, "failed to insert user %s", u.Name) } return nil }
El código de la persona que IsDuplicateKeyError()
usando IsDuplicateKeyError()
no cambiará.
¿Qué ha cambiado en este ejemplo?
ErrDuplicateKey
convirtió en un tipo, no en una instancia de error; comprobarlo es resistente a los errores de copia; no existe una dependencia frágil de la igualdad exacta- Hay un espacio de nombres para los errores de la base de datos; lo más probable es que tenga otros errores, y dicha agrupación es útil para facilitar la lectura y, en algunos casos, puede usarse en el código
- Insert devuelve un nuevo error para cada llamada:
- El error contiene más detalles; esto, por supuesto, es posible sin errorx, pero es imposible si se devuelve la misma instancia de error cada vez, que anteriormente se requería para
IsDuplicateKeyError()
- Estos errores pueden llevar un seguimiento de pila diferente, lo cual es útil porque no para todas las llamadas a la función Insertar, esta situación es aceptable
InsertUser()
complementa el texto de error, pero aplica el error original, que se conserva en su totalidad para las operaciones posterioresIsDuplicateKeyError()
ahora funciona: no se puede estropear ni copiando el error, ni por tantas capas como desee con Decorate ()
No es necesario seguir siempre tal esquema:
- El tipo de error está lejos de ser siempre único: se pueden usar los mismos tipos en muchos lugares
- Si lo desea, la colección de seguimiento de la pila se puede deshabilitar, y no puede crear un nuevo error cada vez, sino devolver el mismo que en el ejemplo original; estos son los llamados errores centinela, y no recomendamos su uso, pero puede ser útil si el error se usa solo como un marcador en el código, y desea guardar en la creación de objetos
- Hay una manera de hacer que la
errorx.IsOfType(err, ErrDuplicateKey)
deje de funcionar si desea ocultar la semántica de la causa raíz de miradas indiscretas - Hay otras formas de comprobar el tipo, además de comparar con el tipo exacto.
Godoc contiene información detallada sobre todo esto. A continuación, profundizaremos un poco en las características principales, que son suficientes para el trabajo diario.
Tipos
Cualquier errorx error pertenece a algún tipo. El tipo importa porque las propiedades de error heredadas pueden pasarse a través de él; es a través de él o de sus rasgos que se realizarán pruebas semánticas si es necesario. Además, el nombre expresivo del tipo complementa el mensaje de error y en algunos casos puede reemplazarlo.
AuthErrors = errorx.NewNamespace("auth") ErrInvalidToken = AuthErrors.NewType("invalid_token")
return ErrInvalidToken.NewWithNoMessage()
El mensaje de error contendrá auth.invalid_token
. La declaración de error puede verse diferente:
ErrInvalidToken = AuthErrors.NewType("invalid_token").ApplyModifiers(errorx.TypeModifierOmitStackTrace)
En esta realización, usando el modificador de tipo, se deshabilita la recopilación de rastreo de pila. El error tiene semántica de marcador: su tipo se le da al usuario externo del servicio, y una pila de llamadas en los registros no sería útil, porque Esto no es un problema para ser reparado.
Aquí podemos hacer una reserva de que los errores tienen una naturaleza dual en varios de sus aspectos. El contenido del error se usa tanto para el diagnóstico y, a veces, como información para un usuario externo: cliente API, usuario de biblioteca, etc. Los errores se usan en el código tanto como un medio de transmitir la semántica de lo que sucedió, como un mecanismo para transferir el control. Cuando se utilizan tipos de error, esto debe tenerse en cuenta.
Error al crear
return MyType.New("fail")
Obtener su propio tipo para cada error es completamente opcional. Cualquier proyecto puede tener su propio paquete de errores de uso general, y algunos conjuntos se proporcionan como parte del espacio de nombres común junto con errorx. Contiene errores que en la mayoría de los casos no implican procesamiento en el código y son adecuados para situaciones "excepcionales" cuando algo salió mal.
return errorx.IllegalArgument.New("negative value %d", value)
En un caso típico, una cadena de llamadas está diseñada para que se cree un error al final de la cadena y se procese desde el principio. En Go, no es sin razón que se considere una mala forma procesar un error dos veces, es decir, escribir un error en el registro y devolverlo más arriba en la pila. Sin embargo, puede agregar información al error en sí mismo antes de revelarlo:
return errorx.Decorate(err, "failed to upload '%s' to '%s'", filename, location)
El texto agregado al error aparecerá en el registro, pero no hará daño verificar el tipo del error original.
A veces surge la necesidad opuesta: cualquiera que sea la naturaleza del error, el usuario externo del paquete no debe saberlo. Si tuviera esa oportunidad, podría crear una dependencia frágil de parte de la implementación.
return service.ErrBadRequest.Wrap(err, "failed to load user data")
Una diferencia importante que hace que Wrap sea la alternativa preferida a Nuevo es que el error original se refleja completamente en los registros. Y, en particular, traerá consigo una útil pila de llamadas iniciales.
Otro truco útil que le permite guardar toda la información posible sobre la pila de llamadas se ve así:
return errorx.EnhanceStackTrace(err, "operation fail")
Si el error original provino de otra goroutina, el resultado de dicha llamada contendrá un rastro de pila de ambas goroutinas, lo que aumenta inusualmente su utilidad. La necesidad de hacer una llamada de este tipo se debe claramente a problemas de rendimiento: este caso es relativamente raro, y la ergonomía que lo detectaría por sí solo ralentizaría la envoltura habitual, donde no es necesario en absoluto.
Godoc contiene más información y también describe características adicionales como DecorateMany.
Manejo de errores
Mejor si el manejo de errores se reduce a lo siguiente:
log.Error("Error: %+v", err)
Cuanto menos error tenga que cometer, excepto para imprimirlo en el registro en la capa del sistema del proyecto, mejor. En realidad, esto a veces no es suficiente, y tienes que hacer esto:
if errorx.IsOfType(err, MyType) { }
Esta comprobación tendrá éxito tanto en un error de tipo MyType
como en sus tipos secundarios, y es resistente a errorx.Decorate()
. Aquí, sin embargo, existe una dependencia directa del tipo de error, que es bastante normal dentro del paquete, pero puede ser desagradable si se usa fuera de él. En algunos casos, el tipo de dicho error es parte de una API externa estable, y a veces nos gustaría reemplazar esta verificación con una verificación de propiedad, y no el tipo exacto de error.
En los errores clásicos de Go, esto se haría a través de una interfaz, tipo de conversión que serviría como indicador del tipo de error. Los tipos de errorx no son compatibles con esta extensión, pero puede utilizar el mecanismo de Trait
lugar. Por ejemplo:
func IsTemporary(err error) bool { return HasTrait(err, Temporary()) }
Esta función integrada en errorx verifica si el error tiene la propiedad estándar Temporary
, es decir si es temporal Marcar los tipos de errores con rasgos es responsabilidad de la fuente del error, y a través de ellos puede transmitir una señal útil sin hacer que los tipos internos específicos formen parte de la API externa.
return errorx.IgnoreWithTrait(err, errorx.NotFound())
Esta sintaxis es útil cuando se necesita un cierto tipo de error para interrumpir el flujo de control, pero no debe pasarse a la función de llamada.
A pesar de la abundancia de herramientas de procesamiento, no todas se enumeran aquí, es importante recordar que trabajar con errores debe ser lo más simple posible. Un ejemplo de las reglas que tratamos de cumplir:
- El código que recibe un error siempre debe registrarlo en su totalidad; si parte de la información es superflua, deje que el código que produce el error se encargue de esto
- Nunca debe usar el texto de error o el resultado de la función
Error()
para procesarlo en código; solo las verificaciones de tipo / rasgo son adecuadas para esto, o la aserción de tipo en caso de errores que no sean errorx - El código de usuario no debe romperse debido a que algún tipo de error no se procesa de una manera especial, incluso si dicho procesamiento es posible y le da características adicionales
- Los errores que son verificados por las propiedades son mejores que los llamados errores centinela, porque tales controles son menos frágiles
Error exteriorx
Aquí describimos lo que está disponible para el usuario de la biblioteca fuera de la caja, pero en Joom la penetración del código relacionado con errores es muy grande. El módulo de registro acepta explícitamente errores en su firma y se imprime para eliminar la posibilidad de formateo incorrecto, así como extraer información contextual disponible opcionalmente de la cadena de errores. El módulo responsable del trabajo seguro contra el pánico con goroutins desempaqueta el error si llega con pánico, y también sabe cómo presentar el pánico utilizando la sintaxis de error sin perder el rastro original de la pila. Algo de esto, quizás también lo publicaremos.
Problemas de compatibilidad
A pesar del hecho de que estamos muy satisfechos con la forma en que errorx nos permite trabajar con errores, la situación con el código de la biblioteca dedicada a este tema está lejos de ser ideal. En Joom resolvemos problemas prácticos bastante específicos con errorx, pero desde el punto de vista del ecosistema Go, sería preferible tener todo este conjunto de herramientas en la biblioteca estándar. El error, cuya fuente pertenece real o potencialmente a otro paradigma, debe considerarse como extraño, es decir. potencialmente no llevar información en la forma que se acepta en el proyecto.
Sin embargo, se han hecho algunas cosas para no entrar en conflicto con otras soluciones existentes.
El formato '%+v'
usa para imprimir un error junto con el seguimiento de la pila, si está presente. Este es el estándar de facto en el ecosistema Go e incluso se incluye en el diseño preliminar para Go 2.
Cause() error
errorx , , , Causer, errorx Wrap().
, Go 2, . .
, errorx Go 1. , Go 2, . , , errorx.
Check-handle , errorx , a Unwrap() error
Wrap()
errorx (.. , , Wrap
), . , , .
design draft Go 2, errorx.Is()
errorx.As()
, errors .
Conclusión
, , , - , . , API : , , . 1.0 , Joom. , - .
: https://github.com/joomcode/errorx
, !
