Por qué Go es malo para los programadores poco inteligentes

El artículo fue escrito como respuesta a un artículo publicado anteriormente. artículo de las antípodas.

Por qué Go es malo para los programadores poco inteligentes

Durante los últimos dos años he estado usando Go para implementar un servidor RADIUS especializado con un sistema de facturación desarrollado. A lo largo del camino, estoy aprendiendo las complejidades del idioma mismo. Los programas en sí son muy simples y no son el propósito de este artículo, pero la experiencia de usar Go merece unas pocas palabras en su defensa. Go se está convirtiendo en un lenguaje cada vez más común para código serio y escalable. El idioma fue creado por Google, donde se utiliza activamente. En pocas palabras, honestamente creo que el diseño del lenguaje Go es malo para los programadores UNintelligent.

¿Diseñado para programadores débiles?

Los débiles hablan de problemas. Los fuertes hablan de ideas y sueños...

Go es muy fácil de aprender, tan fácil que puedes leer el código prácticamente sin ningún tipo de formación. Esta característica del lenguaje se utiliza en muchas empresas globales cuando el código se lee junto con especialistas secundarios (gerentes, clientes, etc.). Esto es muy conveniente para metodologías como Design Driven Development.
Incluso los programadores novatos comienzan a producir código bastante decente después de una semana o dos. El libro que estudié es "Go Programming" (de Mark Summerfield). El libro es muy bueno, toca muchos matices del idioma. Después de lenguajes innecesariamente complicados como Java y PHP, la falta de magia es refrescante. Pero tarde o temprano, muchos programadores limitados tienen la idea de utilizar métodos antiguos en un campo nuevo. ¿Es esto realmente necesario?

Rob Pike (el principal ideólogo del lenguaje) creó el lenguaje Go como un lenguaje industrial fácil de entender y eficaz de usar. El lenguaje está diseñado para la máxima productividad en equipos grandes y no hay duda al respecto. Muchos programadores novatos se quejan de que les faltan muchas funciones. Este deseo de simplicidad fue una decisión consciente de los diseñadores del lenguaje y, para comprender completamente por qué era necesario, debemos comprender la motivación de los desarrolladores y lo que intentaban lograr en Go.

Entonces, ¿por qué se hizo tan simple? Aquí hay un par de citas de Rob Pike:

El punto clave aquí es que nuestros programadores no son investigadores. Generalmente son bastante jóvenes, vienen a nosotros después de estudiar, tal vez hayan estudiado Java, C/C++ o Python. No pueden entender un gran idioma, pero al mismo tiempo queremos que creen un buen software. Por eso el idioma debe ser fácil de entender y aprender.

Debería resultarle familiar, en términos generales similar a C. Los programadores que trabajan en Google comienzan sus carreras temprano y en su mayoría están familiarizados con los lenguajes de procedimiento, en particular la familia C. El requisito de una productividad rápida en un nuevo lenguaje de programación significa que el lenguaje no debería ser demasiado radical.

Sabias palabras, ¿no?

Artefactos de simplicidad

La simplicidad es una condición necesaria para la belleza. Lev Tolstoi.

Mantenerlo simple es uno de los objetivos más importantes en cualquier diseño. Como sabes, un proyecto perfecto no es aquel en el que no hay nada que añadir, sino aquel en el que no hay nada que quitar. Mucha gente cree que para resolver (o incluso expresar) problemas complejos se necesita una herramienta compleja. Sin embargo, no lo es. Tomemos como ejemplo el lenguaje PERL. Los ideólogos del lenguaje creían que un programador debería tener al menos tres formas diferentes de resolver un problema. Los ideólogos del lenguaje Go tomaron un camino diferente; decidieron que un camino, pero realmente bueno, era suficiente para lograr el objetivo. Este enfoque tiene una base seria: la única manera es más fácil de aprender y más difícil de olvidar.

