Go ainda continua não sendo bom
(blog.habets.se)- Várias decisões de design da linguagem Go foram tomadas de forma desnecessária ou ignorando experiências já consolidadas
- O problema de gerenciamento do escopo das variáveis de erro dificulta a legibilidade do código e a identificação de bugs
- Em vários pontos, como a dualidade do
nil, uso de memória e portabilidade do código, aparecem designs pouco intuitivos e desalinhados com a realidade - As limitações da instrução
defere a forma como a biblioteca padrão lida com exceções dificultam garantir segurança contra exceções - Problemas acumulados, como gestão de memória e tratamento deficiente de UTF-8, estão afetando negativamente a qualidade de codebases em Go no longo prazo
Crítica de longo prazo à linguagem Go
- Como já expus em posts anteriores (Why Go is not my favourite language, Go programs are not portable), venho apontando vários problemas da linguagem Go há mais de 10 anos
- Em especial, passam a ser cada vez mais decepcionantes as decisões de design desnecessárias que ignoram boas práticas já conhecidas
A falta de intuição no escopo das variáveis de erro
- A sintaxe do Go amplia desnecessariamente o escopo da variável de erro (
err), aumentando a possibilidade de falhas- No código de exemplo, a variável
errpermanece viva por toda a função e é reutilizada, o que prejudica a legibilidade e a manutenibilidade do código - Desenvolvedores experientes acabam enfrentando mal-entendidos e perda de tempo ao investigar bugs por causa desse problema de escopo
- A sintaxe não permite limitar adequadamente essas variáveis a um escopo mais local
- No código de exemplo, a variável
Duas formas de nil
- Em Go, existe a confusão de que
nilse comporta de forma diferente em tiposinterfacee tipos ponteiro- Como no exemplo abaixo, mesmo que
s(ponteiro) ei(interface) recebamnil,s==ipode ser avaliado de forma diferente, mostrando um comportamento inconsistente - Isso reproduz um problema que normalmente se quer evitar ao lidar com
null, deixando traços de um design feito sem reflexão suficiente
- Como no exemplo abaixo, mesmo que
Limites na portabilidade do código
- O uso de comentários para compilação condicional é claramente ineficiente em termos de manutenção e portabilidade
- Quem já teve experiência real construindo software portável sabe que essa abordagem é trabalhosa e propensa a erros
- Experiências históricas acumuladas (portabilidade de código, casos práticos) estão sendo ignoradas
- Para mais detalhes, veja Go programs are not portable
A falta de clareza sobre a propriedade de append
- A relação de propriedade entre a função
appende slices não é clara, o que dificulta prever o comportamento do código- Pelo exemplo, quando uma slice recebe
appenddentro da funçãofoo, é difícil saber de antemão qual será o impacto real sobre o valor original - Os “quirks” da linguagem que é preciso decorar só aumentam, favorecendo erros
- Pelo exemplo, quando uma slice recebe
Falhas no design da instrução defer
- Ela não oferece suporte claro à liberação de recursos como no princípio RAII (Resource Acquisition Is Initialization)
- Em comparação com estruturas de gerenciamento de recursos em Java e Python, em Go não fica claro quais recursos devem ser liberados com
defer - Como no exemplo com arquivos, até o problema de double-close precisa ser tratado manualmente, e a ordem e a forma corretas de liberação não ficam claras
- Em comparação com estruturas de gerenciamento de recursos em Java e Python, em Go não fica claro quais recursos devem ser liberados com
Tratamento de exceções na biblioteca padrão
- Go não adota uma estrutura com exceções explícitas (
exception), mas situações excepcionais comopanicainda assim acontecem- Em alguns casos,
panicnem encerra completamente o programa e acaba sendo engolido - Existem padrões na biblioteca padrão (
fmt.Print, servidor HTTP etc.) que ignoram exceções, o que torna impossível garantir segurança real contra exceções - No fim, escrever código seguro contra exceções continua sendo necessário, mas não é possível usar exceções diretamente
- Em alguns casos,
Tratamento de UTF-8 e strings
- Mesmo que se coloque dados binários arbitrários no tipo
string, Go continua funcionando sem validação especial- É possível enfrentar casos em que nomes de arquivo criados antes da codificação UTF-8 simplesmente desaparecem silenciosamente
- Isso pode causar perda de dados importantes em backups e reflete uma abordagem simplificada que não considera situações reais de trabalho
Limites da gestão de memória
- É difícil ter controle direto sobre o uso de RAM, e a confiabilidade do GC (garbage collector) também tem limites
- O consumo de memória em Go aumenta e, no longo prazo, isso se transforma em custos e problemas de desempenho
- Em ambientes com múltiplas instâncias e contêineres, problemas reais de custo e escalabilidade acabam surgindo
Conclusão: havia caminhos melhores
- Mesmo já existindo designs de linguagem comprovadamente eficazes, Go optou por ignorá-los em muitos aspectos
- Diferentemente dos problemas das primeiras propostas de Java, quando Go foi lançado já existiam abordagens melhores
Materiais de referência
- Uber: Data race patterns in Go
- FasterThanLime: Lies we tell ourselves to keep using Golang
- FasterThanLime: I want off Mr Golang’s wild ride
1 comentários
Comentários no Hacker News
Uso Go desde a época pré-1.0 em praticamente todos os meus empregos de tempo integral. É simples para os colegas aprenderem o básico e, no geral, funciona de forma estável. Quase nunca há com o que se preocupar ao atualizar para a versão mais recente de Go, e a maioria dos recursos úteis já vem incluída por padrão. A velocidade de compilação é um atrativo. Concorrência é um pouco complicada, mas, se você investir tempo, ela acaba se tornando uma boa forma de expressar fluxo de dados. O sistema de tipos é conveniente na maior parte do tempo, embora às vezes seja verboso. No geral, é uma ferramenta confiável. Ainda assim, acabo concordando com várias das críticas mencionadas no artigo. Está claro que Go tem partes em que desenvolvedores da velha guarda ficaram apegados demais a princípios e deixaram de lado conveniências práticas. Claro, essa é só minha impressão, e também acho que, se tivessem resolvido todos os defeitos, talvez a linguagem tivesse ficado pior do que está hoje. Também quero mencionar que, nos últimos anos, sinto um clima mais aberto a corrigir esquisitices. Houve uma época em que eu jamais imaginaria a chegada de generics ou iteradores customizados. As críticas sobre RAM e portabilidade me parecem mais reclamações pessoais. Seria bom melhorar, mas é extremamente raro que o GC cause problemas realmente graves na maioria dos programas, e debugar também não é difícil. E Go suporta praticamente todas as plataformas importantes. Ainda assim, continuo achando desconfortável a forma como trata erros e
nil. Sinto falta com frequência de sintaxes comoResult[Ok, Err]eOptional[T]Eu vejo o contrário: Go não foi teimoso com princípios, e sim obcecado por conveniências que resolvem rápido os problemas mais imediatos. Em vez de analisar o problema pela raiz e resolvê-lo corretamente, parece que abandonaram a mentalidade de “Not Invented Here” e foram improvisando soluções na hora. A API de sistema de arquivos de Go é um exemplo clássico. Se precisavam de uma função para abrir arquivo, simplesmente fizeram algo como
func Open(name string) (*File, error)e pronto. Mas e se o nome do arquivo não for UTF-8? Como esse problema não apareceu por 5 anos, simplesmente ignoraramMuitas vezes sinto que os princípios de design de Go se concentram demais no objetivo de “facilitar a implementação do compilador e tornar a compilação rápida”. É uma estrutura focada mais no compilador/na compilação do que na ergonomia do desenvolvedor
Depois de 20 anos, foi com Go que voltei a usar de verdade uma linguagem compilada num emprego novo. Pode ser gosto pessoal, mas sinceramente a experiência chegou a me causar antipatia. Não há valores padrão para argumentos, não gosto do tratamento de erros, e não existe stack trace decente em produção. A sintaxe de orientação a objetos também é estranha, porque obriga a anexar uma referência esquisita a cada função. Ponteiros também são um incômodo. No fim, parece um retorno a técnicas antigas de C/C++. A sensação é a mesma da programação que eu fazia na faculdade por volta de 1999
No aspecto de concorrência, Go é, na minha experiência, o único sistema em que a própria linguagem lida de forma natural com paralelismo em CPUs multicore. Graças ao formalismo de goroutine/channel no estilo CSP, a lógica de concorrência é expressa de forma intuitiva. Python é um tormento por causa do GIL e das bibliotecas
asyncobscuras. C, C++, Java etc. exigem bibliotecas extras fora da linguagem, então não é fácil raciocinar sobre concorrência no nível da própria linguagem. Por isso acho que Go combina perfeitamente com servidores HTTP e serviços em geral. Na minha experiência, não há alternativa à alturaDo ponto de vista de ergonomia para desenvolvedores, ou seja, padronização e consistência, achei praticamente perfeito. Mesmo em vários codebases de microsserviços, não preciso me preocupar com estilos diferentes nem com discussões sobre formatação. Ainda assim, ao escolher o “jeito padrão” de Go, parece que insistiram demais em um estilo antigo. Hoje em dia os desenvolvedores esperam mais métodos funcionais como
map/filter, mas Go só oferece laços com risco de erro de índice. Também não tem um sistema de tipos inteligente no nível do TypeScript. O tratamento de erros é incômodo. Entendo a preocupação de que adicionar esses recursos aumentaria os “usos criativos, porém ruins”, mas também percebo como é difícil convencer a geração do JS a adotar GoJá faz mais de 5 anos que me dedico a um grande projeto em Golang, e quando você precisa construir componentes com uso mínimo de memória, acaba esbarrando com frequência nas partes frouxas de Go. O GC não limpa rápido o suficiente ou a fragmentação do heap fica séria (por Go não ter um coletor de lixo com compactação). Por isso tento evitar alocação a todo custo, mas isso facilita o surgimento de bugs. Debugar também é extremamente difícil. Mesmo gerando perfis de heap, só aparecem informações dos objetos sobreviventes; o lixo real acumulado e os detalhes da fragmentação não aparecem, então você acaba dependendo de suposições. Por exemplo, uma função X pode aparecer como tendo alocado apenas 1 KB no heap, mas, se for chamada repetidamente em um loop, pode gerar dezenas de MB de lixo. Então acabamos pré-alocando buffers estáticos para reutilização, mas isso complica a questão de ownership e cria armadilhas com coisas como
append. Às vezes também precisamos reimplementar a própria biblioteca padrão. Sei que nosso caso não é comum, mas dá mesmo a sensação de estar brigando com a linguagem, o que é frustranteNesses casos, talvez seja menos doloroso tirar a memória do heap. Claro, em uma linguagem com GC isso não é simples, mas, em vez de forçar um código muito no estilo C++/Rust dentro de Go, talvez seja melhor simplesmente trocar aquela parte por uma dessas linguagens
Acho que, nesse caso, a escolha de usar Go foi o verdadeiro problema. C/C++/Rust/Zig parecem muito mais adequadas
Há notícias de que o novo coletor de lixo "Green Tea" pode ajudar. Não é necessariamente focado em memória, mas é um algoritmo de marcação paralela que lida melhor com objetos vizinhos na memória. Dá para ver mais aqui
Houve um experimento com
arena, mas no momento ele está suspenso. Ainda assim, vale a pena acompanharIsso talvez não ajude muito, desculpa, mas olhando a situação atual eu realmente acho que a linguagem escolhida foi totalmente inadequada. Imagino até se vocês não estão usando Go à força por causa de alguma política oficial de linguagem dentro da empresa. É comum em grandes empresas aprovarem produção só para linguagens amplamente adotadas
Até hoje não entendo por que o
deferde Go funciona apenas no escopo da função e não no escopo léxico. Descobri isso da pior forma: processando arquivos dentro de um loop, a lista ficou grande, e como odefernão fechava os handles até o fim da função, o programa acabou travando. Os desenvolvedores Go ao meu redor disseram para eu envolver o corpo do loop numa função anônima. Fora alguns detalhes menores, Go me parece agradável de usar, com sintaxe eficiente e até ajudando a evitar uma cultura inútil de “ostentação” no código. Já participei de uma grande reescrita de um projeto em C# para Go e, mesmo com um décimo dos recursos, o código ficou menor. Em vez de forçar alocação via GC, a linguagem incentiva defaults de bom desempenho, e os recursos embutidos de geração de código para coisas como serialização são convenientes. Diferentemente da sintaxe do C#, que tenta substituir tudo pela linguagem, em Go o clima é mais de usar SQL como SQL e gRPC via especificação protobufÀs vezes você quer
deferno escopo léxico, e às vezes no escopo da função. Por exemplo, se quiser abrir vários arquivos em um loop e mantê-los todos abertos até o fim da função, o escopo de função é necessário. Hoje o padrão é escopo de função, mas, quando você precisa de escopo léxico, basta envolver com umafunc. Se só existisse escopo léxico e você precisasse de escopo de função, não fica claro como fariaAs vantagens são reduzir um nível de indentação sem precisar de função de encapsulamento, o fato de o comportamento estar associado à call stack ou ao stack unwinding, e também que isso parece natural vindo do estilo
goto failem C. Claro, é meio incômodo ter que encapsular em outra função quando se usadeferem loopsJá usei tanto linguagens com escopo de bloco quanto com
deferem nível de função, e às vezes fico desejando poder usar odeferde escopo de função até dentro de condicionaisNão me parece que exista algum motivo especialmente profundo, e fico pensando se isso realmente importa tanto
Em C# também dá para trabalhar com SQL ou com especificações protobuf. A diferença é só que existem outras opções também
Go tem muitos defeitos, mas, dentro da categoria de linguagens para servidor, não vejo outra tão equilibrada. É mais rápida que Node ou Python, e também acho o sistema de tipos melhor. Tem barreira de entrada menor que Rust, e a biblioteca padrão e as ferramentas são excelentes. Gosto da sintaxe simples e da forma como força um único jeito de fazer as coisas. O tratamento de erros é problemático, mas ainda prefiro isso a um
catchdo Node onde pode cair qualquer erro. Fico curioso se existe outra linguagem melhor que atenda a todos esses critérios. Não sou fanático por Go; na maior parte da carreira fiz backend em Node, mas recentemente venho experimentando GoNa verdade, acho que todas essas vantagens poderiam ser ditas igualmente sobre Java ou C#
Me incomoda chamar “Node” de linguagem de programação. Node é um runtime de JavaScript, e hoje em dia uma parte considerável dos projetos que rodam em Node é escrita em TypeScript. Ou seja, dizer “Node” não deixa claro qual é a linguagem usada. Se o parâmetro for TypeScript, eu até diria que ele é mais produtivo que o sistema de tipos de Go. Dá para fazer afirmação parecida também em comparação com Rust
A maioria das linguagens tem seus próprios incômodos. Go se destaca em performance, portabilidade e também em runtime/ecossistema. Por outro lado, tem desvantagens como ponteiros
nil,zero value, ausência de destrutores, ausência de macros etc. (a falta de macros em Go leva a um abuso de geração de código para contornar isso). Existem linguagens melhores (por exemplo, Rust), mas aí a complexidade cresce bastante em relação a Go. Isso acontece porque os criadores de Go colocaram simplicidade acima de tudoConsiderando a evolução recente do sistema de tipos do Python, eu diria que ele está muito à frente de Go. Só em structural typing, Python já me parece mais impressionante
Acho o sistema de tipos de Go bastante insuficiente
Já expandi um static site generator feito em Go. O código era muito claro e fácil de ler, mas a linguagem tinha limitações que tornavam a expansão difícil. Até mudanças simples exigiam mexer de forma complicada em várias partes do código. É difícil trabalhar com diferentes níveis de encapsulamento e abstração, e a abstração acaba sacrificada em nome da “simplicidade”. Abstração é a forma mais importante de criar código fácil de estender, e Go escolhe simplicidade em vez de extensibilidade. Em geral, programas em Go me passam a sensação de serem “simples, mas sem extensibilidade”. As pessoas insistem que Go é assim mesmo, mas, pela minha experiência, isso não me convence. Ainda assim, a “experiência de desenvolvimento” em si não foi ruim
As conversas sobre Go sempre me parecem estranhas. Quando você critica, a resposta costuma ser “a linguagem é assim mesmo”, como se fosse algo a ser aceito sem discussão. Dizem que simplicidade é um ponto forte, mas fico em dúvida se é realmente mais simples ter que escrever manualmente um loop só para extrair a lista de chaves de um map
Queria saber se dá mesmo para fazer esse tipo de crítica tão facilmente depois de usar Go só por pouco tempo. Eu lido desde 2015 com vários codebases grandes em Go, com milhões de linhas, e trabalhei em diversas equipes. A extensibilidade de Go não é particularmente pior que a de C, C# ou Java. Go tende fortemente a escolher clareza em vez de expressividade. Por isso, ele leva a menos camadas de abstração e a um estilo mais concreto e explícito. Mas não vejo isso como algo que impeça extensibilidade. Projetos modulares e extensíveis dependem de aprendizado e design dos desenvolvedores, não da linguagem em si. O código com que você lidou provavelmente estava mal projetado; isso não é um limite da linguagem Go
Usei Go por alguns anos, e embora dê para fazer coisas pequenas rapidamente, conforme a escala cresce você passa a sofrer com inúmeros pequenos incômodos. Depurar é especialmente um pesadelo: se existir um X não usado (o que sempre acontece quando você comenta uma parte durante o debug), o código simplesmente não compila. Também é chato lidar com formatação desnecessariamente rígida, nomes de arquivo especiais e muitos nomes de campos reservados. Há
panics escondidos na biblioteca padrão, e cópias inesperadas para o heap também são lentas e irritantes. A maior parte da “mágica” de Go parece efeito colateral de reaproveitar à força recursos já existentes, como nomes especiais de arquivo e distinção entre maiúsculas e minúsculas. Se fosse para expor algo como “public”, poderiam simplesmente ter usadopub, mas existe uma insistência estranha nisso. Hoje, como a IA melhorou muito, quando enfrento problemas de tipos ou de borrow checker em Rust, pergunto imediatamente para a IA e resolvo rápido, o que acaba sendo muito mais agradável. Não preciso mais perder tempo caçando documentação ou respostas no Stack Overflow como antigamenteAinda não mergulhei de verdade em Rust recentemente, mas quando mexi um pouco em dezembro passado fiquei surpreso com o quanto a IA se sai bem com Rust. Como a linguagem tem muita sintaxe detalhada e muita informação explícita de tipos, a IA acaba resolvendo até melhor que pessoas
Quando você reclama que o debug em Go gera erros de compilação, o pessoal de Go costuma repreender dizendo para “usar as ferramentas corretamente”. É um caso de princípios levados ao extremo, gerando desconforto
Já mencionei esse incômodo de debug a um dos criadores de Go, e nem ele pareceu entender o problema. Achei isso decepcionante, meio amador. Aliás, IA lida até pior com Go. Apesar de a linguagem ser relativamente simples, o ChatGPT dá suporte melhor para Java, C# e Python
Pessoalmente não gosto de Go e vejo várias falhas decisivas, mas é claro por que ele continua tão popular. Go é relativamente rápido e, graças às goroutines, permite escrever serviços de alta concorrência de forma simples, estável e confiável, sem precisar lidar diretamente com multithreading. Quando o Google lançou Go, quase não havia linguagens populares, estáticas e compiladas com essa proposta. Mesmo hoje, o concorrente mais próximo nessa posição é Java (agora com suporte a virtual threads). Linguagens com
async/awaitprometem algo parecido, mas na prática carregam muita complexidade (evitar bloqueios em tarefas assíncronas, function coloring etc.). Erlang é outra categoria. No fim, apesar de ter muitos defeitos, Go continua popular por causa das goroutines e do peso da marca GoogleAos poucos, a JVM está reduzindo a distância para Go. Com projetos como virtual threads, zgc, lilliput, Leyden e Valhalla, a plataforma vem evoluindo bastante. A mudança do Java 8 para o 25 é enorme. Parece que vai ficar ainda mais conveniente no futuro
A explicitude e a simplicidade de Go se encaixam muito bem em programação assistida por LLM. Até código antigo de Go 1.x continua funcionando sem problemas nas versões mais novas
Na prática, dentro do Google, usam Java com virtual threads com muito mais frequência do que Go
Fico curioso para saber qual você considera a “linguagem moderna” mais adequada para projetos novos
Gosto de Go desde antes do lançamento da 1.0, mas não concordo com a avaliação de que “ainda não conseguiram fazer direito”. Claro que há defeitos e frustrações, mas também acho que, quando os criadores deixam o projeto, fica difícil manter uma visão central e há o risco de a linguagem piorar. O fato de Go ter ficado posicionado só como “linguagem de servidor” também provavelmente vai empurrar gente para Rust, Python e outras. Houve uma época em que se zombava do Visual Basic, mas no fim as pessoas que precisavam dele usavam bem assim mesmo
As críticas a Go costumam não parecer tão graves quando você realmente examina os detalhes. A maioria é tecnicamente correta, mas trata de coisas pequenas. Em compensação, os problemas realmente sérios de design da linguagem são
zero value, ausência de construtores, tratamento ruim denull, mutabilidade por padrão, sistema de tipos não pensado para generics,intsem suporte a precisão arbitrária e slices com ownership ambíguo (issue relacionada 1, issue relacionada 2). A falta de sum types e de interpolação de strings também é uma desvantagemTalvez eu seja tendencioso a ponto de escrever um livro sobre Go, mas, como alguém que usa Go há mais de 10 anos, no começo achei a linguagem realmente revigorante. Tem menos boilerplate que Java, é fácil de aprender e tem desempenho aceitável. Não existe linguagem perfeita, e cada uso tem sua melhor escolha, mas, para trabalho típico de backend, é uma escolha da qual não se arrepende