La gestión de errores en Go es un tema que está en boca de todos: gophers o no, favorables al lenguaje o no. Y es precisamente esa la razón por la que David López Carrascal nos deleitó con una gran introducción al tema.
Sin embargo, parece que la decisión de prescindir de las excepciones (como tal, panics
a parte) es algo que sigue contrariando
a una parte considerable de la comunidad. El siguiente pedazo de código es demasiado habitual en nuestros desarrollos:
if err != nil {
// error handling
}
Y, aunque hay algunas estrategias para abordar esa problemática y reducir la cantidad de repeticiones de ese patrón, sigue habiendo casos dónde no nos queda otra que tirar del mismo.
Poner de manifiesto ese tipo de problemáticas da lugar a potenciales mejoras. Y estas se suelen presentar en forma de propuestas que, posteriormente, puede ser aprobadas o no.
Go v2: el borrador
Los que nos seguís desde hace tiempo, recordaréis que compartimos un conjunto de borradores, publicados por el equipo de Go, y dónde se recogían las principales problemáticas de la versión actual del lenguaje: los genéricos y la gestión de errores. Este conjunto de propuestas se encapsuló bajo lo que denominaron la segunda versión del lenguaje (Go v2), con todo lo que ello implicaba (véase Python).
Como decíamos, una de estas propuestas buscaba mejorar la gestión de errores.
Esto lo pretendían hacer añadiendo un par de funciones handle
y check
que nos permitirían identificar las funciones que devuelven un error y,
posteriormente, hacer la gestión del mismo (ver ejemplo).
Ahorrándonos así la gestión de los errores en el punto en que estos eran generados y permitiéndonos reducir la cantidad de veces que repetíamos el patrón que veíamos anteriormente.
Por suerte, o por desgracia, la comunidad Go es muy exigente, y en ese contexto es complicado llegar a consensos. En lo que a gestión de errores se refiere, hay quiénes ya están contentos con los recursos que nos ofrece la versión actual del lenguaje, y hay a quiénes les parece muy engorroso. Ambos bandos con una presencia igual de importante. Por esa razón, la propuesta de lanzar una nueva versión del lenguaje sigue aún en stand-by, sin embargo, la gestión de errores sigue en tela de juicio.
Try: la propuesta
Ese es el motivo por el que siguen surgiendo propuestas como la que queremos presentar hoy, cuyo principal objetivo es precisamente eso que veníamos comentando, eliminar todo el boilerplate que representa una repetición abusiva de:
if err != nil {
// error handling
}
Lo que nos proponen es la incorporación de una función ‘try’ (que a algunos os sonará de algo, pero no juzguéis antes de hora), mediante la cuál podemos evitar dicha repetición, a la vez que añadimos muy poca complejidad al lenguaje, una de las principales premisas de los creadores del mismo.
La firma de la función que proponen es:
func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)
Y ésta viene a hacer de handler de cualquier función que tenga entre sus variables retornadas, una de tipo error, concretamente la última.
Por eso, es importante tener en cuenta que, si llamamos a la función try
encapsulando una función que no devuelva un error,
entonces obtendremos un error de compilación.
Con esta encapsulación lo que conseguimos es:
- actualizar el valor de las variables receptoras en caso correcto.
- no actualizar el valor de las mismas en caso de error.
Y, por supuesto, de esta forma nos podríamos ahorrar la gestión de errores en cada punto dónde llamemos a una función que pueda retornar uno de ellos.
Bueno, ¿qué os parece?
Un ejemplo
Vale, sí, necesitáis ver un ejemplo de código dónde usarla para poder opinar. Véamos. Vamos a suponer que partimos del siguiente código:
func CopyFile(src, dst string) error {
r, err := os.Open(src)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if _, err := io.Copy(w, r); err != nil {
w.Close()
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
if err := w.Close(); err != nil {
os.Remove(dst)
return fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}
y recordamos que queremos evitar el siguiente patrón:
if err != nil {
// error handling
}
mediante el uso de la función try
:
func CopyFile(src, dst string) (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("copy %s %s: %v", src, dst, err)
}
}()
r := try(os.Open(src))
defer r.Close()
w := try(os.Create(dst))
defer func() {
w.Close()
if err != nil {
os.Remove(dst) // only if a “try” fails
}
}()
try(io.Copy(w, r))
try(w.Close())
return nil
}
Y ¡voilà! ¡Ahí lo tenemos!
Sin embargo, seguro que esta no es del gusto de todos. De hecho, esta sigue siendo, como su propio nombre indica, una propuesta. Pero una que intenta recoger el feedback de las críticas obtenidas en la propuesta anterior. Por supuesto, en la comunidad ya han salido muchos detractores, pues, como decíamos antes, hay quién ya tienen suficiente con lo que hay.
Y vosotros, ¿qué pensáis de la gestión de errores actual y de propuestas como las comentadas en este artículo?
Si queréis leer más en detalle y ver la opinión de la comunidad, os animamos a entrar en la propuesta.
Y, como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.