domingo, 6 de septiembre de 2020

Manejo de errores en Go vs Rust



Sin duda alguna si hablamos de lenguajes de sistemas los que están haciendo más ruido hoy en día son Go y Rust. 

Son lenguajes que se pensaron para la performance y son de más bajo nivel que Java o Python. Pero a pesar de eso son lenguajes modernos que tienen una especial visión de los errores. 

Go utiliza valores de error para indicar un estado anormal. Por ejemplo, la función os.Open devuelve un valor de error que no es nulo cuando no puede abrir un archivo.

func Open(name string) (file *File, err error)

Por lo tanto si queremos ver si ocurrio un error podemos hacer : 

f, err := os.Open("filename.ext")

if err != nil {

    log.Fatal(err)

}

Lo interesante de esta visión es que nos alejamos del pensamiento o la filosofía de Java en el que el error es un valor especial que debe ser lanzado o tratado, y no es necesario utilizar secuencias try-catch. 

En cambio Rust tiene una visión más funcional, maneja los errores como Haskell. Es decir, devuelve algo llamado Result.

El tipo Result <T, E> es una enumeración que tiene dos variantes: Ok (T) para valor exitoso o Err (E) para valor de error:

enum Result<T, E> {

   Ok(T),

   Err(E),

}

Devolver errores en lugar de lanzarlos es un cambio de paradigma en el manejo de errores y es similar a lo que hace Go, pero en este caso me gusta mucho más porque funciona muy bien con pattern matching pero vamos de a poco.

Si necesitamos ignorar el manejo de errores, no sé porque estamos seguros que no va a pasar o solo porque somos inconscientes podemos utilizar unwrap, por ejemplo : 

use std::fs;

fn main() {

  let content = fs::read_to_string("./Cargo.toml").unwrap();

  println!("{}", content)

}

Aunque sabemos que el archivo estaría presente, el compilador no tiene forma de saberlo. Entonces usamos unwrap para decirle al compilador que confíe en nosotros y devuelva el valor. Si la función read_to_string devuelve un valor Ok (), desenvolver obtendrá el contenido y lo asignará a la variable de contenido. Si devuelve un error, el programa entrará en pánico. Panic termina el programa o sale del hilo actual. (en otro post voy a hablar de panic!) 

expect es lo mismo que unwrap pero nos permite agregar un mensaje de error adicional.

use std::fs;

fn main() {

  let content = fs::read_to_string("./Cargo.toml").expect("Can't read Cargo.toml");

  println!("{}", content)

}

También tenemos varios métodos que nos permiten hacer algo si sale un error cuando desenvolvemos un valor, como unwrap_or_else, unwrap_or_default, veamos un ejemplo : 

use std::env;

fn main() {

  let port = env::var("PORT").unwrap_or("3000".to_string());

  println!("{}", port);

}

Muy elegante, me gusta mucho en Go deberíamos hacer : 

port, ok := os.LookupEnv("PORT")

if !ok {

port = 3000

En Rust tambien podemos utilizar pattern matching que lo hace un tanto más fácil de leer, veamos un ejemplo:

fn main() {
  match fxParaLeerData() {
    Ok(data) => println!("pudimos leer {}!!", data),
    Err(e) => eprintln!("Hubo un problema  {}", e),
  }
}

La verdad que me gusta mucho más el concepto de try-catch es como que separa más la lógica de el manejo de problemas o errores. Más allá de esta opinión creo que una visión más elegante fue la que tomo Rust. Por favor dejen su opinión. 

Hasta acá llego el post pero seguro que va a ver parte 2 ... 


2 comentarios:

  1. Te faltó mencionar el operador `?` de Rust. Con él, es muy fácil manejar errores. Hace que sea similar a try-catch, pero explícito, lo cual lo considero mejor.

    ResponderBorrar