Por que Go é malo para programadores pouco intelixentes

O artigo foi escrito como resposta a un publicado anteriormente artigo antípoda.

Por que Go é malo para programadores pouco intelixentes

Durante os últimos dous anos estiven usando Go para implementar un servidor RADIUS especializado cun sistema de facturación desenvolvido. Ao longo do camiño, estou aprendendo as complejidades da propia lingua. Os programas en si son moi sinxelos e non son o propósito deste artigo, pero a propia experiencia de usar Go merece unhas palabras na súa defensa. Go está a converterse nunha linguaxe cada vez máis común para código serio e escalable. A linguaxe foi creada por Google, onde se usa activamente. En definitiva, sinceramente creo que o deseño da linguaxe Go é malo para os programadores pouco intelixentes.

Deseñado para programadores débiles?

Os débiles falan de problemas. O forte fala de ideas e soños...

Go é moi sinxelo de aprender, tan fácil que podes ler o código sen practicamente ningún adestramento. Esta característica da linguaxe utilízase en moitas empresas globais cando o código se le xunto con especialistas non principais (xestores, clientes, etc.). Isto é moi conveniente para metodoloxías como o Desenvolvemento impulsado por deseño.
Mesmo os programadores novatos comezan a producir un código bastante decente despois dunha ou dúas semanas. O libro do que estudei é "Go Programming" (de Mark Summerfield). O libro está moi ben, toca moitos matices da lingua. Despois de linguaxes innecesariamente complicadas como Java, PHP, a falta de maxia é refrescante. Pero, tarde ou cedo, moitos programadores limitados teñen a idea de usar métodos antigos nun campo novo. Isto é realmente necesario?

Rob Pike (o principal ideólogo da lingua) creou a linguaxe Go como unha linguaxe industrial fácil de entender e eficaz de usar. A linguaxe está deseñada para a máxima produtividade en grandes equipos e non hai dúbida. Moitos programadores novatos quéixanse de que hai moitas funcións que lles faltan. Este desexo de sinxeleza foi unha decisión consciente dos deseñadores da linguaxe e, para entender ben por que era necesario, debemos entender a motivación dos desenvolvedores e o que intentaban conseguir en Go.

Entón, por que se fixo tan sinxelo? Aquí tes un par de citas de Rob Pike:

O punto clave aquí é que os nosos programadores non son investigadores. Son, por regra xeral, bastante novos, chegan a nós despois de estudar, quizais estudaron Java, ou C/C++ ou Python. Non poden entender un gran idioma, pero ao mesmo tempo queremos que creen un bo software. É por iso que a lingua debe ser fácil de entender e aprender.

Debería ser familiar, falando máis ou menos parecido a C. Os programadores que traballan en Google comezan as súas carreiras cedo e están familiarizados principalmente coas linguaxes de procedemento, en particular coa familia C. O requisito dunha produtividade rápida nunha nova linguaxe de programación significa que a linguaxe non debe ser demasiado radical.

Palabras sabias, non?

Artefactos da Sinxeleza

A sinxeleza é unha condición necesaria para a beleza. Lev Tolstoi.

Mantelo sinxelo é un dos obxectivos máis importantes en calquera deseño. Como sabedes, un proxecto perfecto non é un proxecto onde non hai nada que engadir, senón un do que non hai nada que eliminar. Moitas persoas cren que para resolver (ou incluso expresar) problemas complexos é necesaria unha ferramenta complexa. Non obstante, non o é. Poñamos por exemplo a linguaxe PERL. Os ideólogos da linguaxe crían que un programador debería ter polo menos tres formas diferentes de resolver un problema. Os ideólogos da lingua Go tomaron un camiño diferente, decidiron que un camiño, pero moi bo, era suficiente para acadar o obxectivo. Este enfoque ten unha base seria: o único xeito é máis fácil de aprender e máis difícil de esquecer.

