Tecnologia / Artigos / Linguagens de Programação /
Go é um castigo merecido

Cléber

![sleepy-dog.jpg](/files/166) *Photo by [Amy Humphries](https://unsplash.com/@amyjoyhumphries) on [Unsplash](https://unsplash.com/s/photos/sleepy)* --- Ou seria "golang"? golang golang golang golang golang golang golang golang golang golang golang golang golang golang Pronto. Talvez assim esse artigo seja encontrado quando alguém buscar algo sobre "Go" no Google. --- # Briguinhas Durante uma década inteira ficamos contentes com nossas pequenas brigas entre diversos feudos. Python (ou, "*os programadores que usavam linguagem Python*") fazia graça com Java e sua sintaxe ridícula, Java gabava-se de ser "*Enterprise*" enquanto temia a vinda da .Net que, por sua vez, tentava ser um pouco de tudo, mas focava mesmo em C#, que por sua vez era desprezada pelo C++ por ser "muito pesada" ou "pouco portável" enquanto o velho C assistia a tudo, sentindo-se superior, e dava risada enquanto seus usuários dançavam num salão recém-encerado carregando navalhas. Mas no fundo todo mundo meio que se gostava e a aparência de guerra escondia uma espécie de fraternidade, um sentimento de que as coisas eram do jeito que eram e assim permaneceriam e, por isso, cada um poderia gozar de paz e tranquilidade em seu próprio nicho. Se você queria desenvolver rápido, que usasse Python. Se precisava de desempenho máximo, que usasse C. Se queria desempenho, mas ainda mantendo um mínimo de abstrações úteis, poderia usar C++. E se queria jogar dinheiro no lixo ou precisava integrar com aplicações "corporate", sempre podia usar Java. E a vida ia seguindo, com um ou outro *hipster* aqui e ali apregoando o uso de Haskell ou Elixir e lá no canto do salão, encostado na parede segurando um copo de refrigerante sozinho estava alguém que defendia o uso da linguagem D. E debaixo da mesa, onde ninguém via nada, alguns programadores *assembly*. E foi no meio dessa mamata que, despercebida, chegou A Praga, trazida para punir nossa falta de vigilância, nossa loucura e até nossa soberba. **Chegou a linguagem Go**. ``` SE não chegou { log.Fatalln("Não chegou!") } ``` ## Necessidade desprezada Era evidente que esse dia chegaria. Que os problemas que as principais linguagens simplesmente ignoravam ou tentavam remendar com *fita crepe* seriam finalmente resolvidos por alguma entidade que tivesse não somente a capacidade de pagar para isso mas também para criar *hype* suficiente para alavancar o uso da nova solução por meio de uma legião da *fanboys* ensandecidos. Python tem uma sintaxe magnífica. E *features* magníficas. Mas e o tempo de execução? Uma hora ou outra o fato de os Lambda demorarem 10x mais para cada requisição do que uma linguagem compilada nativamente iria pesar na balança. Uma hora ou outra as chatices da distribuição de código com bibliotecas em repositórios privados interdependentes seria percebida por alguém. C é rápido, mas como faz para instalar um pacote? Não tem pacote? Pessoal ainda faz "*vendoring*"? Sério? ``` SE C tem sistema de distribuição de módulos/pacotes { log.Println("Legal. Gostaria de conhecer") return err, nil } ``` E Java é... "*Enterprise*". Mas por mais que seja vendida como algo magnífico, por que os jovens talentos geralmente passam longe? ``` SE jovens talentos não passam longe { log.Println("Ops, desculpe. É a minha impressão") return err, nil } ``` Em resumo, havia sim uma necessidade grande entre as empresas de uma linguagem simples o bastante para ser aprendida rapidamente, compilada nativamente e de maneira rápida, sem grandes dificuldades de gestão de memória e que tivesse excelente desempenho para todos poderem economizar em tempo de execução nessa nossa era de computação-paga-por-milissegundo. Mas nós desprezamos isso e, soberbos, achamos que tudo iria ficar do mesmo jeito para sempre. Que engano triste! Que sonolência da percepção! Que arrogância! ``` SE não era arrogância { return nil, err } ``` # O grande problema de Go A linguagem Go é uma mal terrível por dois motivos de igual importância, que são: * Go é uma atrocidade em termos de sintaxe e ferramental; * Go cumpre todos os requisitos necessários para desenvolvimento de software profissional. # Go é uma atrocidade ## Em termos de sintaxe Go é uma linguagem horrenda que sabe vender-se bem. Não é? Go é uma linguagem complicada, que consegue unir a presença de *garbage collection* com **ponteiros**. Que limpa memória para você, mas te obriga a ficar pensando em alocação de memória o tempo todo. Que te dá "ferramentas simples", como *go routines* e canais, mas que no fim do dia não passam de corda para você se enforcar. ### Import com sintaxe de include As linguagens que implementam **módulos** geralmente lidam com *importação*. Importação de módulos é um processo geralmente muito diferente da *inclusão* de arquivos. E como os módulos geralmente tem nomes seguindo algum padrão razoável, as linguagens mais comuns acabam usando uma sintaxe assim: ```python import os ``` Em geral, quando aplicável, a importação implica também em execução do módulo em questão. Se você quiser dar outro nome para o módulo importado, em Python, pode fazê-lo assim: ```python import my_class as base_class ``` Ou seja: `importe ᐸnome originalᐳ como ᐸnome localᐳ `. Já a *inclusão* é um processo mais literal: o `#include` do pré-processador de C, por exemplo, copia todo o conteúdo do arquivo a ser incluído dentro do arquivo em que essa diretiva é chamada. ```c #include "vendor/gui/window.h" ``` E numa espécie de meio-termo existe o conceito do `require`, que geralmente tem sintaxe mais similar ao `include`, mas comportamento muitas vezes similar ao `import`, ou seja, implicando execução. ```javascript User = require("../modules/user.js"); ``` Em Nodejs, o `require` traz nomes que foram explicitamente expostos pelo módulo. Já em Ruby, o `require` tem um processo muito mais parecido com uma inclusão, o que explica, inclusive, por que Ruby permite definir módulos dentro dos arquivos (ao contrário de Python, em que os módulos são definidos pela forma como os arquivos estão dispostos no sistema de arquivos). ```ruby require 'amqp_client' ``` Mas aí vem Go e manda os consensos pras cucuias: ```go import ( "fmt" "os" log "github.com/sirupsen/logrus" ) ``` Que. Parada. Grotesca. Há N maneiras de tornar a sintaxe mais clara e até agradável, mas não, os criadores de Go são mais espertos do que nós. Eles inventaram um jeito próprio ou baseado em alguma referência obscura que por mais "inovador" que possa parecer, **não acrescenta nada** às nossas vidas -- pelo contrário, torna a leitura muito mais confusa. Não é porque você se dá a liberdade de inovar que precisa, necessariamente, fazer uma bela cagada. Veja **Zig**, por exemplo, que também é uma linguagem muito jovem mas trata as importações de maneira muito mais inteligente: ```zig const std = @import("std"); ``` Tudo aqui é "sintaxe normal" da linguagem, exceto o `@`, que deve ligar um alerta na cabeça do desenvolvedor, que pensa "*hum, esse símbolo deve significar que essa instrução "import" tem algo de especial*". E tem! Instruções que começam com `@` são a forma como o programador fala com o compilador. **Nim**, por sua vez, mesmo sendo uma linguagem compilada, implementou um sistema magnífico de módulos (é magnífico **mesmo**) com a sintaxe já bem conhecida: ```nim import threads ``` Mas Go não somente implementa esse jeito bizarro, com parêntesis, sem vírgulas e usando algo que aparenta ser *strings*, como também permite uma forma simplificada: ```go import "fmt" import "log" ``` Curiosamente, o compilador não reclama se você importar todos os seus pacotes assim... ### Weird Walrus Uma das coisas mais esquisitas de Go é o *walrus operator* (`:=`). É esquisito, em primeiro lugar, porque gera dois jeitos completamente diferentes de se declarar variáveis. ```go var name string // 1 surname := "Silva" // 2 ``` Considero **C** a sintaxe mais elegante quanto à declaração e inicialização de variáveis. Veja como é feito: ```c char *name; // 1 char *surname = "Silva"; // 2 ``` (Lembre-se: não existe um tipo "string" em C. Strings são cadeias de bytes e a sintaxe `"string"`, com as aspas, é mero açúcar sintático.) Mas não para por aí. O *walrus* serve, em Go, para "declarar e inicializar", certo? Tanto que não se pode declarar duas vezes a mesma variável: ```go a := 1 a := 2 // Aqui o compilador acusará um erro. ``` Entretanto, como uma costura bem mal-feita para o problema do tratamento imediato e constante de erros, os criadores de Go resolveram que tudo bem uma variável ser exposta ao *walrus* mais de uma vez, contanto que esteja no meio de um grupo em que **alguma** variável nova é declarada. Veja: ```go err := nil result, err := function() ``` Viu? `err` passou pelo *walrus* duas vezes e quanto a isso o compilador não falará nada. Ou seja: o *walrus operator* em Go serve para "*declarar e inicializar **alguma** variável no grupo à esquerda*". O que torna a sintaxe levemente confusa. Não tanto, admito, mas leva a outro grande mal de Go, que é... ### Constante reescrita das coisas Você chama N funções dentro de um mesmo bloco de código? Pois se prepare, se um dia quiser chamar mais uma antes de todas as outras, terá que meter a mão no código antigo, muito provavelmente. Explico: lembram que o *walrus operator* é usado para "*declarar e inicializar*"? Pois é. E praticamente ninguém declara o maldito do `err` separadamente. O que causa o seguinte efeito. Imagine que você tenha um código assim: ```go _, err := function_1() if err != nil { // Tratar o erro } z, err := function_2() if err != nil { // Tratar o erro } ``` Não somente é **irritante** ter que tratar todos os erros imediatamente (e uma decisão de design patética, sinceramente), mas se você quiser adicionar uma chamada a outra função antes de `function_1`, **terá que reescrever sua chamada**: ```go x, err := function_0() // err é declarado pelo walrus, aqui if err != nil { // Tratar o erro } _, err := function_1() // Agora ESSE walrus ficou errado! Tem que reescrever if err != nil { // Tratar o erro } z, err := function_2(y) if err != nil { // Tratar o erro } ``` E quando você está lidando em um problema razoavelmente complexo, ou quando terminou de implementar e está melhorando o *design* da sua função, trocando algumas chamadas de lugar para tornar tudo mais claro, é **de arrancar os cabelos** ter que ficar tratando esses erros estúpidos de *walrus/not-walrus* a toda hora. A "solução"? Declarar `err` antecipadamente. ``` var err error x, err := function_0() if err != nil { // Tratar o erro } _, err = function_1() if err != nil { // Tratar o erro } z, err := function_2(y) if err != nil { // Tratar o erro } ``` Além da horripilância que é ter que dizer `var err error` (parece um cachorro rosnando) no topo da função, chamando a atenção para um aspecto secundário da vida da mesma (vital, mas secundário), **ainda assim** você tem que prestar muita atenção no uso ou não do *walrus operator*, porque se quiser descartar algum valor, terá que usar atribuição simples. (Ou seja: não tem como ficar bom.) Como a linguagem poderia corrigir isso? Simples: implementando **apenas** atribuição simples. Mas precisaria ser implementado na linguagem, porque tentar simular isso é praticamente impossível, hoje, já que o desenvolvedor geralmente quer aproveitar **a mesma** variável `err` ao invés de piorar ainda mais sua situação criando um rastro de `err1`, `err2`, `err3`... ```go var a, b = 10, 20 // Funciona! var a, err = f(b) // Não compila, porque `err` não foi declarada. a, err := f(b) // Não compila, porque `a` já foi declarada. (E nem queremos usar o walrus!) // Solução medonha: var err Error a, err = f(b) // Compila // Mas, agora, somos obrigados a sempre declarar previamente cada variável usada: var c int c, err = f(b) ``` "*Mas é desse jeito porque os erros não podem ser ignorados. É uma feature da linguagem!*", alguém dirá. Ao que respondo: não confunda funcionalidade com sintaxe. **Zig** também te proíbe de ignorar erros, mas resolveu a questão toda simplesmente com o uso de **tipos**: ```zig const std = @import("std"); pub fn main() void { f = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| switch (err) {/* handle all errors here */}; } ``` Cada função pode retornar tanto um tipo "comum" quanto algum tipo derivado do tipo base de erro. A linguagem é capaz de detectar um retorno do tipo erro. E para evitar que o desenvolvedor fique repetindo código a toda hora, especialmente nos casos em que quer que os erros "*popem para o chamador*", há o seguinte atalho: ```zig const std = @import("std"); pub fn main() void { f = try std.fs.cwd().openFile("does_not_exist/foo.txt", .{}); } ``` (`try` do Zig **não tem nada a ver com exceções ou com a famosa dupla try/catch** de outras linguagens, que fique bem claro desde já.) O que esse `try` faz é justamente isso: ele tenta. Se não der certo (ou seja: se o retorno da função é um valor cujo tipo representa um erro), retorna imediatamente esse mesmo erro. Isso é um atalho para o seguinte: ```zig const std = @import("std"); pub fn main() void { f = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| return err; } ``` E eis aí um mesmo problema sendo resolvido de forma patética e magnífica por duas linguagens diferentes, e ambas sem recorrer ao lançamento de exceções. ### Tratar erros precocemente é ridículo Ao ponto de ter se criado o costume de criar a função `DoSomething() (int, error)` e sua irmã `MustDoSomething() int`, ou seja, a primeira retorna um valor em `err` caso tenha ocorrido algum erro, enquanto a outra já tem um comportamento embutido que é matar o programa caso algo de errado aconteça. Há décadas sabemos que, a não ser que seja o caso de uma saída imediata e forçada, tratar erros precocemente é um *anti-pattern*, uma coisa ruim que não deve ser feita. Mas Go, na cabeça genial de quem criou a linguagem, considera que o mundo é o oposto do que é e que a maioria dos casos de erro significa saída imediata, sem tratamento algum por parte da pilha de chamadores. E o problema é que, no mundo real, teu código ficará repleto dessa construção particular: ```go y, err := f(x) if err != nil { return nil, err } ``` Go é tão chato que não permite nem que você use um atalho mais simpático, como isso: ```go y, err := f(x) if err != nil return nil, err // Cuéum! Não pode. ``` Tampouco como isso: ```go y, err := f(x) if err return nil, err // Cuéum! Menos ainda! ``` Ou seja: se acontecer de você chamar N funções, haverá no mínimo **3N linhas** servindo simplesmente para retornar o código de erro para o chamador. Chama 5 funções? Serão 15 linhas gastas apenas para **propagar** os erros. Mas, espere!, você pode economizar 1 linha, sabia? Sim. E o preço a pagar é simplesmente **abrir mão de um mínimo de legibilidade**. ```go if y, err := f(x); err != nil { return nil, err } ``` Pronto. Agora não dá pra entender mais nada. :-) Mas fica pior! Lembra que Zig trata os erros elegantemente com o uso de **tipos**? Pois é. Em Go você não é obrigado a usar somente o `error` do sistema, já que este é uma *interface*. Excelente, não? Exceto que o tratamento de acordo com o tipo de erro é assim: ```go if err := dec.Decode(&val); err != nil { if serr, ok := err.(*json.SyntaxError); ok { line, col := findLine(f, serr.Offset) return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err) } return err } ``` Entendeu alguma coisa? Pois é. Um horror. Acontece que *type assertion* em Go tem essa sintaxe bizarra: ```go extracted_value, ok := analyzed_value.(T) ``` Ou seja: `ok` será verdadeiro se o tipo de `analyzed_value` for igual a `T`. O problema disso? **A sintaxe é horrenda**. E por que eu digo que é horrenda? Bom, experimente ler o seguinte código em voz alta, um conceito depois do outro, sem pular nenhum: ```go if value, ok := err.(JsonSyntaxError); ok { log.Fatalf("Error: JSON is in incorrect format: %s\n", value.details) } ``` *Se valor, ok, recebe err ponto JsonSyntaxError entre parêntesis e ok é verdadeiro, então...* **Horrível!** Zig permite criar estruturas muito mais legíveis: ```zig if (parseU64(str, 10)) |number| { doSomethingWithNumber(number); } else |err| switch (err) { error.Overflow =ᐳ { // handle overflow... }, ... } ``` Novamente, experimente ler em voz alta: *se ParseU64 de str, 10 retornar number, faça algo com number, se não, com o erro retornado, caso este erro seja Overflow, faça...*. Muito melhor, não? Mas você não é obrigado a fazer isso e pode usar a sintaxe que mencionei anteriormente, ainda: ```zig const number = parseU64(str, 10) catch |err| switch (err) { error.Overflow =ᐳ { // handle overflow... } } ``` O primeiro jeito pode ser melhor caso um erro seja um acontecimento esperado, fazendo parte do fluxo considerado normal da função. Já o segundo faz com que o fato de ter acontecido um erro pareça algo fora do comum e inesperado. E você tem a liberdade de escolher a forma que achar mais adequada. ## Em termos de ferramental As bibliotecas tratam certos casos incomuns com um desleixo bizarro e, no geral, "corretude" é uma palavra estranha à biblioteca padrão da linguagem. Confira o post [I want off Mr. Golang's Wild Ride](https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-ride) quando tiver um tempo. Ademais, os *modules* são uma história complicada, a implementação de versionamento gerou uns casos bizarros, `GOPATH` é um conceito que ainda complica a vida de muita gente e simplesmente compilar um pacote é um trabalho bem mais complicado do que poderia ser, especialmente tratando-se de uma linguagem tão nova. (Mas não tratarei sobre cada caso desses aqui. Talvez em outro artigo.) # Go cumpre os requisitos para desenvolvimento profissional E, apesar de todos os problemas de sintaxe e de implementação, absolutamente não há um candidato à altura de Go para essa tarefa de desenvolver software que chamo de *commodity*. Software commodity é aquele serviço de autenticação que pega usuário e senha, bate o olho no banco, faz um hash, compara strings e retorna um token ou um código de erro adequado. Inovação? Zero. E é implementado todo santo dia em N empresas mundo afora. Para **esse** tipo de trabalho, Go é a linguagem perfeita, **infelizmente**. É perfeita porque funciona, é perfeita porque é, sim, fácil de aprender (você entende todos os motivos para odiar a linguagem em dois ou três dias, apenas) e é perfeita porque **tem *hype***, o que faz com que haja um ecossistema vivo o bastante para não obrigar as empresas a implementar *in-house* a maioria das coisas que precisa para fazer software commodity. E é aí que entra a minha crítica a "programadores esnobes" como eu, que valorizam a clareza magnífica da sintaxe de Python ou Nim ou mesmo Zig mas não mexeram um dedo para implementar em alguma linguagem que seja uma potencial candidata a arrancar Go do trono (como Nim ou Zig) **aquela** biblioteca que serve todo santo dia de desculpa para não escolher Nim ou Zig como nova "linguagem principal" da empresa e ir com Go que "*é mais garantido*". Eu encontrei **uma** biblioteca para lidar com GraphQL em Nim, que é baseada na `libgraphql` escrita em C! Entende quantos pontos isso tira de uma linguagem? A compatibilidade com C é incrível se você **usa** alguma biblioteca em C, mas ter que instalar a *lib* em Nim **e também** a biblioteca em C, provavelmente usando "*vendoring*" (porque não achei no repositório da minha distro) é de matar. O post que citei reclama que as bibliotecas Go não tratam direito o caso de haver um arquivo chamado, veja só, `\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98` (um conjuntão de bytes meio aleatórios). E a reclamação é válida caso você esteja criando algum software que trabalhe primariamente com o sistema de arquivos e precise lidar com casos assim "complexos" (não é nem um pouco complexo, mas certamente é pouco habitual, certo?), mas **na maioria dos sofwares commodity isso absolutamente não faz diferença**. E é por isso que, no fim do dia, "Go compila rápido" ganha de "Rust é correto". E de goleada! É óbvio que a realidade é completamente distinta quando se trata de **software de valor** (ao contrário do sofware commodity). Veja o [Ray tracing in Nim](https://nim-lang.org/blog/2020/06/30/ray-tracing-in-nim.html), por exemplo, para entender como as *features* da linguagem **ajudam muitão** o desenvolvedor a chegar num excelente resultado. Mas esse é o tipo de caso em que os fatores levados em consideração são absolutamente outros. No dia-a-dia de 90% das empresas que postam vagas nos *boards* de emprego Brasil afora **o que se faz majoritariamente é sofware commodity** que compõe algum sistema que, ele sim, entrega algum valor real. Mas não os componentes! Os componentes são simples e pouco importantes individualmente, o que faz com que Go seja a escolha perfeita para elas. É claro que isso não se aplica a todas as empresas. Tampouco a "todas as grandes empresas". A **Basecamp** está lá, firme e forte com seu belo monolito escrito em Rails. **Lá** é importante que a linguagem seja rica em funcionalidades, clara e correta. Mas lá até mesmo as exigências para contratação são muito diferentes. **A Basecamp pode rejeitar candidatos** e ainda assim haverá uma fila de gente querendo trabalhar lá. **A Josécamp EIRELI não pode se dar a esse luxo** e tem muita dificuldade de encontrar **interessados**! E lá, se a linguagem absolutamente não conseguir tratar um arquivo com nome `\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98`, o que se faz é simplesmente **enviar um e-mail** para o cliente dizendo "*por favor, use caracteres normais ou o sistema não vai funcionar*" e, pronto!, o problema está resolvido e podemos gastar energia em algo mais lucrativo. # E a culpa é nossa Eu, particularmente, torço para que uma linguagem decente destrone Go nesses casos de uso específicos. E não acho que será Nim ou Zig que conseguirão (apesar de achar que Zig, quando tiver um ecossistema bem rico, chegará muito, muito próximo disso). Mas, enquanto isso não acontece, é responsabilidade nossa, dos desenvolvedores que conseguem ver a confusão miserável que é a sintaxe e a implementação de Go, **enriquecer os ecossistemas das linguagens que apreciamos** e que podem ser magníficas, rápidas, belas, portáveis e repletas de funcionalidades interessantíssimas, mas carecem de boas bibliotecas para coisas triviais como HTTP, REST, GraphQL, ORMs, templating, comunicação com inúmeros protocolos, etc. E nós temos plena capacidade de fazer isso, certo? # Prosseguindo Nos próximos episódios dessa série pretendo **implementar uma biblioteca de GraphQL em diversas linguagens**, entre elas Go (não acho as implementações atuais muito boas, particularmente), Zig, Nim e outras, em parte para explorar essa capacidade de preencher a mesma lacuna que Go preenche, em parte para demonstrar o quão interessantes e poderosas certas linguagens são (por puro prazer, sem nenhum objetivo realmente prático). E aos poucos quero ir construindo a ideia do que seria uma linguagem de programação "ideal" (o conceito é amplo, mas vamos definindo-o, também). Até!

Curti

38 visitantes curtiram esse Item.

Anterior: Artigos / XFCE: quase como o Lada | Próximo: O princípio: gramáticas