4 Críticas à linguagem Go
Publicados: 2022-03-11Go (também conhecido como Golang) é um dos idiomas em que as pessoas estão mais interessadas. Em abril de 2018, ocupava o 19º lugar no índice TIOBE. Mais e mais pessoas estão mudando de PHP, Node.js e outras linguagens para Go e usando-o em produção. Muitos softwares legais (como Kubernetes, Docker e Heroku CLI) são escritos usando Go.
Então, qual é a chave para o sucesso do Go? Há muitas coisas dentro da linguagem que a tornam muito legal. Mas uma das principais coisas que tornaram o Go tão popular é sua simplicidade, como apontado por um de seus criadores, Rob Pike.
A simplicidade é legal: você não precisa aprender muitas palavras-chave. Isso torna o aprendizado de idiomas muito fácil e rápido. No entanto, por outro lado, às vezes os desenvolvedores não têm alguns recursos que eles têm em outras linguagens e, portanto, precisam codificar soluções alternativas ou escrever mais código a longo prazo. Infelizmente, o Go carece de muitos recursos por design e, às vezes, é realmente irritante.
Golang foi feito para tornar o desenvolvimento mais rápido, mas em muitas situações, você está escrevendo mais código do que escreveria usando outras linguagens de programação. Descreverei alguns desses casos nas minhas críticas à linguagem Go abaixo.
As 4 críticas da linguagem Go
1. Falta de Sobrecarga de Função e Valores Padrão para Argumentos
Vou postar um exemplo de código real aqui. Quando eu estava trabalhando na ligação Selenium de Golang, eu precisava escrever uma função que tivesse três parâmetros. Dois deles eram opcionais. Veja como ficou após a implementação:
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error { // the actual implementation was here } func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error { return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } func (wd *remoteWD) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) }
Eu tive que implementar três funções diferentes porque eu não podia simplesmente sobrecarregar a função ou passar os valores padrão—Go não fornece isso por design. Imagine o que aconteceria se eu acidentalmente ligasse para a pessoa errada? Aqui está um exemplo:
Eu tenho que admitir que às vezes a sobrecarga de funções pode resultar em um código confuso. Por outro lado, por causa disso, os programadores precisam escrever mais código.
Como pode ser melhorado?
Aqui está o mesmo (bem, quase o mesmo) exemplo em JavaScript:
function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) { // actual implementation here }
Como você pode ver, parece muito mais claro.
Eu também gosto da abordagem Elixir nisso. Aqui está como ficaria no Elixir (eu sei que eu poderia usar valores padrão, como no exemplo acima - estou apenas mostrando como isso pode ser feito):
defmodule Waiter do @default_interval 1 @default_timeout 10 def wait(condition, timeout, interval) do // implementation here end def wait(condition, timeout), do: wait(condition, timeout, @default_interval) def wait(condition), do: wait(condition, @default_timeout, @default_interval) end Waiter.wait("condition", 2, 20) Waiter.wait("condition", 2) Waiter.wait("condition")
2. Falta de genéricos
Este é sem dúvida o recurso que os usuários do Go mais estão pedindo.
Imagine que você deseja escrever uma função map, onde está passando o array de inteiros e a função, que será aplicada a todos os seus elementos. Parece fácil, certo?
Vamos fazer isso para números inteiros:
package main import "fmt" func mapArray(arr []int, callback func (int) (int)) []int { newArray := make([]int, len(arr)) for index, value := range arr { newArray[index] = callback(value) } return newArray; } func main() { square := func(x int) int { return x * x } fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25] }
Parece bom, certo?
Bem, imagine que você também precisa fazer isso para strings. Você precisará escrever outra implementação, que é exatamente a mesma, exceto pela assinatura. Essa função precisará de um nome diferente, pois Golang não suporta sobrecarga de função. Como resultado, você terá várias funções semelhantes com nomes diferentes e será algo assim:
func mapArrayOfInts(arr []int, callback func (int) (int)) []int { // implementation } func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 { // implementation } func mapArrayOfStrings(arr []string, callback func (string) (string)) []string { // implementation }
Isso definitivamente vai contra o princípio DRY (Don't Repeat Yourself), que afirma que você precisa escrever o mínimo de código copiar/colar possível e, em vez disso, movê-lo para funções e reutilizá-los.
Outra abordagem seria usar implementações únicas com interface{}
como parâmetro, mas isso pode resultar em um erro de tempo de execução porque a verificação de tipo de tempo de execução é mais propensa a erros. E também será mais lento, então não há uma maneira simples de implementar essas funções como uma só.
Como pode ser melhorado?
Existem muitas linguagens boas que incluem suporte a genéricos. Por exemplo, aqui está o mesmo código em Rust (usei vec
em vez de array
para simplificar):
fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> { let mut new_vec = vec![]; for value in vec { new_vec.push(callback(value)); } return new_vec; } fn square (val:i32) -> i32 { return val * val; } fn underscorify(val:String) -> String { return format!("_{}_", val); } fn main() { let int_vec = vec![1, 2, 3, 4, 5]; println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25] let string_vec = vec![ "hello".to_string(), "this".to_string(), "is".to_string(), "a".to_string(), "vec".to_string() ]; println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"] }
Observe que há uma única implementação da função map
e ela pode ser usada para qualquer tipo que você precisar, mesmo os personalizados.
3. Gerenciamento de Dependências
Qualquer pessoa que tenha experiência em Go pode dizer que o gerenciamento de dependências é muito difícil. As ferramentas Go permitem que os usuários instalem diferentes bibliotecas executando go get <library repo>
. O problema aqui é o gerenciamento de versões. Se o mantenedor da biblioteca fizer algumas alterações incompatíveis com versões anteriores e as enviar para o GitHub, qualquer pessoa que tentar usar seu programa depois disso receberá um erro, porque go get
não faz nada além de git clone
seu repositório em uma pasta de biblioteca. Além disso, se a biblioteca não estiver instalada, o programa não compilará por causa disso.

Você pode fazer um pouco melhor usando o Dep para gerenciar dependências (https://github.com/golang/dep), mas o problema aqui é que você está armazenando todas as suas dependências em seu repositório (o que não é bom, porque seu repositório conter não apenas seu código, mas milhares e milhares de linhas de código de dependência), ou apenas armazenar a lista de pacotes (mas, novamente, se o mantenedor da dependência fizer uma alteração incompatível com versões anteriores, tudo irá travar).
Como pode ser melhorado?
Acho que o exemplo perfeito aqui é Node.js (e JavaScript em geral, suponho) e NPM. O NPM é um repositório de pacotes. Ele armazena as diferentes versões de pacotes, portanto, se você precisar de uma versão específica de um pacote, não há problema - você pode obtê-lo de lá. Além disso, uma das coisas em qualquer aplicativo Node.js/JavaScript é o arquivo package.json
. Aqui, todas as dependências e suas versões são listadas, então você pode instalá-las todas (e obter as versões que definitivamente funcionam com seu código) com npm install
.
Além disso, os grandes exemplos de gerenciamento de pacotes são RubyGems/Bundler (para pacotes Ruby) e Crates.io/Cargo (para bibliotecas Rust).
4. Tratamento de erros
O tratamento de erros em Go é simples. Em Go, basicamente você pode retornar vários valores de funções, e a função pode retornar um erro. Algo assim:
err, value := someFunction(); if err != nil { // handle it somehow }
Agora imagine que você precisa escrever uma função que faz três ações que retornam um erro. Vai parecer algo assim:
func doSomething() (err, int) { err, value1 := someFunction(); if err != nil { return err, nil } err, value2 := someFunction2(value1); if err != nil { return err, nil } err, value3 := someFunction3(value2); if err != nil { return err, nil } return value3; }
Há muito código repetível aqui, o que não é bom. E com funções grandes, pode ser ainda pior! Você provavelmente precisará de uma tecla no teclado para isso:
Como pode ser melhorado?
Eu gosto da abordagem do JavaScript sobre isso. A função pode lançar um erro e você pode pegá-lo. Considere o exemplo:
function doStuff() { const value1 = someFunction(); const value2 = someFunction2(value1); const value3 = someFunction3(value2); return value3; } try { const value = doStuff(); // do something with it } catch (err) { // handle the error }
É muito mais claro e não contém código repetível para tratamento de erros.
As coisas boas em Go
Embora o Go tenha muitas falhas de design, ele também possui alguns recursos muito legais.
1. Goroutinas
A programação assíncrona ficou muito simples em Go. Embora a programação multithreading seja geralmente difícil em outras linguagens, gerar um novo thread e executar uma função nele para que não bloqueie o thread atual é realmente simples:
func doSomeCalculations() { // do some CPU intensive/long running tasks } func main() { go doSomeCalculations(); // This will run in another thread; }
2. Ferramentas que acompanham o Go
Enquanto em outras linguagens de programação você precisa instalar diferentes bibliotecas/ferramentas para diferentes tarefas (como testes, formatação de código estático etc.), existem muitas ferramentas legais que já estão incluídas no Go por padrão, como:
-
gofmt
- Uma ferramenta para análise de código estático. Comparando com JavaScript, onde você precisa instalar uma dependência adicional, comoeslint
oujshint
, aqui está incluída por padrão. E o programa nem compilará se você não escrever código no estilo Go (sem usar variáveis declaradas, importar pacotes não utilizados, etc.). -
go test
- Uma estrutura de teste. Novamente, comparando com JavaScript, você precisa instalar dependências adicionais para teste (Jest, Mocha, AVA, etc.). Aqui, está incluído por padrão. E permite que você faça muitas coisas legais por padrão, como benchmarking, conversão de código na documentação para testes, etc. -
godoc
- Uma ferramenta de documentação. É bom tê-lo incluído nas ferramentas padrão. - O próprio compilador. É incrivelmente rápido, comparado a outras linguagens compiladas!
3. Adiar
Eu acho que esse é um dos recursos mais bonitos da linguagem. Imagine que você precise escrever uma função que abra três arquivos. E se algo falhar, você precisará fechar os arquivos abertos existentes. Se houver muitas construções assim, parecerá uma bagunça. Considere este exemplo de pseudocódigo:
function openManyFiles() { let file1, file2, file3; try { file1 = open('path-to-file1'); } catch (err) { return; } try { file2 = open('path-to-file2'); } catch (err) { // we need to close first file, remember? close(file1); return; } try { file3 = open('path-to-file3'); } catch (err) { // and now we need to close both first and second file close(file1); close(file2); return; } // do some stuff with files // closing files after successfully processing them close(file1); close(file2); close(file3); return; }
Parece complicado. É aí que entra o defer
de Go:
package main import ( "fmt" ) func openFiles() { // Pretending we're opening files fmt.Printf("Opening file 1\n"); defer fmt.Printf("Closing file 1\n"); fmt.Printf("Opening file 2\n"); defer fmt.Printf("Closing file 2\n"); fmt.Printf("Opening file 3\n"); // Pretend we've got an error on file opening // In real products, an error will be returned here. return; } func main() { openFiles() /* Prints: Opening file 1 Opening file 2 Opening file 3 Closing file 2 Closing file 1 */ }
Como você vê, se recebermos um erro ao abrir o arquivo número três, outros arquivos serão fechados automaticamente, pois as instruções defer
são executadas antes do retorno na ordem inversa. Além disso, é bom ter o arquivo abrindo e fechando no mesmo local, em vez de partes diferentes de uma função.
Conclusão
Não mencionei todas as coisas boas e ruins de Go, apenas as que considero as melhores e as piores.
Go é realmente uma das linguagens de programação interessantes em uso atual, e realmente tem potencial. Ele nos fornece ferramentas e recursos muito legais. No entanto, há muitas coisas que podem ser melhoradas lá.
Se nós, como desenvolvedores Go, implementarmos essas mudanças, isso beneficiará muito nossa comunidade, pois tornará a programação com Go muito mais agradável.
Enquanto isso, se você está tentando melhorar seus testes com Go, tente Testar seu aplicativo Go: Comece do jeito certo pelo colega Toptaler Gabriel Aszalos.