Moitos emigrantes quéixanse de que a lingua non contén abstraccións elegantes. Si, isto é certo, pero esta é unha das principais vantaxes da lingua. A linguaxe contén un mínimo de maxia, polo que non se requiren coñecementos profundos para ler o programa. En canto á verbosidade do código, isto non é un problema en absoluto. Un programa Golang ben escrito le en vertical, con pouca ou ningunha estrutura. Ademais, a velocidade de lectura dun programa é polo menos unha orde de magnitude maior que a velocidade de escritura. Se consideras que todo o código ten un formato uniforme (feito usando o comando gofmt incorporado), entón ler algunhas liñas extra non é un problema en absoluto.

Non moi expresivo

A arte non tolera cando a súa liberdade está restrinxida. A precisión non é a súa responsabilidade.

Debido ao afán de sinxeleza, Go carece de construcións que noutras linguas sexan percibidas como algo natural polas persoas afeitas a elas. Ao principio pode resultar algo incómodo, pero despois notas que o programa é moito máis doado e sen ambigüidades de ler.

Por exemplo, unha utilidade de consola que le stdin ou un ficheiro desde argumentos da liña de comandos sería así:

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)
}

A solución ao mesmo problema en D, aínda que parece algo máis breve, non é máis fácil de ler

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);
    }
}

O inferno de copiar

O home leva o inferno dentro de si. Martín Lutero.

Os principiantes quéixanse constantemente de Go en canto á falta de xenéricos. Para resolver este problema, a maioría deles usa a copia directa de código. Por exemplo, unha función para sumar unha lista de números enteiros, tales aspirantes a profesionais cren que a funcionalidade non se pode implementar doutro xeito que mediante un simple copiar e 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))
}

A lingua ten medios suficientes para implementar tales construcións. Por exemplo, a programación xenérica estaría ben.

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))
}

E, aínda que o noso código resultou ser algo máis longo que o caso anterior, xeneralizouse. Polo tanto, non nos será difícil implementar todas as operacións aritméticas.

Moitos dirán que un programa en D parece significativamente máis curto, e terán razón.

import std.stdio;
import std.algorithm;

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

Non obstante, só é máis breve, pero non máis correcta, xa que a implementación D ignora por completo o problema do tratamento de erros.

Na vida real, a medida que aumenta a complexidade da lóxica, a brecha redúcese rapidamente. A brecha péchase aínda máis rapidamente cando precisa realizar unha acción que non se pode realizar mediante operadores de linguaxe estándar.

En canto a mantebilidade, extensibilidade e lexibilidade, na miña opinión, a linguaxe Go gaña, aínda que perde en verbosidade.

A programación xeneralizada nalgúns casos ofrécenos beneficios innegables. Isto está claramente ilustrado polo paquete de clasificación. Entón, para ordenar calquera lista, só necesitamos implementar a interface 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)
}

Se tomas calquera proxecto de código aberto e executas o comando grep "interface{}" -R, verás con que frecuencia se usan interfaces confusas. Os compañeiros pechados dirán inmediatamente que todo isto se debe á falta de xenéricos. Non obstante, non sempre é así. Tomemos DELPHI como exemplo. A pesar da presenza destes mesmos xenéricos, contén un tipo VARIANT especial para operacións con tipos de datos arbitrarios. O idioma Go fai o mesmo.

Dende un canón ata os pardais

E a camisa de forza debe axustarse ao tamaño da tolemia. Stanislav Lec.

Moitos fans extremos poden afirmar que Go ten outro mecanismo para crear xenéricos: a reflexión. E terán razón... pero só en casos raros.

Rob Pike advírtenos:

Esta é unha ferramenta poderosa que debe usarse con precaución. Debe evitarse a non ser que sexa estritamente necesario.

A Wikipedia dinos o seguinte:

A reflexión refírese ao proceso durante o cal un programa pode supervisar e modificar a súa propia estrutura e comportamento durante a execución. O paradigma de programación que subxace á reflexión chámase programación reflexiva. Este é un tipo de metaprogramación.

