Por que Go é ruim para programadores pouco inteligentes

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

Por que Go é ruim para programadores pouco inteligentes

Nos últimos dois anos, tenho usado Go para implementar um servidor RADIUS especializado com um sistema de faturamento desenvolvido. Ao longo do caminho, estou aprendendo as complexidades da própria linguagem. Os programas em si são muito simples e não são o objetivo deste artigo, mas a própria experiência de uso do Go merece algumas palavras em sua defesa. Go está se tornando uma linguagem cada vez mais popular para códigos sérios e escaláveis. A linguagem foi criada pelo Google, onde é ativamente utilizada. Resumindo, honestamente acho que o design da linguagem Go é ruim para programadores pouco inteligentes.

Projetado para programadores fracos?

Os fracos falam de problemas. O forte fala sobre ideias e sonhos...

Go é muito fácil de aprender, tão fácil que você pode ler o código praticamente sem nenhum treinamento. Esse recurso da linguagem é utilizado em muitas empresas globais quando o código é lido em conjunto com especialistas não essenciais (gerentes, clientes, etc.). Isso é muito conveniente para metodologias como Design Driven Development.
Mesmo os programadores novatos começam a produzir códigos bastante decentes depois de uma ou duas semanas. O livro que estudei é “Go Programming” (de Mark Summerfield). O livro é muito bom, aborda muitas nuances da linguagem. Depois de linguagens desnecessariamente complicadas como Java, PHP, a falta de mágica é revigorante. Mas, mais cedo ou mais tarde, muitos programadores limitados têm a ideia de usar métodos antigos em um novo campo. Isso é realmente necessário?

Rob Pike (o principal ideólogo da linguagem) criou a linguagem Go como uma linguagem industrial fácil de entender e eficaz de usar. A linguagem foi projetada para máxima produtividade em grandes equipes e não há dúvidas disso. Muitos programadores novatos reclamam que faltam muitos recursos. Esse desejo por simplicidade foi uma decisão consciente dos designers da linguagem e, para entender completamente por que ela era necessária, devemos entender a motivação dos desenvolvedores e o que eles estavam tentando alcançar no Go.

Então, por que foi tão simples? Aqui estão algumas citações de Rob Pike:

O ponto chave aqui é que nossos programadores não são pesquisadores. Eles são, via de regra, bem jovens, chegam até nós depois de estudar, talvez tenham estudado Java, ou C/C++, ou Python. Eles não conseguem entender uma boa linguagem, mas ao mesmo tempo queremos que eles criem um bom software. É por isso que a linguagem deve ser fácil de entender e aprender.

Ele deveria ser familiar, grosso modo semelhante a C. Os programadores que trabalham no Google iniciam suas carreiras cedo e estão familiarizados principalmente com linguagens procedurais, em particular a família C. A exigência de produtividade rápida em uma nova linguagem de programação significa que a linguagem não deve ser muito radical.

Palavras sábias, não são?

Artefatos de Simplicidade

A simplicidade é uma condição necessária para a beleza. Lev Tolstoi.

Manter a simplicidade é um dos objetivos mais importantes em qualquer design. Como você sabe, um projeto perfeito não é aquele onde não há nada a acrescentar, mas sim aquele do qual não há nada a retirar. Muitas pessoas acreditam que para resolver (ou mesmo expressar) problemas complexos é necessária uma ferramenta complexa. No entanto, não é. Vejamos a linguagem PERL, por exemplo. Os ideólogos da linguagem acreditavam que um programador deveria ter pelo menos três maneiras diferentes de resolver um problema. Os ideólogos da linguagem Go seguiram um caminho diferente; decidiram que um caminho, mas realmente bom, era suficiente para atingir o objetivo. Esta abordagem tem uma base séria: a única maneira é mais fácil de aprender e mais difícil de esquecer.

