1 pontos por GN⁺ 3 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 3 시간 전
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 != nil nã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ção

    • Ainda acho melhor do que esses textos genéricos de IA que tentam agradar todos os lados e não têm posição nenhuma de verdade
    • Achei divertido de ler, mas não vejo muito esse tipo de texto. Ainda assim, acho mais engraçado chamar alguém de “walnut” do que de “dipshit”
      Se vai escrever assim, pelo menos que os insultos tenham um pouco de criatividade
    • Concordo. Tem como denunciar? Não se encaixa em nenhuma categoria de denúncia
  • 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 != nil me 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 feature
    Se 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

    • Acho que o lugar que Go ocupa no espaço de design é o de priorizar a simplicidade para desenvolvedores iniciantes em codebases grandes e organizações grandes acima de quase todo o resto. Isso facilita para desenvolvedores menos experientes lerem código e fazerem mudanças locais sem precisar acumular muito contexto
      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
    • Dart também é uma linguagem do Google que não parece ignorar décadas de pesquisa, mas ninguém usa fora do Flutter. Go é aceitável
    • Esse texto imita um formato de meme agressivo e arrogante. Então era óbvio que ia provocar as pessoas, e eu acho ruim porque o ponto dele merece uma conversa de verdade, não uma guerra de comentários
      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 segundo
    • Tirando Rust, quase não há linguagens modernas com esse tipo de recurso. A menos que você queira programar em Gleam ou Swift, mas, se já vai para um nicho desses, é quase como usar Haskell mesmo
  • Está errado dizer que if err != nil não é bug, é feature, e que isso faz você ver todo ponto onde algo pode dar errado
    Na 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

    • Exato. Sem algo como errcheck, fica fácil demais ignorar erros, e isso é simplesmente idiota. No mínimo, deveria obrigar você a descartar o erro explicitamente
      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
    • Nisso o Swift é melhor. Funcionalmente, é o mesmo modelo, mas no Swift fica mais fácil propagar erros entre bibliotecas diferentes. Só que são escolhas de design com vantagens e desvantagens diferentes, não uma questão de ser melhor ou pior
  • Eu realmente odeio essa moda de escrita, mas concordo com o ponto que o texto quer passar
    A frase “não existe um node_modules do tamanho de um Volkswagen” está correta, mas no lugar de um node_modules local ao projeto existe só um cache global de pacotes em ~/go

    • E ainda por cima suja o diretório home. Nem um ponto na frente tem. Não sei como isso é aceitável
    • Antes de zoar o tamanho da árvore de dependências de outros ecossistemas, eu sempre tenho vontade de mandar a pessoa rodar wc -l go.sum
  • Abri 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

    • Parece que tem bastante gente aqui achando que precisa vender Go para os leitores do Lobsters. Para algumas pessoas, isso pode até ter o efeito contrário
  • 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

    • Eu gosto do valor zero e acho bem inteligente. Mas realmente sinto falta de um recurso de configuração de valores padrão. Por exemplo, é muito difícil fazer marshal de um objeto JSON em que um bool ausente deveria virar true
  • A 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

    • Recentemente comecei a compilar projetos Rust para 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 com scp e executo
      Ainda fica o problema de alocar uma porta e iniciar manualmente, mas pretendo resolver isso com um pouco de mágica de systemd
    • Em termos de experiência de deploy, tivemos um sucesso surpreendentemente grande na empresa com o nix bundler. Para contextualizar, estamos fazendo um app GUI em Qt6
      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

    • Goroutines não são threads; são uma abstração mais leve que roda sobre um pool de threads. Por isso você consegue criar milhares delas facilmente
      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
    • Goroutines são green threads com várias ferramentas extras acopladas