Non obstante, como sabes, hai que pagar por todo. Neste caso é:

  • dificultade para escribir programas
  • velocidade de execución do programa

Polo tanto, a reflexión debe usarse con precaución, como unha arma de gran calibre. O uso irreflexivo da reflexión leva a programas ilexíbeis, erros constantes e baixa velocidade. Xusto a cousa para que un programador snob poida mostrar o seu código diante doutros compañeiros máis pragmáticos e modestos.

Bagaxe cultural de Xi? Non, de varias linguas!

Xunto coa fortuna, tamén se deixan débedas aos herdeiros.

A pesar de que moitos cren que a lingua está totalmente baseada no patrimonio C, non é así. A linguaxe incorpora moitos aspectos das mellores linguaxes de programación.

sintaxe

En primeiro lugar, a sintaxe das estruturas gramaticais baséase na sintaxe da linguaxe C. Porén, a lingua DELPHI tamén tivo unha influencia significativa. Así, vemos que as parénteses redundantes, que reducen moito a lexibilidade do programa, foron completamente eliminadas. A linguaxe tamén contén o operador ":="" inherente á linguaxe DELPHI. O concepto de paquetes está tomado de linguaxes como ADA. A declaración de entidades non utilizadas tómase prestada da linguaxe PROLOG.

Semántica

Os paquetes baseáronse na semántica da linguaxe DELPHI. Cada paquete encapsula datos e código e contén entidades públicas e privadas. Isto permítelle reducir a interface do paquete ao mínimo.

A operación de implementación por método de delegación foi tomada da linguaxe DELPHI.

Recompilación

Non é sen razón que hai unha broma: Go desenvolveuse mentres se compilaba un programa en C. Un dos puntos fortes da linguaxe é a súa compilación ultrarrápida. A idea foi tomada da lingua DELPHI. Cada paquete Go corresponde a un módulo DELPHI. Estes paquetes recompílanse só cando é realmente necesario. Polo tanto, despois da seguinte edición, non precisa compilar todo o programa, senón recompilar só os paquetes modificados e os paquetes que dependen destes paquetes modificados (e aínda así, só se as interfaces dos paquetes cambiaron).

Construcións de alto nivel

A linguaxe contén moitas construcións de alto nivel diferentes que de ningún xeito están relacionadas con linguaxes de baixo nivel como C.

  • Cordas
  • Táboas hash
  • Rebandas
  • A dixitación de pato está tomada de linguas como RUBY (que, por desgraza, moitos non entenden nin usan ao máximo).

Xestión da memoria