Muitos migrantes queixam-se de que a linguagem não contém abstrações elegantes. Sim, é verdade, mas esta é uma das principais vantagens da linguagem. A linguagem contém um mínimo de magia - portanto, nenhum conhecimento profundo é necessário para ler o programa. Quanto à verbosidade do código, isso não é um problema. Um programa Golang bem escrito é lido verticalmente, com pouca ou nenhuma estrutura. Além disso, a velocidade de leitura de um programa é pelo menos uma ordem de grandeza maior que a velocidade de sua escrita. Se você considerar que todo o código tem formatação uniforme (feita usando o comando gofmt integrado), ler algumas linhas extras não é um problema.

Não muito expressivo

A arte não tolera quando a sua liberdade é restringida. A precisão não é responsabilidade dele.

Devido ao desejo de simplicidade, Go carece de construções que em outras linguagens são percebidas como algo natural por pessoas acostumadas a elas. No início pode ser um pouco inconveniente, mas depois você percebe que o programa é muito mais fácil e inequívoco de ler.

Por exemplo, um utilitário de console que lê stdin ou um arquivo de argumentos de linha de comando ficaria assim:

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 solução para o mesmo problema em D, embora pareça um pouco mais curta, não é mais 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);
    }
}

Inferno de copiar

O homem carrega o inferno dentro de si. Martinho Lutero.

Os iniciantes reclamam constantemente do Go em termos de falta de genéricos. Para resolver esse problema, a maioria deles usa cópia direta de código. Por exemplo, uma função para somar uma lista de números inteiros, esses aspirantes a profissionais acreditam que a funcionalidade não pode ser implementada de outra forma a não ser simplesmente copiar e colar para cada tipo de dados.

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 linguagem possui meios suficientes para implementar tais construções. Por exemplo, programação genérica seria adequada.

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, embora nosso código tenha sido um pouco mais longo que o caso anterior, ele se generalizou. Portanto, não será difícil implementar todas as operações aritméticas.

Muitos dirão que um programa em D parece significativamente mais curto e estarão certos.

import std.stdio;
import std.algorithm;

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

No entanto, é apenas mais curto, mas não mais correto, uma vez que a implementação de D ignora completamente o problema do tratamento de erros.

Na vida real, à medida que a complexidade da lógica aumenta, a lacuna diminui rapidamente. A lacuna diminui ainda mais rapidamente quando você precisa executar uma ação que não pode ser executada usando operadores de linguagem padrão.

Em termos de manutenibilidade, extensibilidade e legibilidade, na minha opinião, a linguagem Go vence, embora perca em verbosidade.

A programação generalizada, em alguns casos, nos traz benefícios inegáveis. Isso é claramente ilustrado pelo pacote sort. Portanto, para classificar qualquer lista, precisamos apenas 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 você pegar qualquer projeto de código aberto e executar o comando grep “interface{}” -R, verá com que frequência interfaces confusas são usadas. Camaradas de mente fechada dirão imediatamente que tudo isso se deve à falta de genéricos. No entanto, nem sempre é esse o caso. Tomemos o DELPHI como exemplo. Apesar da presença desses mesmos genéricos, contém um tipo VARIANT especial para operações com tipos de dados arbitrários. A linguagem Go faz o mesmo.

De uma arma em pardais

E a camisa de força deve caber no tamanho da loucura. Stanislav Lec.

Muitos fãs radicais podem afirmar que Go tem outro mecanismo para criar genéricos - reflexão. E eles estarão certos... mas apenas em casos raros.

Rob Pike nos avisa:

Esta é uma ferramenta poderosa que deve ser usada com cautela. Deve ser evitado, a menos que seja estritamente necessário.

A Wikipedia nos diz o seguinte:

A reflexão refere-se ao processo durante o qual um programa pode monitorar e modificar sua própria estrutura e comportamento durante a execução. O paradigma de programação subjacente à reflexão é chamado de programação reflexiva. Este é um tipo de metaprogramação.

