4 Críticas al lenguaje Go

Publicado: 2022-03-11

El go (también conocido como Golang) es uno de los idiomas que más interesa a la gente. En abril de 2018, ocupa el puesto 19 en el índice TIOBE. Cada vez más personas están cambiando de PHP, Node.js y otros lenguajes a Go y usándolo en producción. Una gran cantidad de software interesante (como Kubernetes, Docker y Heroku CLI) se escribe con Go.

Entonces, ¿cuál es la clave del éxito de Go? Hay muchas cosas dentro del lenguaje que lo hacen realmente genial. Pero una de las principales cosas que hizo que Go fuera tan popular es su simplicidad, como lo señaló uno de sus creadores, Rob Pike.

La simplicidad es genial: no necesita aprender muchas palabras clave. Hace que el aprendizaje de idiomas sea muy fácil y rápido. Sin embargo, por otro lado, a veces los desarrolladores carecen de algunas características que tienen en otros lenguajes y, por lo tanto, necesitan codificar soluciones alternativas o escribir más código a largo plazo. Desafortunadamente, Go carece de muchas funciones por diseño y, a veces, es realmente molesto.

Golang estaba destinado a acelerar el desarrollo, pero en muchas situaciones, está escribiendo más código del que escribiría con otros lenguajes de programación. Describiré algunos de estos casos en mis críticas al lenguaje Go a continuación.

Las críticas del lenguaje 4 Go

1. Falta de sobrecarga de funciones y valores predeterminados para argumentos

Publicaré un ejemplo de código real aquí. Cuando estaba trabajando en el enlace Selenium de Golang, necesitaba escribir una función que tuviera tres parámetros. Dos de ellos eran opcionales. Así es como se ve después de la implementación:

 func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }

Tuve que implementar tres funciones diferentes porque no podía simplemente sobrecargar la función o pasar los valores predeterminados; Go no los proporciona por diseño. ¿Imagina qué pasaría si accidentalmente llamo al equivocado? Aquí hay un ejemplo:

Obtendría un montón de `undefined`

Tengo que admitir que a veces la sobrecarga de funciones puede resultar en un código desordenado. Por otro lado, por eso, los programadores necesitan escribir más código.

¿Cómo puede ser mejorado?

Aquí está el mismo (bueno, casi el mismo) ejemplo en JavaScript:

 function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }

Como puedes ver, se ve mucho más claro.

También me gusta el enfoque de Elixir en eso. Así es como se vería en Elixir (sé que podría usar valores predeterminados, como en el ejemplo anterior; solo lo muestro como una forma en que se puede hacer):

 defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")

2. Falta de genéricos

Podría decirse que esta es la función que más piden los usuarios de Go.

Imagine que desea escribir una función de mapa, donde está pasando la matriz de enteros y la función, que se aplicará a todos sus elementos. Suena fácil, ¿verdad?

Hagámoslo para números enteros:

 package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }

Se ve bien, ¿verdad?

Bueno, imagina que también necesitas hacerlo para cuerdas. Deberá escribir otra implementación, que es exactamente igual excepto por la firma. Esta función necesitará un nombre diferente, ya que Golang no admite la sobrecarga de funciones. Como resultado, tendrá un montón de funciones similares con diferentes nombres, y se verá así:

 func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }

Eso definitivamente va en contra del principio DRY (Don't Repeat Yourself), que establece que debe escribir la menor cantidad posible de código de copiar/pegar y, en su lugar, moverlo a funciones y reutilizarlas.

La falta de genéricos significa cientos de funciones variantes

Otro enfoque sería usar implementaciones únicas con interface{} como parámetro, pero esto puede generar un error de tiempo de ejecución porque la verificación de tipo de tiempo de ejecución es más propensa a errores. Y también será más lento, por lo que no hay una forma sencilla de implementar estas funciones como una sola.

¿Cómo puede ser mejorado?

Hay muchos buenos lenguajes que incluyen soporte genérico. Por ejemplo, aquí está el mismo código en Rust (he usado vec en lugar de array para hacerlo más simple):

 fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }

Tenga en cuenta que hay una sola implementación de la función de map y se puede usar para cualquier tipo que necesite, incluso los personalizados.

3. Gestión de Dependencias

Cualquiera que tenga experiencia en Go puede decir que la gestión de dependencias es realmente difícil. Las herramientas Go permiten a los usuarios instalar diferentes bibliotecas ejecutando go get <library repo> . El problema aquí es la gestión de versiones. Si el mantenedor de la biblioteca realiza algunos cambios incompatibles con versiones anteriores y lo carga en GitHub, cualquier persona que intente usar su programa después de eso recibirá un error, porque go get no hace nada más que git clone su repositorio en una carpeta de biblioteca. Además, si la biblioteca no está instalada, el programa no se compilará por eso.