Muchos inmigrantes se quejan de que el idioma no contiene abstracciones elegantes. Sí, esto es cierto, pero esta es una de las principales ventajas del idioma. El lenguaje contiene un mínimo de magia, por lo que no se requieren conocimientos profundos para leer el programa. En cuanto a la verbosidad del código, esto no es un problema en absoluto. Un programa Golang bien escrito se lee verticalmente, con poca o ninguna estructura. Además, la velocidad de lectura de un programa es al menos un orden de magnitud mayor que la velocidad de escritura. Si considera que todo el código tiene un formato uniforme (hecho usando el comando gofmt incorporado), entonces leer algunas líneas adicionales no es un problema en absoluto.

no muy expresivo

El arte no tolera que se limite su libertad. La precisión no es su responsabilidad.

Debido al deseo de simplicidad, Go carece de constructos que en otros idiomas son percibidos como algo natural por personas acostumbradas a ellos. Al principio puede resultar algo incómodo, pero luego te das cuenta de que el programa es mucho más fácil y menos ambiguo de leer.

Por ejemplo, una utilidad de consola que lea la entrada estándar o un archivo desde los argumentos de la línea de comandos tendría este aspecto:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

La solución al mismo problema en D, aunque parece algo más corta, no es más fácil de leer.

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

infierno de copiar

El hombre lleva dentro de sí el infierno. Martín Lutero.

Los principiantes se quejan constantemente de Go por la falta de genéricos. Para resolver este problema, la mayoría utiliza la copia directa de código. Por ejemplo, una función para sumar una lista de números enteros, estos aspirantes a profesionales creen que la funcionalidad no se puede implementar de otra manera que simplemente copiar y pegar para cada tipo de datos.

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

El lenguaje tiene medios suficientes para implementar tales construcciones. Por ejemplo, la programación genérica estaría bien.

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

Y, aunque nuestro código resultó algo más largo que el caso anterior, se ha generalizado. Por tanto, no nos resultará difícil implementar todas las operaciones aritméticas.

Muchos dirán que un programa en D parece mucho más corto, y tendrán razón.

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

Sin embargo, es más breve, pero no más correcto, ya que la implementación D ignora por completo el problema del manejo de errores.

En la vida real, a medida que aumenta la complejidad de la lógica, la brecha se reduce rápidamente. La brecha se cierra aún más rápidamente cuando necesita realizar una acción que no se puede realizar utilizando operadores de lenguaje estándar.

En términos de mantenibilidad, extensibilidad y legibilidad, en mi opinión, el lenguaje Go gana, aunque pierde en detalle.

La programación generalizada en algunos casos nos aporta beneficios innegables. Esto se ilustra claramente con el paquete sort. Entonces, para ordenar cualquier lista, solo necesitamos implementar la interfaz sort.Interface.

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

Si toma cualquier proyecto de código abierto y ejecuta el comando grep “interface{}” -R, verá con qué frecuencia se utilizan interfaces confusas. Los camaradas de mente cerrada dirán inmediatamente que todo esto se debe a la falta de genéricos. Sin embargo, este no es siempre el caso. Tomemos como ejemplo a DELPHI. A pesar de la presencia de estos mismos genéricos, contiene un tipo VARIANT especial para operaciones con tipos de datos arbitrarios. El lenguaje Go hace lo mismo.

De un arma en gorriones

Y la camisa de fuerza debe ajustarse al tamaño de la locura. Stanislav Lec.

Muchos fanáticos extremos pueden afirmar que Go tiene otro mecanismo para crear genéricos: la reflexión. Y tendrán razón... pero sólo en casos excepcionales.

Rob Pike nos advierte:

Esta es una herramienta poderosa que debe usarse con precaución. Debe evitarse a menos que sea estrictamente necesario.

Wikipedia nos dice lo siguiente:

La reflexión se refiere al proceso durante el cual un programa puede monitorear y modificar su propia estructura y comportamiento durante la ejecución. El paradigma de programación que subyace a la reflexión se llama programación reflexiva. Este es un tipo de metaprogramación.