Porém, como você sabe, você tem que pagar por tudo. Neste caso é:

  • dificuldade em escrever programas
  • velocidade de execução do programa

Portanto, a reflexão deve ser usada com cautela, como uma arma de grande calibre. O uso impensado da reflexão leva a programas ilegíveis, erros constantes e baixa velocidade. Ideal para um programador esnobe poder exibir seu código diante de outros colegas mais pragmáticos e modestos.

Bagagem cultural de Xi? Não, de vários idiomas!

Junto com a fortuna, as dívidas também ficam com os herdeiros.

Apesar de muitos acreditarem que a linguagem é inteiramente baseada na herança C, este não é o caso. A linguagem incorpora muitos aspectos das melhores linguagens de programação.

sintaxe

Em primeiro lugar, a sintaxe das estruturas gramaticais é baseada na sintaxe da linguagem C. Contudo, a linguagem DELPHI também teve influência significativa. Assim, vemos que os parênteses redundantes, que reduzem bastante a legibilidade do programa, foram totalmente removidos. A linguagem também contém o operador “:=” inerente à linguagem DELPHI. O conceito de pacotes é emprestado de linguagens como ADA. A declaração de entidades não utilizadas é emprestada da linguagem PROLOG.

Semântica

Os pacotes foram baseados na semântica da linguagem DELPHI. Cada pacote encapsula dados e código e contém entidades públicas e privadas. Isso permite reduzir ao mínimo a interface do pacote.

A operação de implementação pelo método de delegação foi emprestada da linguagem DELPHI.

Compilação

Não é à toa que existe uma piada: Go foi desenvolvido enquanto um programa C estava sendo compilado. Um dos pontos fortes da linguagem é sua compilação ultrarrápida. A ideia foi emprestada da linguagem DELPHI. Cada pacote Go corresponde a um módulo DELPHI. Esses pacotes são recompilados somente quando realmente necessário. Portanto, após a próxima edição, você não precisa compilar o programa inteiro, mas sim recompilar apenas os pacotes alterados e os pacotes que dependem desses pacotes alterados (e mesmo assim, somente se as interfaces dos pacotes tiverem sido alteradas).

Construções de alto nível

A linguagem contém muitas construções diferentes de alto nível que não estão de forma alguma relacionadas a linguagens de baixo nível como C.

  • Cordas
  • Tabelas hash
  • Fatias
  • A digitação Duck é emprestada de linguagens como RUBY (que, infelizmente, muitos não entendem ou não utilizam todo o seu potencial).

Gerenciamento de memória

