Especificación de Manejo de Errores en Golang
Tipos de Errores
Hay algunas opciones para declarar errores. Antes de elegir la opción que mejor se adapte a su caso de uso, considere lo siguiente:
- ¿El llamante necesita hacer coincidir el error para manejarlo? Si es así, debemos admitir las funciones
errors.Is
oerrors.As
declarando variables de error de nivel superior o tipos personalizados. - ¿El mensaje de error es una cadena estática o una cadena dinámica que requiere información contextual? Para cadenas estáticas, podemos usar
errors.New
, pero para este último, debemos usarfmt.Errorf
o un tipo de error personalizado. - ¿Estamos pasando nuevos errores devueltos por funciones secundarias? Si es así, consulte la sección de envoltura de errores.
¿Coincidencia de Error? | Mensaje de Error | Guía |
---|---|---|
No | estática | errors.New |
No | dinámica | fmt.Errorf |
Sí | estática | var de nivel superior con errors.New |
Sí | dinámica | tipo de error personalizado |
Por ejemplo, use errors.New
para representar errores con cadenas estáticas. Si el llamante necesita hacer coincidir y manejar este error, expórtelo como una variable para admitir la coincidencia con errors.Is
.
Sin Coincidencia de Errores
// paquete foo
func Abrir() error {
return errors.New("no se pudo abrir")
}
// paquete bar
if err := foo.Abrir(); err != nil {
// No se puede manejar el error.
panic("error desconocido")
}
Coincidencia de Errores
// paquete foo
var ErrNoSePudoAbrir = errors.New("no se pudo abrir")
func Abrir() error {
return ErrNoSePudoAbrir
}
// paquete bar
if err := foo.Abrir(); err != nil {
if errors.Is(err, foo.ErrNoSePudoAbrir) {
// manejar el error
} else {
panic("error desconocido")
}
}
Para errores con cadenas dinámicas, use fmt.Errorf
si el llamante no necesita hacer coincidir. Si de hecho el mismo necesita hacer coincidir, entonces use un error
personalizado.
Sin Coincidencia de Errores
// paquete foo
func Abrir(archivo string) error {
return fmt.Errorf("archivo %q no encontrado", archivo)
}
// paquete bar
if err := foo.Abrir("archivo.txt"); err != nil {
// No se puede manejar el error.
panic("error desconocido")
}
Coincidencia de Errores
// paquete foo
type ErrorNoEncontrado struct {
Archivo string
}
func (e *ErrorNoEncontrado) Error() string {
return fmt.Sprintf("archivo %q no encontrado", e.Archivo)
}
func Abrir(archivo string) error {
return &ErrorNoEncontrado{Archivo: archivo}
}
// paquete bar
if err := foo.Abrir("archivo.txt"); err != nil {
var noEncontrado *ErrorNoEncontrado
if errors.As(err, &noEncontrado) {
// manejar el error
} else {
panic("error desconocido")
}
}
Tenga en cuenta que si exporta variables o tipos de error desde un paquete, estos formarán parte de la API pública del paquete.
Envolver errores
Cuando ocurre un error al llamar a otro método, generalmente hay tres maneras de manejarlo:
- Devolver el error original tal como está.
- Utilizar
fmt.Errorf
con%w
para agregar contexto al error y luego devolverlo. - Utilizar
fmt.Errorf
con%v
para agregar contexto al error y luego devolverlo.
Si no hay contexto adicional para agregar, devolver el error original tal como está. Esto preservará el tipo y el mensaje de error original. Esto es particularmente adecuado cuando el mensaje de error subyacente contiene suficiente información para rastrear de dónde provino el error.
De lo contrario, agregue contexto al mensaje de error tanto como sea posible para que no ocurran errores ambiguos como "conexión rechazada". En su lugar, recibirás errores más útiles, como "llamando al servicio foo: conexión rechazada".
Utiliza fmt.Errorf
para agregar contexto a tus errores y elige entre los verbos %w
o %v
según si el llamante debe poder igualar y extraer la causa raíz.
- Utiliza
%w
si el llamante debe tener acceso al error subyacente. Este es un buen valor por defecto para la mayoría de errores de envoltura, pero ten en cuenta que el llamante puede empezar a depender de este comportamiento. Por lo tanto, para errores de envoltura que son variables o tipos conocidos, regístralos y pruébalos como parte del contrato de la función. - Utiliza
%v
para ocultar el error subyacente. El llamante no podrá igualarlo, pero puedes cambiar a%w
en el futuro si es necesario.
Al agregar contexto al error devuelto, evita usar frases como "falló al" para mantener el contexto conciso. Cuando el error se propague a través de la pila, se irá apilando capa por capa:
No recomendado:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"falló al crear una nueva tienda: %w", err)
}
// falló al x: falló al y: falló al crear una nueva tienda: el error
Recomendado:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"nueva tienda: %w", err)
}
// x: y: nueva tienda: el error
Sin embargo, una vez que el error se envía a otro sistema, debe quedar claro que el mensaje es un error (por ejemplo, una etiqueta "err" o un prefijo "Fallido" en los registros).
Nomenclatura incorrecta
Para valores de error almacenados como variables globales, utiliza el prefijo Err
o err
según si están exportados. Consulta las pautas. Para constantes y variables de nivel superior no exportadas, utiliza un guion bajo (_) como prefijo.
var (
// Exporta los dos siguientes errores para que los usuarios de este paquete puedan igualarlos con errors.Is.
ErrEnlaceRoto = errors.New("el enlace está roto")
ErrNoSePudoAbrir = errors.New("no se pudo abrir")
// Este error no se exporta porque no queremos que forme parte de nuestra API pública. Aun así, aún podemos usarlo dentro del paquete con errors.
errNoEncontrado = errors.New("no encontrado")
)
Para tipos de error personalizados, utiliza el sufijo Error
.
// Del mismo modo, este error se exporta para que los usuarios de este paquete puedan igualarlo con errors.As.
type ErrorNoEncontrado struct {
Archivo string
}
func (e *ErrorNoEncontrado) Error() string {
return fmt.Sprintf("archivo %q no encontrado", e.Archivo)
}
// Este error no se exporta porque no queremos que forme parte de la API pública. Aun así, aún podemos usarlo dentro de un paquete con errors.As.
type errorResolucion struct {
Ruta string
}
func (e *errorResolucion) Error() string {
return fmt.Sprintf("resolución %q", e.Ruta)
}
Manejo de Errores
Cuando el llamante recibe un error del llamado, puede manejar el error de diversas maneras según su comprensión del error.
Esto incluye, pero no se limita a:
- Coincidir el error con
errors.Is
oerrors.As
si el llamado ha acordado una definición específica de error, y manejar la ramificación de diferentes maneras - Registrar el error y degradar graciosamente si el error es recuperable
- Devolver un error bien definido si representa una condición de fallo específica del dominio
- Devolver el error, ya sea envuelto o textual
Independientemente de cómo el llamante maneje el error, típicamente debería manejar cada error solo una vez. Por ejemplo, el llamante no debería registrar el error y luego devolverlo, ya que su llamante podría también manejar el error.
Por ejemplo, considera los siguientes escenarios:
Malo: Registrar el error y devolverlo
Otros llamantes más arriba en la pila pueden tomar acciones similares sobre este error. Esto crearía mucho ruido en los registros de la aplicación con poco beneficio.
u, err := getUser(id)
if err != nil {
// MALO: Ver descripción
log.Printf("No se pudo obtener el usuario %q: %v", id, err)
return err
}
Bueno: Envolver el error y devolverlo
Los errores más arriba en la pila manejarán este error. Usar %w
asegura que puedan coincidir el error con errors.Is
o errors.As
si es relevante.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("obtener usuario %q: %w", id, err)
}
Bueno: Registrar el error y degradar graciosamente
Si la operación no es absolutamente necesaria, podemos proporcionar degradación grácil al recuperarnos de ella sin interrumpir la experiencia.
if err := emitMetrics(); err != nil {
// La falla al escribir las métricas no debería
// romper la aplicación.
log.Printf("No se pudieron emitir las métricas: %v", err)
}
Bueno: Coincidir el error y degradar graciosamente de manera apropiada
Si el llamado ha definido un error específico en su acuerdo y el fallo es recuperable, coincidir ese caso de error y degradarse graciosamente. Para todos los otros casos, envolver el error y devolverlo. Los errores más arriba en la pila manejarán otros errores.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// El usuario no existe. Usar UTC.
tz = time.UTC
} else {
return fmt.Errorf("obtener usuario %q: %w", id, err)
}
}
Manejo de Fallas de Asertos
Las afirmaciones de tipo entrarán en pánico con un único valor de retorno en caso de una detección de tipo incorrecta. Por lo tanto, siempre utiliza el "coma, ok" idiomático.
No Recomendado:
t := i.(string)
Recomendado:
t, ok := i.(string)
if !ok {
// Manejar el error graciosamente
}
Evite usar panic
El código que se ejecuta en el entorno de producción debe evitar usar panic. El panic es la principal fuente de fallas en cascada. Si ocurre un error, la función debe devolver el error y permitir que el llamante decida cómo manejarlo.
No recomendado:
func run(args []string) {
if len(args) == 0 {
panic("se requiere un argumento")
}
// ...
}
func main() {
run(os.Args[1:])
}
Recomendado:
func run(args []string) error {
if len(args) == 0 {
return errors.New("se requiere un argumento")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
El panic/recover no es una estrategia de manejo de errores. Solo debe haber un panic cuando ocurre un evento irreparable (por ejemplo, una referencia nula). Una excepción a esto es durante la inicialización del programa: las situaciones que provocarían un panic en el programa deben manejarse durante el inicio del programa.
var _statusTemplate = template.Must(template.New("nombre").Parse("_statusHTML"))
Incluso en el código de prueba, es preferible usar t.Fatal
o t.FailNow
en lugar del panic para asegurarse de que las fallas estén marcadas.
No recomendado:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("error al configurar la prueba")
}
Recomendado:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("error al configurar la prueba")
}