A xestión da memoria en xeral merece un artigo aparte. Se en linguaxes como C++, o control déixase completamente ao desenvolvedor, entón en linguaxes posteriores como DELPHI utilizouse un modelo de conta de referencia. Con este enfoque, non se permitían referencias cíclicas, xa que se formaron clústeres orfos, entón Go ten incorporada a detección de tales clústeres (como C#). Ademais, o colector de lixo é máis eficiente que a maioría das implementacións coñecidas actualmente e xa se pode usar para moitas tarefas en tempo real. A propia linguaxe recoñece situacións nas que se pode asignar un valor para almacenar unha variable na pila. Isto reduce a carga do xestor de memoria e aumenta a velocidade do programa.

Concurrencia e concorrencia

O paralelismo e a competitividade da lingua están máis aló dos eloxios. Ningún idioma de baixo nivel pode competir remotamente con Go. Para ser xustos, paga a pena sinalar que o modelo non foi inventado polos autores da lingua, senón que simplemente foi tomado prestado da boa e antiga linguaxe ADA. A linguaxe é capaz de procesar millóns de conexións paralelas usando todas as CPU, ao tempo que ten unha orde de magnitude de problemas menos complexos con bloqueos e condicións de carreira típicas do código multiproceso.

Beneficios adicionais

Se é rendible, todos se volverán desinteresados.

A linguaxe tamén nos proporciona unha serie de vantaxes indubidables:

  • Un único ficheiro executable despois de construír o proxecto simplifica moito o despregamento de aplicacións.
  • A dixitación estática e a inferencia de tipos poden reducir significativamente o número de erros no teu código, mesmo sen escribir probas. Coñezo algúns programadores que prescinden de escribir probas para nada e a calidade do seu código non se ve afectado significativamente.
  • Compilación cruzada moi sinxela e excelente portabilidade da biblioteca estándar, o que simplifica moito o desenvolvemento de aplicacións multiplataforma.
  • As expresións regulares RE2 son seguras para fíos e teñen tempos de execución previsibles.
  • Unha poderosa biblioteca estándar que permite que a maioría dos proxectos prescinda de frameworks de terceiros.
  • A linguaxe é o suficientemente poderosa como para centrarse no problema en lugar de como resolvelo, pero o suficientemente baixo nivel para que o problema se poida resolver de forma eficiente.
  • O sistema Go eco xa contén ferramentas desenvolvidas fóra da caixa para todas as ocasións: probas, documentación, xestión de paquetes, potentes linters, xeración de código, detector de condicións de carreira, etc.
  • A versión 1.11 de Go introduciu a xestión de dependencias semánticas integrada, construída sobre o popular hospedaxe VCS. Todas as ferramentas que compoñen o ecosistema Go utilizan estes servizos para descargar, construír e instalar código dun só golpe. E iso é xenial. Coa chegada da versión 1.11, o problema coa versión de paquetes tamén foi completamente resolto.
  • Debido a que a idea central da linguaxe é reducir a maxia, a linguaxe incentiva aos desenvolvedores a xestionar os erros de forma explícita. E isto é correcto, porque se non, simplemente esquecerase por completo do tratamento de erros. Outra cousa é que a maioría dos desenvolvedores ignoran deliberadamente o manexo de erros, preferindo en lugar de procesalos simplemente reenviar o erro cara arriba.
  • A linguaxe non implementa a metodoloxía clásica de POO, xa que na súa forma pura non hai virtualidade en Go. Non obstante, isto non é un problema cando se usan interfaces. A ausencia de POO reduce significativamente a barreira de entrada para os principiantes.

Sinxeleza para o beneficio da comunidade

É doado de complicar, difícil de simplificar.

Go foi deseñado para ser sinxelo e ten éxito nese obxectivo. Foi escrito para programadores intelixentes que entenden os beneficios do traballo en equipo e están cansos da infinita variabilidade das linguaxes de nivel empresarial. Tendo un conxunto relativamente pequeno de estruturas sintácticas no seu arsenal, practicamente non está suxeito a cambios co paso do tempo, polo que os desenvolvedores teñen moito tempo libre para o desenvolvemento, e non para estudar sen parar as innovacións lingüísticas.

As empresas tamén reciben unha serie de vantaxes: unha baixa barreira de entrada permítelles atopar rapidamente un especialista e a inmutabilidade da linguaxe permítelles utilizar o mesmo código incluso despois de 10 anos.

Conclusión

O gran tamaño do cerebro nunca fixo que ningún elefante fose gañador do Premio Nobel.

Para aqueles programadores cuxo ego persoal prima sobre o espírito de equipo, así como para os teóricos amantes dos retos académicos e da "superación persoal" infinita, a linguaxe é realmente mala, xa que é unha linguaxe artesanal de propósito xeral que non permite conseguir pracer estético do resultado do teu traballo e mostrarte profesional diante dos compañeiros (sempre que medimos a intelixencia por estes criterios, e non polo coeficiente intelectual). Como todo na vida, é unha cuestión de prioridades persoais. Como todas as innovacións que valen a pena, a lingua xa percorreu un longo camiño dende a negación universal ata a aceptación masiva. A linguaxe é enxeñosa na súa sinxeleza e, como sabes, todo o enxeñoso é sinxelo!

Fonte: www.habr.com

Engadir un comentario