O gerenciamento de memória geralmente merece um artigo separado. Se em linguagens como C++ o controle é deixado completamente para o desenvolvedor, então em linguagens posteriores como DELPHI, um modelo de contagem de referência foi usado. Com esta abordagem, referências cíclicas não eram permitidas, uma vez que clusters órfãos foram formados, então Go possui detecção integrada de tais clusters (como C#). Além disso, o coletor de lixo é mais eficiente do que a maioria das implementações conhecidas atualmente e já pode ser usado para muitas tarefas em tempo real. A própria linguagem reconhece situações em que um valor para armazenar uma variável pode ser alocado na pilha. Isso reduz a carga do gerenciador de memória e aumenta a velocidade do programa.

Simultaneidade e simultaneidade

O paralelismo e a competitividade da linguagem estão além dos elogios. Nenhuma linguagem de baixo nível pode competir, mesmo remotamente, com Go. Para ser justo, é importante notar que o modelo não foi inventado pelos autores da linguagem, mas simplesmente emprestado da boa e velha linguagem ADA. A linguagem é capaz de processar milhões de conexões paralelas usando todas as CPUs, ao mesmo tempo em que apresenta problemas muito menos complexos com deadlocks e condições de corrida que são típicos de código multithread.

Benefícios adicionais

Se for lucrativo, todos se tornarão altruístas.

A linguagem também nos proporciona uma série de benefícios indiscutíveis:

  • Um único arquivo executável após a construção do projeto simplifica bastante a implantação de aplicativos.
  • A digitação estática e a inferência de tipos podem reduzir significativamente o número de erros no seu código, mesmo sem escrever testes. Conheço alguns programadores que ficam sem escrever testes e a qualidade de seu código não é significativamente prejudicada.
  • Compilação cruzada muito simples e excelente portabilidade da biblioteca padrão, o que simplifica muito o desenvolvimento de aplicativos multiplataforma.
  • Expressões regulares RE2 são thread-safe e têm tempos de execução previsíveis.
  • Uma biblioteca padrão poderosa que permite que a maioria dos projetos funcione sem estruturas de terceiros.
  • A linguagem é poderosa o suficiente para focar no problema e não em como resolvê-lo, mas de baixo nível o suficiente para que o problema possa ser resolvido de forma eficiente.
  • O sistema Go eco já contém ferramentas desenvolvidas prontas para uso para todas as ocasiões: testes, documentação, gerenciamento de pacotes, linters poderosos, geração de código, detector de condições de corrida, etc.
  • A versão 1.11 do Go introduziu gerenciamento de dependência semântica integrado, baseado na popular hospedagem VCS. Todas as ferramentas que compõem o ecossistema Go usam esses serviços para baixar, construir e instalar código deles de uma só vez. E isso é ótimo. Com a chegada da versão 1.11, o problema de versionamento de pacotes também foi completamente resolvido.
  • Como a ideia central da linguagem é reduzir a magia, a linguagem incentiva os desenvolvedores a lidar explicitamente com erros. E isso está correto, porque caso contrário, ele simplesmente esquecerá completamente o tratamento de erros. Outra coisa é que a maioria dos desenvolvedores ignora deliberadamente o tratamento de erros, preferindo, em vez de processá-los, simplesmente encaminhar o erro para cima.
  • A linguagem não implementa a metodologia OOP clássica, pois em sua forma pura não há virtualidade em Go. No entanto, isso não é um problema ao usar interfaces. A ausência de OOP reduz significativamente a barreira de entrada para iniciantes.

Simplicidade para benefício da comunidade

É fácil complicar, difícil simplificar.

Go foi projetado para ser simples e atinge esse objetivo. Ele foi escrito para programadores inteligentes que entendem os benefícios do trabalho em equipe e estão cansados ​​da infinita variabilidade das linguagens de nível empresarial. Tendo em seu arsenal um conjunto relativamente pequeno de estruturas sintáticas, ele praticamente não está sujeito a mudanças ao longo do tempo, de modo que os desenvolvedores têm muito tempo liberado para o desenvolvimento, e não para o estudo incessante das inovações linguísticas.

As empresas também recebem uma série de vantagens: uma baixa barreira de entrada permite-lhes encontrar rapidamente um especialista e a imutabilidade da linguagem permite-lhes utilizar o mesmo código mesmo após 10 anos.

Conclusão

O grande tamanho do cérebro nunca fez de nenhum elefante um ganhador do Prêmio Nobel.

Para aqueles programadores cujo ego pessoal tem precedência sobre o espírito de equipe, bem como para teóricos que amam os desafios acadêmicos e o "autoaperfeiçoamento" sem fim, a linguagem é muito ruim, pois é uma linguagem artesanal de uso geral que não permite que você obtenha prazer estético com o resultado do seu trabalho e mostrar-se profissional diante dos colegas (desde que medimos a inteligência por esses critérios, e não pelo QI). Como tudo na vida, é uma questão de prioridades pessoais. Como todas as inovações que valem a pena, a linguagem já percorreu um longo caminho desde a negação universal até à aceitação em massa. A linguagem é engenhosa na sua simplicidade e, como você sabe, tudo que é engenhoso é simples!

Fonte: habr.com

Adicionar um comentário