- Go é uma opção para reduzir a complexidade excessiva do desenvolvimento backend, e suas principais vantagens são compilação rápida, distribuição em binário único e gerenciamento de dependências estável
- Em vez de abstrações complexas como decorators, metaclasses, macros, traits e monads, Go adota um design de linguagem simples centrado em structs, funções, interfaces, goroutines e channels
- Só com a biblioteca padrão e ferramentas básicas como
embed, html/template, net/http, database/sql, encoding/json, go test e pprof, já dá para lidar com apps web, banco de dados, testes, benchmarks e profiling
- Goroutines são unidades de execução stackful com custo de cerca de 2 KB, e permitem tratar concorrência e propagação de cancelamento de forma simples com channels,
sync.Mutex, race detector e context.Context
- O fluxo com
go mod init, go build, scp e systemctl restart defende uma implantação simples, centrada em um único binário Go e Postgres, em vez de node_modules, configurações complexas de Docker e Kubernetes, ou microservices em excesso
Por que escolher Go
- Go é uma opção para reduzir a complexidade excessiva do desenvolvimento backend, e suas principais vantagens são compilação rápida, distribuição em binário único e gerenciamento de dependências estável
- Assim como no frontend o HTML continuou sendo uma alternativa à complexidade excessiva, Go também existe há mais de 10 anos como uma opção para simplificar o backend
- Para um app CRUD com formulários simples ou algo em torno de 40 requisições por segundo, usar dezenas de pacotes Node, ferramentas de build em TypeScript, Kubernetes, um time de plataforma para Rails ou até uma reescrita em Rust é exagero
- O foco do Go não está em “abstrações espertas”, mas em código fácil de ler, artefatos implantáveis e baixa carga operacional
Um design de linguagem deliberadamente entediante
- Go parece entediante por decisão de projeto, e não oferece abstrações complexas como decorators, metaclasses, macros, traits e monads
- Seus componentes centrais ficam basicamente limitados a structs, funções, interfaces, goroutines e channels
- A meta é ser simples o bastante para que alguém leia a especificação em pouco tempo e já consiga escrever código produtivo no mesmo dia
- Esse tédio vira uma vantagem em codebases de equipe
- Até um júnior que entrou no mês passado consegue ler o código escrito há 2 anos por um principal engineer
- Como o
gofmt impõe um único formato, há menos discussões sobre estilo de código
- A própria linguagem dificulta enfiar abstrações complexas demais na codebase
A biblioteca padrão faz o papel do framework
- Em Go, dá para criar um app web só com a biblioteca padrão, sem framework web separado
- Usando
embed, html/template e net/http, é possível montar um app que embute templates HTML no binário e os renderiza via handlers HTTP
package main
import (
"embed"
"html/template"
"net/http"
)
//go:embed templates/*.html
var files embed.FS
var tmpl = template.Must(template.ParseFS(files, "templates/*.html"))
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "index.html", map[string]string{
"Name": "asshole",
})
})
http.ListenAndServe(":8080", nil)
}
- Este exemplo é um app web funcional, e os templates HTML são compilados e incluídos no binário
- Sem
webpack, Vite, servidor de desenvolvimento ou um enorme node_modules, basta rodar go build e implantar um único arquivo
- Só com a biblioteca padrão e ferramentas básicas, já dá para cobrir as principais tarefas de backend
- Banco de dados:
database/sql
- JSON:
encoding/json
- Chamar outros serviços: cliente
net/http
- Execução concorrente: palavra-chave
go
- Testes:
go test
- Benchmarks:
go test -bench
- Profiling:
pprof
Uma biblioteca padrão profunda
-
io.Reader e io.Writer
io.Reader e io.Writer são interfaces com apenas um método cada, mas funcionam como base importante em todo o ecossistema Go
- Dá para encadear o corpo de uma resposta HTTP a um gzip writer e depois a um arquivo em disco com pouco código
- Como os principais pacotes compartilham essas duas interfaces, o mesmo padrão pode ser reaproveitado em vários lugares
-
context.Context
context.Context é a forma padrão de propagar cancelamento
- Se o usuário fecha a aba do navegador, o context da requisição é cancelado, e com isso também podem ser canceladas a query no banco e as chamadas HTTP subsequentes
- Para evitar vazamento de goroutines ou queries zumbis que consomem o pool de conexões, é preciso passar o context como primeiro argumento e respeitá-lo
-
Pacotes de encoding
encoding/json, encoding/xml, encoding/csv e encoding/binary já vêm na biblioteca padrão
- Como seguem um uso parecido, com pattern de tags em structs e decodificação com ponteiros, aprender um facilita usar os outros
Um modelo de concorrência que dói menos
- Goroutine não é uma thread de SO, e sim uma unidade de execução stackful multiplexada pelo runtime sobre threads do SO
- O custo inicial de uma goroutine é de cerca de 2 KB, e dá para criar 100 mil delas até em um notebook
- Channels funcionam como pipes tipados entre goroutines; uma ponta envia, a outra recebe, e o runtime cuida da sincronização
- Quando é preciso estado compartilhado, dá para usar
sync.Mutex, e o race detector ajuda a encontrar data races
- Até um fetcher HTTP paralelo pode ser escrito sem biblioteca separada, framework ou ritual de
async/await
results := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
results <- resp.Status
}(url)
}
for range urls {
fmt.Println(<-results)
}
Exemplo real de rota CRUD
- Até uma rota de estilo CRUD que lê posts do Postgres e renderiza HTML pode ser montada de forma simples o bastante para caber em uma tela
//go:embed templates/*.html
var tmplFS embed.FS
var tmpl = template.Must(template.ParseFS(tmplFS, "templates/*.html"))
type Post struct {
ID int
Title string
Body string
}
func postsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.QueryContext(r.Context(),
"SELECT id, title, body FROM posts ORDER BY id DESC LIMIT 50")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var posts []Post
for rows.Next() {
var p Post
if err := rows.Scan(&p.ID, &p.Title, &p.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
posts = append(posts, p)
}
tmpl.ExecuteTemplate(w, "posts.html", posts)
}
}
- Este exemplo mostra banco de dados, template e handler HTTP no mesmo lugar
- Como
r.Context() é passado para a query SQL, se a conexão for fechada a query também pode ser cancelada
- Sem ORM, contêiner de DI, camada de serviço ou um diretório
controllers/ cheio de classes-base abstratas, dá para ler de cima a baixo e entender o funcionamento
Gerenciamento de dependências que não destrói seu fim de semana
- Ao iniciar um módulo com
go mod init, as dependências ficam registradas em go.mod e go.sum
- O
go.sum é, na prática, um registro criptográfico do que foi realmente baixado, permitindo verificar quando chega uma dependência diferente da esperada
- Não existe a complexidade de diretório
node_modules, lockfile drift entre ambiente de desenvolvimento e CI, peer dependencies, optional dependencies, devDependencies ou peerDependenciesMeta
- Se precisar de build offline,
go mod vendor baixa as dependências para o diretório vendor/, e a toolchain usa isso automaticamente
- Também dá para empacotar o projeto inteiro e suas dependências em um único tarball, o que ajuda em operação e revisão de segurança
Ferramentas que vêm com o compilador
- As ferramentas básicas do Go já vêm prontas, sem plugins de terceiros nem arquivos extras de configuração
gofmt padroniza a formatação do código, reduzindo discussões e diffs inchados por mudanças de espaço em branco
go vet serve para detectar erros óbvios
go test executa os testes
go test -race roda os testes com o race detector para encontrar data races
go test -bench executa benchmarks
go test -cover mostra a cobertura de testes
go tool pprof permite obter flame graphs de CPU e uso de memória a partir de endpoints HTTP de um serviço em produção
Implantação termina com um comando de cópia
- O fluxo central de deploy em Go é: compilar o binário, copiá-lo para o servidor e executá-lo
GOOS=linux GOARCH=amd64 go build -o myapp ./cmd/myapp
scp myapp user@server:/usr/local/bin/
ssh user@server 'systemctl restart myapp'
- Esse fluxo permite implantar sem Dockerfile, multi-stage build, alertas de CVE em image base, manifestos de Kubernetes, Helm chart, ArgoCD, service mesh ou sidecars
- Com um binário estaticamente linkado de cerca de 12 MB e um arquivo unit do systemd com 20 linhas, já dá para fazer deploy em produção
- Se Docker for realmente necessário, colocar o binário Go em uma imagem
FROM scratch já é suficiente
Em contraste com frameworks
- Frameworks como Rails, Django, Express e Next.js trazem seus próprios custos: processo de deploy, ORM, admin, middleware, warnings do npm e mudanças em convenções de roteamento
- O binário Go é compilado e executa, com a vantagem de estabilidade para ainda poder rodar dali a 5 anos
- Em contraste com frameworks que podem ser descartados mais rápido ou cujos mantenedores podem sofrer burnout, o modelo simples de execução do Go se destaca
Um único binário Go em vez de microservices
- Microservices não deveriam ser a escolha padrão; em geral, é melhor começar escrevendo um monólito
- A configuração recomendada é um binário Go, um Postgres e, só se realmente necessário, um Redis
- Dá para servir HTML e API JSON na mesma porta e rodar tudo em um único VPS
- Como Go tem baixo custo por goroutine e forte suporte a concorrência, escala tranquilamente até 10 mil requisições por segundo
- Quando realmente surgir a necessidade de separar, é possível dividir a partir do monólito em Go movendo pacotes para repositórios separados
- Como as interfaces já existem, a própria linguagem acaba incentivando uma estrutura que considera separação naturalmente
Generics e tratamento de erros
if err != nil não é bug, é feature
- Isso faz com que você decida explicitamente o que fazer em cada ponto de falha, sem esconder erros
- Aninhar
try/catch não elimina erros; só pode escondê-los até o momento de uma falha em produção
- Generics chegaram no Go 1.18, e você pode usá-los quando precisar
Conclusão
- Nem sempre é preciso framework, microservices, reescrita em Rust ou um novo meta-framework JavaScript
- A recomendação é seguir um fluxo simples: rodar
go mod init, escrever main.go, embutir os templates, compilar e fazer deploy
- A escolha entediante é a escolha certa, e Go é essa opção
1 comentários
Opiniões no Lobste.rs
Não quero culpar o mensageiro, mas esse tipo de estilo de blog é cansativo e infantil. Pode até ter sido engraçado no começo, mas quanto mais se repete, mais irrita em progressão exponencial
Ainda assim, Go é bom. Troquei recentemente de um projeto em TypeScript para um em Go, e minha saúde mental e meu ânimo no trabalho estão melhorando rápido
Aceito a ideia de que
if err != nilnão é bug, é feature, mas ainda acho que esse é o maior defeito de Go. Se tivesse sum types, isso poderia ser muito mais ergonômico sem depender de asserções de tipo em tempo de execuçãoSe vai escrever assim, pelo menos que os insultos tenham um pouco de criatividade
Pelos outros comentários, isso parece ser uma opinião impopular, e não quero soar agressivo, mas eu realmente odeio Go
Go é uma linguagem com uma sintaxe até que razoável em cima de um runtime eficiente para concorrência, e cujo ecossistema foi empurrado pelo peso do Google. Fora isso, acho horrível
O maior problema é que ela parece ter sido projetada tentando ignorar deliberadamente décadas de pesquisa em design de linguagens de programação e até práticas de engenharia já consolidadas. Só foi ganhar genéricos décadas depois
Não estou dizendo que todo mundo precisa usar tipos dependentes, mas existe limite. Go quase não tem recursos de linguagem moderna para modelagem de dados, modelagem de invariantes e estruturação de código. Rust tem uma curva de aprendizado mais íngreme, mas nisso é muito melhor, e nem precisa de um sistema de tipos tão sofisticado quanto o de Rust para já ficar bom. Se a preocupação é tempo de compilação, ainda dá para ter um sistema de tipos sólido, rápido e expressivo com recursos simples, mas úteis
E
if err != nilme parece a pior forma possível de encher o código de ruído de tratamento de erro. Não entendo essa aversão do pessoal de Go a sum types. Nesse ponto, até exceções em Java são melhores. Na prática, como a linguagem não tem recursos melhores para lidar com erros, as pessoas acabam confundindo a pior gambiarra possível com uma featureSe o texto original não tivesse sido tão arrogante, eu nem teria escrito esse comentário. “Só use X” é uma coisa idiota de se dizer. Use a ferramenta que se encaixa no caso de uso, que seja confortável e produtiva. Se isso for Go, use Go; se não for, escolha outra coisa
Isso ajuda especialmente em organizações como o Google, onde há milhares de desenvolvedores e o tempo de permanência em um time ou empresa específica pode ser curto
Nesse contexto, especialmente para desenvolvedores inexperientes, a ausência de um sistema de tipos avançado acaba sendo uma vantagem em certa medida. Você quase não precisa pensar em tipos além de conceitos bem básicos como tipos primitivos ou structs. A linguagem te dá poucas ferramentas para modelar dados, mas em compensação permite escrever muito código sem pensar demais
Não acho isso muito bom para correção no nível da linguagem. Mas, em organizações grandes, você acaba dependendo muito mais da infraestrutura ao redor — análise de monorepo, CI/CD, testes canário, observabilidade — do que em organizações pequenas. Essa infraestrutura segura muito mais carga
Eu também gosto um pouco de Go por causa dessa baixa carga cognitiva. Escrevo código só de vez em quando em certos projetos e hoje não estou profundamente envolvido todos os dias em um projeto grande de longo prazo. Poder entrar numa codebase que não vejo há um mês e trabalhar nela por menos de uma hora é uma vantagem enorme. Mas acho que eu gostaria menos se fosse desenvolvedor em tempo integral de um projeto complexo
Acho que os desenvolvedores de Go focaram em acertar o básico, porque as linguagens anteriores e a comunidade de pesquisa em teoria de linguagens têm ignorado esse básico. As pessoas ficam obcecadas com o sistema de tipos mais abrangente possível, mas, quanto mais complexo e expressivo ele fica, menor é o retorno. E nenhum investimento em sistema de tipos compensa gerenciamento de pacotes horrível, ferramentas de build que obrigam o time a aprender uma DSL nova, sistemas de documentação que não geram automaticamente links para informação de tipos ou docs de pacotes de terceiros, biblioteca padrão fraca, problemas graves de performance, ausência de estratégia de compilação estática, tempos de build dolorosos, curva de aprendizado íngreme, sistema de tipos punitivo, sintaxe difícil de ler ou integração ruim com editor
Dizer que Go não tem nenhuma capacidade de modelagem de dados está claramente errado. Em qualquer linguagem dá para modelar dados e invariantes, e Go oferece um sistema de tipos suficientemente capaz de impor esse modelo
Rust é excelente e uma boa escolha quando velocidade de iteração não importa, ou quando você vai fazer deploy em bare metal, ou quando as exigências de correção e performance são muito altas. Mas não é um bom padrão para desenvolvimento de aplicações em geral, especialmente em equipe. Você realmente digita muito
if err != nil, mas não acho que exista alguém cujo gargalo seja teclas por segundoEstá errado dizer que
if err != nilnão é bug, é feature, e que isso faz você ver todo ponto onde algo pode dar erradoNa prática, isso não é forçado. Ignorar erros acaba sendo mais fácil se você não verificar explicitamente
Rust continua sendo o exemplo que mais brilha em como tratar ou propagar erros
Felizmente, todos os projetos Go em que trabalhei nos últimos anos usavam golangci-lint por cima das verificações estáticas fracas que Go oferece nativamente. Sinceramente, isso deveria ser obrigatório em todo projeto Go
Eu realmente odeio essa moda de escrita, mas concordo com o ponto que o texto quer passar
A frase “não existe um
node_modulesdo tamanho de um Volkswagen” está correta, mas no lugar de umnode_moduleslocal ao projeto existe só um cache global de pacotes em~/gowc -l go.sumAbri a página, vi “Hey, dipshit.” logo de cara e fechei na hora
Tem o mesmo problema da maioria dos textos que exaltam linguagens de programação. Foca menos em como a linguagem atual é ótima e mais em como a anterior era horrível
O autor claramente sofreu muito com Ruby e TypeScript, talvez Python também, e Go parece ter resolvido isso para ele. Mas eu não uso Ruby nem TypeScript, então o texto não me disse muita coisa
Parece a enésima versão de algo que já li dezenas de vezes ao longo dos anos. Ao contrário de Python e JavaScript, tem tipos estáticos, então use Haskell. Ao contrário de Perl e Erlang, dá para distribuir como binário único, então use Rust. Ao contrário de Ruby e Tcl, tem concorrência de verdade e canais, então use Elixir
Fico feliz que o autor tenha encontrado a linguagem certa para ele, mas não vou seguir esse conselho
O valor zero de Go sempre me pareceu um defeito. Acho melhor forçar o usuário a declarar valores padrão explicitamente. Fora isso, para uma linguagem que não é OCaml, ela é até bem boa
boolausente deveria virartrueA experiência de compilação e deploy é excelente, mas eu realmente odeio escrever na linguagem em si. Toda vez que uso, a experiência é ruim. Existe alguma outra linguagem com uma experiência de deploy boa sem ser tão limitada quanto Go?
Será que estou deixando passar alguma coisa em Go?
Recentemente fui fazer deploy de uma pequena aplicação Rails e precisei de configuração demais, então deu para valorizar bem as vantagens de Go
x86_64-unknown-linux-musl. Isso gera um binário estático que simplesmente roda em qualquer máquina Linux 64-bit. Depois eu transfiro comscpe executoAinda fica o problema de alocar uma porta e iniciar manualmente, mas pretendo resolver isso com um pouco de mágica de systemd
Com o bundler, dá para criar um único executável e jogar em máquinas Linux de outras distribuições; mesmo que o Qt não esteja instalado, o usuário só precisa rodar o executável e a GUI inteira funciona
A ressalva é que ainda há problemas com drivers OpenGL. Continua sendo possível, mas fica mais complicado do que “copiar e executar”
O maior problema, para mim, é afirmar que Go foi projetada para concorrência e ainda assim embutir ponteiros brutos que podem ser compartilhados por acidente com facilidade
Ser entediante por si só não é um problema, mas acho que Go falha de um jeito incomum em realmente ser uma linguagem entediante
Dizem que “não tem decoradores”, mas existem struct tags e reflexão. É difícil entender como essas coisas interagem sem executar
Interfaces estruturais e reflexão são uma fonte assustadora de comportamento mudando à distância. Você adiciona um método errado numa struct e o comportamento de uma biblioteca pode mudar completamente
Também é estranho do ponto de vista de documentação. Por que alguém não gostaria de deixar claro que um tipo foi pensado para satisfazer uma determinada interface?
E por que goroutines não são simplesmente chamadas de threads?
Por que channels precisam ser um recurso da linguagem? Acho que é porque Go demorou 10 anos para admitir que genéricos servem para mais do que uns três tipos
Acho que channels fazem parte do runtime porque o scheduler de goroutines precisa conhecê-los para conseguir acordar mais facilmente a goroutine receptora quando um channel deixa de estar vazio. Provavelmente esse jeito era mais simples