juanjonavarro.com

secciones

Gestión de errores y liberación de recursos en Go

Hace algo así como un mes dediqué unas cuantas noches a aprender Go (más sobre esto en un próximo artículo) y una de las cosas que más me llamó la atención fue que el lenguaje Go no tiene excepciones.

En Go todas las funciones pueden devolver más de un resultado. Esto es aprovechado en todo el lenguaje para señalar condiciones de error. Muchas funciones devuelven un objeto “Error” (en realidad es un interface, por lo que puede estar devolviendose bastante información). El código que llama a la función debe simplemente comprobar el error. Por ejemplo, la función strconv.Atoi que convierte una cadena en un entero:

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("No se puede convertir número %v\n", err)
}
fmt.Println("Entero convertido", i)

Como se puede ver, la función devuelve a la vez dos valores, que se asignan a las variables i y errr. No es necesario recuperar siempre todos los valores devueltos, por lo que perfectamente se podría escribir i := strconv.Atoi("42") para ignorar el error.

Esto nos evita tener que utilizar siempre excepciones, como ocurre en Java:

int i = 0;
try {
	i = Integer.parseInt("42");
	System.out.println("Entero convertido "+i);
} catch (NumberFormatException e) {
	System.out.println("No se puede convertir número "+e.getMessage());
}

Las excepciones también se usan en Java y otros lenguajes para hacer “limpieza”, para asegurarte de que se cierran las conexiones a la bbdd, se liberan recursos, etc. El típico ejemplo en Java para acceder a una base de datos:

Connection conn = null;
Statement stmt = null;
try {
	conn = DriverManager.getConnection(DB_URL,USER,PASS);
	stmt = conn.createStatement();
	ResultSet rs = stmt.executeQuery(sql);
	while (rs.next()) {
		// Código de gestión de cada fila
	}
	rs.close();
} catch (SQLException e) {
	// Gestión del error
} finally {
	if (stmt!=null) {
		stmt.close();
	}
 	if (conn!=null) {
		conn.close();
 	}
}

El código en el finally nos aseguramos que siempre se ejecuta. No obstante, como no sabemos hasta donde se llegó a ejecutar nuestro código, tenemos que hacer esas feas comprobaciones para saber si los objetos stmt y conn valen null.

Go tiene una forma muy ingeniosa de crear este código de liberación de recursos. Veamos esta función de ejemplo, que copia un fichero:

func CopyFile(dstName, srcName string) (written int64, err error) {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    return io.Copy(dst, src)
}

La palabra clave defer de Go retrasa la ejecución de un código hasta que termina la función actual. En el ejemplo se retrasa la ejecución de src.Close() y de dst.Close(), las dos instrucciones que cierran los ficheros previamente abiertos. Las llamadas defer se van añadiendo a una pila de llamadas (por lo tanto, las últimas añadidas son las que primero se ejecutan) y se ejecutan al finalizar la función de ejemplo CopyFile.

Yo a esto le veo un par de ventajas:

  • Hace innecesario comprobar si los ficheros están previamente abiertos antes de cerrarlos. Simplemente, cada vez que se abre un fichero, se hace un defer de su cierre. Si se produce un error al abrir el segundo fichero, por ejemplo, el defer dst.Close() no llega a ejecutarse y por lo tanto no es llamado al finalizar la función.
  • El código que libera el recurso está cerca del código que lo maneja y no todo junto al final de la función, lo cual facilita entender mejor el código de liberación de recursos.
2 Comentarios
José Antonio
José Antonio
27 febrero 2015, 19:59 — #1

No conozco Go, pero para ser lo primero que he leído, me ha gustado.

Gracias por el artículo.

Juanjo Navarro
28 febrero 2015, 12:37 — #2

Gracias por tu mensaje, José Antonio.

Tengo previsto escribir algún otro articulo sobre Go, para tratar de resumir un poco lo que saqué en claro. Stay tuned! :-)

Comentarios cerrados para este artículo