Sin embargo, como sabes, hay que pagar por todo. En este caso lo es:

  • dificultad para escribir programas
  • velocidad de ejecución del programa

Por tanto, el reflejo debe utilizarse con precaución, como arma de gran calibre. El uso irreflexivo de la reflexión conduce a programas ilegibles, errores constantes y baja velocidad. Lo ideal para un programador snob es poder mostrar su código delante de otros colegas más pragmáticos y modestos.

¿El equipaje cultural de Xi? ¡No, de varios idiomas!

Junto a la fortuna, también quedan deudas para los herederos.

A pesar de que muchos creen que el lenguaje se basa completamente en la herencia C, este no es el caso. El lenguaje incorpora muchos aspectos de los mejores lenguajes de programación.

sintaxis

En primer lugar, la sintaxis de las estructuras gramaticales se basa en la sintaxis del lenguaje C. Sin embargo, el lenguaje DELPHI también tuvo una influencia significativa. Así, vemos que se han eliminado por completo los paréntesis redundantes, que reducen en gran medida la legibilidad del programa. El lenguaje también contiene el operador “:=" inherente al lenguaje DELPHI. El concepto de paquete está tomado de lenguajes como ADA. La declaración de entidades no utilizadas está tomada del lenguaje PROLOG.

Semántica

Los paquetes se basaron en la semántica del lenguaje DELPHI. Cada paquete encapsula datos y código y contiene entidades públicas y privadas. Esto le permite reducir la interfaz del paquete al mínimo.

La operación de implementación por método de delegación se tomó prestada del lenguaje DELPHI.

Compilacion

No en vano hay una broma: Go se desarrolló mientras se compilaba un programa en C. Uno de los puntos fuertes del lenguaje es su compilación ultrarrápida. La idea fue tomada del lenguaje DELPHI. Cada paquete Go corresponde a un módulo DELPHI. Estos paquetes se vuelven a compilar sólo cuando es realmente necesario. Por lo tanto, después de la siguiente edición, no necesita compilar todo el programa, sino volver a compilar solo los paquetes modificados y los paquetes que dependen de estos paquetes modificados (e incluso entonces, solo si las interfaces del paquete han cambiado).

Construcciones de alto nivel

El lenguaje contiene muchas construcciones diferentes de alto nivel que no están relacionadas de ninguna manera con lenguajes de bajo nivel como C.

  • Cuerdas
  • tablas hash
  • rebanadas
  • La mecanografía Duck se toma prestada de lenguajes como RUBY (que, desafortunadamente, muchos no comprenden ni utilizan en todo su potencial).

Gestión de la memoria

