Go (a.k.a. Golang) es uno de los idiomas que más le interesa a las personas. A partir de Abril de 2018, ocupa el puesto 19 en el índice TIOBE. Cada vez más personas cambian de PHP, Node.js y otros idiomas a Go y lo utilizan en producción. Una gran cantidad de software genial (como Kubernetes, Docker y Heroku CLI) está escrito usando Go.

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

La simplicidad es genial: no necesitas 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 idiomas y, por lo tanto, necesitan codificar soluciones temporales o escribir más código a largo plazo. Desafortunadamente, a Go le faltan muchas características por diseño y, a veces, es realmente molesto.

Golang estaba destinado a hacer el desarrollo más rápido, pero en muchas situaciones, estás escribiendo más código de lo que escribirías usando otros lenguajes de programación. Describiré algunos de estos casos en mis críticas de Go a continuación.

Las 4 Críticas al Lenguaje Go

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

Voy a publicar un ejemplo de código real aquí. Cuando estaba trabajando en el enlace Selenium de Golang, necesitaba escribir una función que tiene tres parámetros. Dos de ellos eran opcionales. Esto es lo que parece 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 lo proporciona por diseño. Imagina lo que sucedería si accidentalmente llamara al equivocado. Aquí hay un ejemplo:

Me daría un montón de `undefined`

Tengo que admitir que a veces la sobrecarga de funciones puede dar como resultado un código desordenado. Por otro lado, debido a esto, 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 puede ver, se ve mucho más claro.

También me gusta el enfoque Elixir sobre 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 de hacerlo):

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. La falta de genéricos

Esta es sin duda la característica que los usuarios de Go piden más.

Imagina que deseas 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 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 por cuerdas. Tendrás que escribir otra implementación, que es exactamente la misma excepto la firma. Esta función necesitará un nombre diferente, ya que Golang no admite la sobrecarga de funciones. Como resultado, tendrás un montón de funciones similares con diferentes nombres, y se verá más o menos 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 (No repetir), que establece que debes escribir el menor código de copiar/pegar posible y, en su lugar, moverlo a funciones y volver a utilizarlas.

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 dar como resultado 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 simple 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 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 la dependencia es realmente difícil. Las herramientas de Go permiten a los usuarios instalar diferentes bibliotecas ejecutando go get <repositorio de biblioteca>. El problema aquí es la gestión de versiones. Si el mantenedor de la biblioteca hace algunos cambios incompatibles con versiones anteriores y los sube a GitHub, cualquiera que intente usar tu programa después de eso obtendrá un error, porque go get no hace nada más que git clone en tu repositorio en una carpeta de la biblioteca. Además, si la biblioteca no está instalada, el programa no se compilará debido a eso.

Puedes hacerlo un poco mejor usando Dep para gestionar dependencias (https://github.com/golang/dep), pero el problema aquí es que estás almacenando todo sus dependencias en su repositorio (que no es bueno, porque su repositorio contendrá no solo su código sino miles y miles de líneas de código de dependencia), o simplemente almacenará la lista de paquetes (pero nuevamente, si el mantenedor de la dependencia hace un reverso - cambio incompatible, todo se bloqueará).

¿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 paquetes, por lo que si necesita una versión específica de un paquete, no hay problema, puede obtenerlo 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 excelentes 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 completamente simple. En Go, básicamente puede devolver múltiples valores de funciones, y la función puede devolver un error. Algo como esto:

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

Ahora imagina que necesitas 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;
}

Hay un montón de código repetible aquí, que no es bueno. Y con funciones grandes, puede ser aún peor. Probablemente necesites una tecla en tu teclado para esto:

imagen cómica 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 puedes atraparlo. Considera 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 de Go

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

1. Goroutines

La programación asincrónica se hizo realmente simple en Go. Si bien la programación multiproceso suele ser difícil en otros idiomas, generar un nuevo hilo y ejecutar una función en él para que no bloquee el hilo 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 que se incluyen con Go

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

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

3. Defer

Creo que esta es una de las mejores características en el lenguaje. Imagina que necesitas escribir una función que abre tres archivos. Y si algo falla, tendrás que cerrar los archivos abiertos existentes. Si hay muchas construcciones así, se verá como un desastre. Considera este ejemplo de pseudo-có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 Go defer entra en su lugar:

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 ve, si obtenemos un error al abrir el archivo número tres, otros archivos se cerrarán automáticamente, ya que las sentencias defer se ejecutan antes de regresar en orden inverso. Además, es bueno tener archivos para abrir y cerrar 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 funciones realmente geniales. Sin embargo, hay muchas cosas que se pueden mejorar allí.

Si nosotros como desarrolladores Go, implementáramos estos cambios, beneficiaría mucho a nuestra comunidad, ya que hará que la programación con Go sea mucho más agradable.

Mientras tanto, si intentas mejorar tus pruebas con Go, intenta con Prueba tu aplicación Go: comienza de manera correcta por el colega de Toptal Gabriel Aszalos.

Understanding the Basics

Is Go a scripting language?

There's a thin line between the definition of a script and a program, but I'd say it's not a scripting language, since Go programs are not run in runtime—they are compiled and run as an executable.

About the author

Sergei Peshkov, Russia
member since January 19, 2018
Sergei has worked for more than three years as a back-end developer of web applications specializing in Node.js with MondoDB/PostgreSQL. He is also proficient with Goland and Elixir. [click to continue...]
Hiring? Meet the Top 10 Freelance Go Engineers for Hire in October 2018

Comments

comments powered by Disqus
Subscribe
Free email updates
Get the latest content first.
No spam. Just great articles & insights.
Free email updates
Get the latest content first.
Thank you for subscribing!
Check your inbox to confirm subscription. You'll start receiving posts after you confirm.
Trending articles
Relevant Technologies
About the author
Sergei Peshkov
SQL Developer
Sergei has worked for more than three years as a back-end developer of web applications specializing in Node.js with MondoDB/PostgreSQL. He is also proficient with Goland and Elixir.