En Go (Golang) no existe un mecanismo específico para manejar excepciones, y los creadores del lenguaje no planean añadirlo. Se analizará si esto es positivo o negativo y cómo resolver mejor las situaciones problemáticas en las aplicaciones.
En Go se utiliza el término manejo de errores y su enfoque difiere significativamente de los practicados en otros lenguajes de programación ampliamente utilizados. Este enfoque suele ser criticado, pero también es elogiado con frecuencia.
Verificación de errores devueltos
A continuación, se presenta un ejemplo básico de manejo de errores en Go con una función que calcula el cociente de dos números almacenados en variables de tipo string:
func divide(a, b string) (int, error) {
firstNumber, err := strconv.Atoi(a)
if err != nil {
return 0, fmt.Errorf("convertir la cadena %s a número: %w", a, err)
}
secondNumber, err := strconv.Atoi(b)
if err != nil {
return 0, fmt.Errorf("convertir la cadena %s a número: %w", b, err)
}
division := firstNumber / secondNumber
return division, nil
}
La función strconv.Atoi
convierte una cadena en un número entero. El parámetro pasado puede no ser un número, por lo que la función devuelve dos valores: el primero es el resultado si la ejecución es exitosa; el segundo es el valor del error, si ocurre. Tras llamar a la función, se verifica si hubo un error y, en caso afirmativo, se realizan las siguientes acciones:
- Se añade información adicional al error, útil para identificar la causa de su aparición (usando la función
fmt.Errorf
y la secuencia especial de caracteres%w
). - La función interrumpe su ejecución normal y devuelve el error como valor (en Go, es común devolver el error como último valor).
Si no hay problemas, la función continúa y, al finalizar, devuelve el resultado (si lo hay) y un valor de error nulo.
Hay otra variante: por ejemplo, strconv.Itoa
convierte un número en una cadena y no devuelve errores.
Mecanismo de panic
Cuando un problema se vuelve crítico y la ejecución normal del programa es imposible, se utiliza el mecanismo de panic. En el ejemplo anterior, un panic ocurrirá si el divisor es cero. Si no se hace nada, la aplicación se detendrá. Para evitar esto, se debe añadir la siguiente verificación:
if secondNumber == 0 {
return 0, ErrZeroDivision
}
division := firstNumber / secondNumber
return division, nil
donde:
var ErrZeroDivision = errors.New("no se permite dividir por cero")
En esencia, todo se reduce a devolver valores de error desde las funciones y verificarlos posteriormente. El manejo explícito simplifica la resolución de situaciones problemáticas, pero requiere añadir código repetitivo y extenso para verificar err != nil
tras casi cada llamada a una función o método. Esto aumenta el número de líneas en el código fuente y dificulta la comprensión de la lógica principal. Este tipo de manejo de errores genera descontento entre programadores acostumbrados a un enfoque más tradicional de manejo de excepciones.
Si se añadieran excepciones en Go
Este enfoque es más sencillo: al surgir una situación problemática, en lugar de devolver explícitamente un valor de error, se lanza una excepción. Esta se propaga implícitamente hacia arriba en la pila de llamadas de funciones y puede ser manejada en un bloque especial. Si no se encuentra un bloque de manejo de excepciones, el programa (o, más precisamente, el hilo del programa) termina con un mensaje sobre la excepción. Al generarse una excepción, también se registra el estado de la pila de llamadas.
Esto elimina la necesidad de verificar errores en cada llamada a una función. Si Go incorporara excepciones en el futuro, podrían verse así:
func divide(a, b string) int {
firstNumber := strconv.Atoi(a)
secondNumber := strconv.Atoi(b)
division := firstNumber / secondNumber
return division
}
La verificación de errores desaparece. La función strconv.Atoi
ahora devuelve un solo valor y, en caso de un problema, lanza una excepción con el operador throw
:
func Atoi(s string) int {
if s == "" {
throw ErrEmptyArgument
}
// ...
}
donde:
var ErrEmptyArgument = errors.New("argumento vacío")
La división por cero también generaría una excepción. El código que llama a esta función se vería así:
try {
result := divide("15", "10")
fmt.Println(result)
} catch (e ErrZeroDivision) {
fmt.Println("No se puede dividir por cero")
} catch (e ErrEmptyArgument) {
fmt.Println("No se puede convertir una cadena vacía en número")
}
En el bloque try
se encuentra el código capaz de generar una excepción. Si se lanza una excepción, esta es capturada por uno de los bloques catch
y se ejecuta el código correspondiente al tipo de excepción. Si no se encuentra un bloque catch
adecuado para el tipo de excepción, esta se propaga hacia la función que la llamó y continúa hasta que se encuentre un catch
adecuado o el programa (hilo) termine. La función divide
sería más corta y fácil de leer.
Implementación de excepciones mediante panic
Los ejemplos de código con “excepciones” no funcionarían actualmente y, probablemente, tampoco en el futuro, ya que no está planeado añadirlas en Go. Sin embargo, es posible imitar el mecanismo de excepciones.
El resultado final sería algo así:
Try(func() {
result := divide("15", "10")
fmt.Println(result)
}, func(err error) {
if err == ErrZeroDivision {
fmt.Println("No se puede dividir por cero")
} else if err == ErrEmptyArgument {
fmt.Println("No se puede convertir una cadena vacía en número")
}
})
Para que esto funcione, las funciones deben modificarse así:
func divide(a, b string) int {
firstNumber := Atoi(a)
secondNumber := Atoi(b)
if secondNumber == 0 {
panic(ErrZeroDivision)
}
division := firstNumber / secondNumber
return division
}
func Atoi(s string) int {
if s == "" {
panic(ErrEmptyArgument)
}
converted, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return converted
}
Y el código de la función Try
:
var ErrNotAnError = errors.New("no es un error")
func Try(code func(), catch func(err error)) {
defer func() {
e := recover()
if e != nil {
err, ok := e.(error)
if !ok {
err = ErrNotAnError
}
catch(err)
}
}()
code()
}
El mecanismo de panic en Go es muy similar a las excepciones. Las funciones terminan implícitamente y el panic se propaga hacia atrás en la pila de llamadas hasta que el programa termina abruptamente. Sin embargo, el panic puede ser capturado y detenido con la función recover
. Además, la función integrada panic
es fácil de invocar. Esta función acepta un argumento de tipo interface{}
, que puede recuperarse tras llamar a recover
. La función recover
debe invocarse en una función diferida con defer
, ya que solo las funciones diferidas se ejecutan incluso durante un pánico.
Con este conocimiento, se puede proceder así: al surgir un problema, se genera un panic artificialmente, pasando el error ocurrido como argumento. Para capturar este error, se usa la función auxiliar Try
. El primer argumento es una función anónima con el código, y el segundo es una función que maneja cualquier error surgido. Si la función del primer argumento genera un panic, Try
captura el panic con recover
, extrae el tipo error
del valor devuelto y lo pasa a la función de manejo.
Este enfoque imita el manejo de excepciones en Go. La función Try
podría modificarse para aceptar múltiples manejadores de errores y llamar al adecuado, aunque implementarlo es complejo. Para mayor comodidad y reutilización, la función Try
debería colocarse en un paquete separado e importarse con un dot import, por ejemplo: import . "exception/try"
.
Ahora es posible usar un enfoque de manejo de excepciones, pero ¿es realmente tan bueno? El manejo de excepciones tiene desventajas, aunque está separado de la lógica del programa y permite escribir código más corto.
¿Son necesarias las excepciones en Go?
El código principal del programa es más claro, pero el manejo de excepciones es más difícil de usar y, lo que es más grave, distinguir entre un código que maneja excepciones correctamente y uno que no lo hace es complicado. Resolver situaciones problemáticas ya es una tarea ardua y responsable, y este enfoque imitativo la complica aún más. Los desarrolladores de Go decidieron que crear programas confiables requiere un enfoque diferente, con sus propias ventajas y desventajas. No son los únicos: alternativas a las excepciones existen en otros lenguajes de programación.
Entre los efectos secundarios de las excepciones está la pérdida de rendimiento, razón por la cual los desarrolladores de juegos las evitan en favor de soluciones alternativas.
Escribir código basado en excepciones en Go es posible hasta cierto punto, pero en la comunidad esto incluso se desaprueba. Al intentar usar este enfoque, surgirán problemas con el ecosistema del lenguaje. La biblioteca estándar y los paquetes de terceros usan el manejo de errores mediante valores devueltos, y combinar métodos complicará aún más la escritura de código.
El manejo de errores es una parte crucial del trabajo de un programador, y sea cual sea el método utilizado, el comportamiento del programa en situaciones problemáticas debe planificarse cuidadosamente. ¡Buena suerte!
Listado completo del código
package main
import (
"errors"
"fmt"
"strconv"
)
func main() {
Try(func() {
result := divide("15", "0")
fmt.Println(result)
}, func(err error) {
switch err {
case ErrZeroDivision:
fmt.Println("No se puede dividir por cero")
case ErrEmptyArgument:
fmt.Println("No se puede convertir una cadena vacía en número")
}
})
}
var ErrZeroDivision = errors.New("no se permite dividir por cero")
var ErrEmptyArgument = errors.New("argumento vacío")
func divide(a, b string) int {
firstNumber := Atoi(a)
secondNumber := Atoi(b)
if secondNumber == 0 {
panic(ErrZeroDivision)
}
division := firstNumber / secondNumber
return division
}
func Atoi(s string) int {
if s == "" {
panic(ErrEmptyArgument)
}
converted, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return converted
}
var ErrNotAnError = errors.New("no es un error")
func Try(code func(), catch func(err error)) {
defer func() {
e := recover()
if e != nil {
err, ok := e.(error)
if !ok {
err = ErrNotAnError
}
catch(err)
}
}()
code()
}
El código anterior demuestra cómo simular excepciones en Go, un lenguaje que no las soporta nativamente, y compara este enfoque con el manejo de errores estándar (devolviendo error).