La gestión de la memoria generalmente merece un artículo aparte. Si en lenguajes como C++ el control se deja completamente en manos del desarrollador, en lenguajes posteriores como DELPHI se utilizó un modelo de recuento de referencias. Con este enfoque, no se permitieron referencias cíclicas, ya que se formaron grupos huérfanos, por lo que Go tiene una detección integrada de dichos grupos (como C#). Además, el recolector de basura es más eficiente que la mayoría de las implementaciones conocidas actualmente y ya puede usarse para muchas tareas en tiempo real. El propio lenguaje reconoce situaciones en las que se puede asignar en la pila un valor para almacenar una variable. Esto reduce la carga en el administrador de memoria y aumenta la velocidad del programa.

Concurrencia y Concurrencia

El paralelismo y la competitividad del idioma son incomparables. Ningún lenguaje de bajo nivel puede competir ni remotamente con Go. Para ser justos, vale la pena señalar que el modelo no fue inventado por los autores del lenguaje, sino que simplemente fue tomado prestado del viejo lenguaje ADA. El lenguaje es capaz de procesar millones de conexiones paralelas utilizando todas las CPU, al tiempo que tiene problemas de un orden de magnitud menos complejos con interbloqueos y condiciones de carrera que son típicos del código multiproceso.

Beneficios adicionales

Si es rentable, todos se volverán desinteresados.

El lenguaje también nos proporciona una serie de beneficios indudables:

  • Un único archivo ejecutable después de construir el proyecto simplifica enormemente la implementación de aplicaciones.
  • La escritura estática y la inferencia de tipos pueden reducir significativamente la cantidad de errores en su código, incluso sin escribir pruebas. Conozco algunos programadores que prescinden de escribir pruebas y la calidad de su código no se ve afectada significativamente.
  • Compilación cruzada muy simple y excelente portabilidad de la biblioteca estándar, lo que simplifica enormemente el desarrollo de aplicaciones multiplataforma.
  • Las expresiones regulares de RE2 son seguras para subprocesos y tienen tiempos de ejecución predecibles.
  • Una poderosa biblioteca estándar que permite que la mayoría de los proyectos funcionen sin marcos de terceros.
  • El lenguaje es lo suficientemente potente como para centrarse en el problema en lugar de cómo resolverlo, pero lo suficientemente bajo como para que el problema pueda resolverse de manera eficiente.
  • El sistema Go eco ya contiene herramientas desarrolladas listas para usar para todas las ocasiones: pruebas, documentación, gestión de paquetes, potentes linters, generación de código, detector de condiciones de carrera, etc.
  • La versión 1.11 de Go introdujo la gestión de dependencias semánticas integrada, basada en el popular alojamiento VCS. Todas las herramientas que componen el ecosistema Go utilizan estos servicios para descargar, crear e instalar código de una sola vez. Y eso es genial. Con la llegada de la versión 1.11, el problema con el versionado de paquetes también quedó completamente resuelto.
  • Debido a que la idea central del lenguaje es reducir la magia, el lenguaje incentiva a los desarrolladores a manejar errores explícitamente. Y esto es correcto, porque de lo contrario, simplemente se olvidará por completo del manejo de errores. Otra cosa es que la mayoría de los desarrolladores ignoran deliberadamente el manejo de errores y prefieren, en lugar de procesarlos, simplemente reenviar el error hacia arriba.
  • El lenguaje no implementa la metodología clásica de programación orientada a objetos, ya que en su forma pura no hay virtualidad en Go. Sin embargo, esto no supone un problema cuando se utilizan interfaces. La ausencia de programación orientada a objetos reduce significativamente la barrera de entrada para los principiantes.

Simplicidad para el beneficio de la comunidad

Es fácil de complicar, difícil de simplificar.

Go fue diseñado para ser simple y logra ese objetivo. Fue escrito para programadores inteligentes que comprenden los beneficios del trabajo en equipo y están cansados ​​de la infinita variabilidad de los lenguajes de nivel empresarial. Al tener un conjunto relativamente pequeño de estructuras sintácticas en su arsenal, prácticamente no está sujeto a cambios con el tiempo, por lo que los desarrolladores tienen mucho tiempo libre para el desarrollo y no para estudiar sin cesar las innovaciones del lenguaje.

Las empresas también obtienen una serie de ventajas: una barrera de entrada baja les permite encontrar rápidamente un especialista y la inmutabilidad del idioma les permite utilizar el mismo código incluso después de 10 años.

Conclusión

El gran tamaño del cerebro nunca ha convertido a ningún elefante en ganador del Premio Nobel.

Para aquellos programadores cuyo ego personal prima sobre el espíritu de equipo, así como para los teóricos que aman los desafíos académicos y la "superación personal" sin fin, el lenguaje es realmente malo, ya que es un lenguaje artesanal de propósito general que no permite obtener placer estético por el resultado de su trabajo y mostrarse profesional frente a sus compañeros (siempre que midamos la inteligencia según estos criterios, y no según el coeficiente intelectual). Como todo en la vida, es una cuestión de prioridades personales. Como todas las innovaciones que valen la pena, el lenguaje ya ha recorrido un largo camino desde la negación universal hasta la aceptación masiva. El lenguaje es ingenioso en su simplicidad y, como sabes, ¡todo lo ingenioso es simple!

Fuente: habr.com

Añadir un comentario