Puede hacerlo un poco mejor usando Dep para administrar dependencias (https://github.com/golang/dep), pero el problema aquí es que está almacenando todas sus dependencias en su repositorio (lo cual no es bueno, porque su repositorio contienen no solo su código sino miles y miles de líneas de código de dependencia), o simplemente almacenan la lista de paquetes (pero nuevamente, si el mantenedor de la dependencia realiza un cambio incompatible con versiones anteriores, todo fallará).

¿Cómo puede ser mejorado?

Creo que el ejemplo perfecto aquí es Node.js (y JavaScript en general, supongo) y NPM. NPM es un repositorio de paquetes. Almacena las diferentes versiones de los paquetes, por lo que si necesita una versión específica de un paquete, no hay problema, puede obtenerla desde allí. Además, una de las cosas en cualquier aplicación Node.js/JavaScript es el archivo package.json . Aquí se enumeran todas las dependencias y sus versiones, por lo que puede instalarlas todas (y obtener las versiones que definitivamente funcionan con su código) con npm install .

Además, los grandes ejemplos de administración de paquetes son RubyGems/Bundler (para paquetes de Ruby) y Crates.io/Cargo (para bibliotecas de Rust).

4. Manejo de errores

El manejo de errores en Go es muy simple. En Go, básicamente puede devolver múltiples valores de las funciones, y la función puede devolver un error. Algo como esto:

 err, value := someFunction(); if err != nil { // handle it somehow }

Ahora imagine que necesita escribir una función que realice tres acciones que devuelvan un error. Se verá algo como esto:

 func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }

Aquí hay mucho código repetible, lo cual no es bueno. ¡Y con funciones grandes, puede ser incluso peor! Probablemente necesitará una tecla en su teclado para esto:

imagen humorística del código de manejo de errores en un teclado

¿Cómo puede ser mejorado?

Me gusta el enfoque de JavaScript sobre eso. La función puede arrojar un error y usted puede detectarlo. Considere el ejemplo:

 function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }

Es mucho más claro y no contiene código repetible para el manejo de errores.

Las cosas buenas en Go

Aunque Go tiene muchos defectos de diseño, también tiene algunas características realmente geniales.

1. Gorrutinas

La programación asíncrona se hizo realmente simple en Go. Si bien la programación de subprocesos múltiples suele ser difícil en otros idiomas, generar un nuevo subproceso y ejecutar una función en él para que no bloquee el subproceso actual es realmente simple:

 func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }

2. Herramientas incluidas con Go

Mientras que en otros lenguajes de programación necesita instalar diferentes bibliotecas/herramientas para diferentes tareas (como pruebas, formato de código estático, etc.), hay muchas herramientas geniales que ya están incluidas en Go de forma predeterminada, como:

  • gofmt - Una herramienta para el análisis de código estático. En comparación con JavaScript, donde necesita instalar una dependencia adicional, como eslint o jshint , aquí se incluye de forma predeterminada. Y el programa ni siquiera compilará si no escribe un código de estilo Go (sin usar variables declaradas, importando paquetes no utilizados, etc.).
  • go test - Un marco de prueba. Nuevamente, en comparación con JavaScript, debe instalar dependencias adicionales para realizar pruebas (Jest, Mocha, AVA, etc.). Aquí, está incluido por defecto. Y le permite hacer muchas cosas geniales de manera predeterminada, como evaluación comparativa, convertir código en documentación a pruebas, etc.
  • godoc - Una herramienta de documentación. Es bueno tenerlo incluido en las herramientas predeterminadas.
  • El propio compilador. ¡Es increíblemente rápido, en comparación con otros lenguajes compilados!

3. Aplazar

Creo que esta es una de las mejores características del lenguaje. Imagina que necesitas escribir una función que abra tres archivos. Y si algo falla, deberá cerrar los archivos abiertos existentes. Si hay muchas construcciones así, parecerá un desastre. Considere este ejemplo de pseudocódigo:

 function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }

Parece complicado. Ahí es donde entra en juego el defer de Go:

 package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }

Como puede ver, si obtenemos un error al abrir el archivo número tres, los demás archivos se cerrarán automáticamente, ya que las declaraciones de defer se ejecutan antes de volver en orden inverso. Además, es bueno tener la apertura y cierre de archivos en el mismo lugar en lugar de diferentes partes de una función.

Conclusión

No mencioné todas las cosas buenas y malas de Go, solo las que considero las mejores y las peores.

Go es realmente uno de los lenguajes de programación interesantes en uso actual, y realmente tiene potencial. Nos proporciona herramientas y características realmente geniales. Sin embargo, hay muchas cosas que se pueden mejorar allí.

Si nosotros, como desarrolladores de Go, implementamos estos cambios, beneficiará mucho a nuestra comunidad, porque hará que programar con Go sea mucho más agradable.

Mientras tanto, si está tratando de mejorar sus pruebas con Go, intente Probar su aplicación Go: Comience de la manera correcta por el compañero Toptaler Gabriel Aszalos.

Relacionado: Lógica bien estructurada: un tutorial de Golang OOP