1 pontos por GN⁺ 3 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • A transição de Go para Rust se parece menos com uma escolha por ganho de velocidade e mais com mover problemas de nil, tratamento de erros, corridas de dados e tempo de vida de recursos para garantias em tempo de compilação
  • Go tem como vantagens a compilação rápida, a simplicidade das goroutines e um ecossistema forte para backend, mas Rust bloqueia mais erros no sistema de tipos com Option, Result e Send/Sync
  • O borrow checker de Rust e async/await criam um custo de curva de aprendizado e usabilidade, e o tempo de compilação também deve ser encarado como uma regressão clara em relação a Go
  • Na transição, é mais adequado adotar uma estratégia que comece por componentes com fronteiras bem definidas, como serviços de hot path, workers e alguns endpoints por trás de gateways, em vez de reescrever tudo
  • Os efeitos esperados podem ser resumidos em redução de 20% a 60% de CPU, redução de 30% a 50% de memória, latência P99 mais estável e menos falhas por dereferência de nil e condições de corrida

Foco da transição

  • A transição de Go para Rust está mais próxima de discutir garantias de correção, trade-offs de runtime e diferenças na experiência do desenvolvedor do que perguntar se “Rust é mais rápido”
  • O centro da comparação são os serviços de backend, tomando como referência o fato de Go ser forte em binários estáticos pequenos, biblioteca padrão voltada a rede e ecossistema de servidor HTTP, gRPC e banco de dados
  • Parte do conteúdo também pode se aplicar a ferramentas de CLI, firmware embarcado e engines de jogos, mas esses não são os alvos otimizados
  • Como material de contexto relacionado, são apresentados “Go vs Rust? Choose Go.” de 2017 e “Rust vs Go: A Hands-On Comparison” da equipe da Shuttle
  • Go é uma linguagem bem-sucedida, mas escolhas de design como o uso disseminado de nil, o tratamento de erros que depende de disciplina em vez de tipos e a ausência de generics por muito tempo se tornam pontos centrais quando comparadas com Rust
  • Na JetBrains Developer Ecosystem Survey, Go aparece como uma linguagem que mantém uma proporção de 17% a 19% de desenvolvedores ativos, enquanto Rust cresce de forma consistente, mas com participação menor

Ferramentas

  • Tanto Go quanto Rust têm um conjunto de ferramentas com tudo incluído, oferecendo build, teste, formatação, lint e gerenciamento de dependências por meio de uma interface consistente
  • O cargo oferece, como ferramenta principal, um conjunto mais amplo de funções correspondentes às ferramentas de Go
    • go.mod / go.sumCargo.toml / Cargo.lock: configuração do projeto e manifesto de dependências
    • go get / go mod tidycargo add / cargo update: adição e resolução de dependências
    • go buildcargo build: compilação
    • go run .cargo run: executar após compilar
    • go test ./...cargo test: testes
    • go vet ./...cargo clippy: lint, sendo que Clippy é muito mais opinativo que vet
    • gofmt / goimportscargo fmt: formatador automático sem configuração
    • golangci-lint runcargo clippy -- -D warnings: modo de lint rigoroso
    • go doccargo doc --open: geração e visualização de documentação de API
    • pprofcargo flamegraph / samply: profiling de CPU
    • govulncheckcargo audit: verificação de vulnerabilidades baseada em banco de dados de avisos
  • Em Go, muitas vezes ferramentas de terceiros como golangci-lint, mockgen, air e goreleaser são usadas para preencher lacunas, mas em Rust o ecossistema principal já cobre mais recursos por padrão
  • Mesmo quando crates externas são necessárias, elas podem ser instaladas com um único cargo install cargo-nextest, como no caso de cargo watch e cargo nextest, e depois funcionam como ferramentas nativas, como em cargo nextest
  • gofmt e rustfmt têm como benefício maior eliminar discussões de estilo em code review do que satisfazer preferências detalhadas de estilo
    • Citação dos Go Proverbs de Rob Pike: “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.”

Diferenças centrais entre Go e Rust

  • As duas linguagens oferecem compilação, tipagem estática, distribuição em binário único e um modelo forte de concorrência, mas a diferença está no alcance das garantias do compilador e no nível de controle sobre o comportamento do runtime
  • Os principais itens de comparação são os seguintes
    • Releases estáveis: Go em 2012, Rust em 2015
    • Sistema de tipos: Go é estático e estrutural, com suporte a generics desde a 1.18; Rust é estático e nominal, com suporte a generics, traits e lifetimes
    • Gerenciamento de memória: Go usa garbage collection concorrente e de baixa latência; Rust é baseado em ownership e borrowing, sem GC
    • Segurança contra nulos: em Go, nil existe amplamente; em Rust, não há nulo, e Option<T> é o substituto no nível de tipo
    • Tratamento de erros: Go usa a interface error e if err != nil { ... }; Rust usa Result<T, E>, o operador ? e pattern matching completo
    • Concorrência: Go usa CSP com goroutines e channels; Rust usa async/await, channels e threads sobre tokio
    • Cancelamento: em Go, context.Context é baseado em convenção; em Rust, a passagem é explícita e verificada por tipos, com recursos como CancellationToken
    • Corridas de dados: Go detecta de forma probabilística em runtime com -race; Rust detecta em tempo de compilação com Send/Sync
    • Tempo de compilação: Go é muito rápido, e Rust é lento, especialmente em builds limpos
    • Runtime: Go tem runtime de cerca de 2 MB e GC; Rust não tem runtime além de libc, ou pode fazer build totalmente estático com MUSL
    • Tamanho do ecossistema: Go tem cerca de 750 mil+ módulos, Rust tem 250 mil+ crates
  • Verificações que em Go dependiam de convenção, ferramentas e detecção em runtime — como tratamento de nil, propagação de erros, corridas de dados, tempo de vida de recursos, cancelamento e generics — em Rust passam para dentro do sistema de tipos
  • O Mutex<T> de Rust permite acessar o valor interno apenas por meio do guard obtido com .lock(), removendo do próprio tipo a possibilidade de “esquecer o lock” em algum caminho
  • O mesmo padrão se repete em Option, Result, &mut T, Send/Sync e guards RAII de forma geral, e, com o tempo, o compilador passa a substituir boa parte da checagem mental

Limitações do Go que levam a considerar Rust

  • Como o Go é rápido o suficiente para a maioria das cargas de trabalho de backend, o principal motivo para avaliar Rust tem menos a ver com velocidade e mais com a verbosidade do tratamento de erros, o risco de ponteiros nil e a falta de recursos mais sofisticados do sistema de tipos, como enums e traits
  • As interfaces do Go não substituem adequadamente as traits do Rust, e a biblioteca padrão não tem um tipo Set, exigindo soluções idiomáticas alternativas como map[T]struct{}
  • Pânicos de nil em produção

    • Um serviço em Go pode funcionar normalmente por meses e então, em um caminho específico de código, gerar um pânico em goroutine por não verificar um ponteiro nil
    • No exemplo, Find retorna (*User, error) e, em caso de “not found”, error é nil, mas a verificação de user continua sendo responsabilidade do chamador
    • user.Account.Notify() pode travar quando user ou Account for nil
    • Linters como nilaway e staticcheck, além das verificações da IDE, detectam parte desses casos, mas são opt-in, probabilísticos e não atravessam fronteiras de pacote com confiabilidade
    • O Option<T> do Rust elimina essa categoria de falha ao impedir a desreferenciação sem tratar o caso None
  • Data races que o -race não detectou

    • go test -race é uma ferramenta excelente, mas como é um detector em tempo de execução, só encontra races que de fato acontecem durante os testes
    • No Go, código em que duas goroutines modificam um map sem lock ainda compila e pode estourar em produção sob carga
    • No Rust, compartilhar estado mutável entre threads exige tipos que implementem Send e Sync, e tentar compartilhar um HashMap comum entre threads não compila
    • Isso força o uso de Arc<Mutex<...>>, Arc<RwLock<...>> ou canais, transformando condições de corrida em erros de tipo
    • Paul Dix cita diretamente a eliminação de data races como motivação para a reescrita do InfluxDB 3.0
      • “[The main benefit is] fearless concurrency — eliminating data races essentially, which we had before. Really gnarly bugs in version 1 of Influx due to that.”
      • Fonte: Paul Dix, Founder & CTO, InfluxData, Rust in Production
  • Tratamento de erros combinável

    • O if err != nil { return err } do Go pode diluir a lógica real da função, e envolver contexto com fmt.Errorf("doing X: %w", err) depende mais de disciplina do que de regras impostas pelo compilador
    • Na thread do Lobste.rs, desenvolvedores experientes de Go rebatem dizendo que errcheck e golangci-lint capturam a maioria das omissões no tratamento de erros, e que o if err != nil explícito é mais legível do que cadeias densas de ?
    • Peter Bourgon apresenta o tratamento explícito de erros em Go como um valor cultural intencional
      • “I think that error handling should be explicit, this should be a core value of the language.”
      • Fonte: Peter Bourgon, GoTime #91, citado no Zen of Go de Dave Cheney
    • O Result<T, E> do Rust faz parte da própria assinatura de tipo, então não pode ser esquecido, e com enums definidos via thiserror::Error e #[from] é possível obter conversão de erros e verificação de exaustividade
    • Ao adicionar uma nova variant de erro, o compilador informa quais pontos de match precisam ser atualizados
  • Genéricos sem boxing

    • Os genéricos do Go 1.18 são úteis, mas têm limitações como a ausência de métodos com parâmetros de tipo, GC shape stenciling e características de desempenho às vezes surpreendentes
    • Os genéricos do Rust são monomorfizados, então cada instanciação gera código especializado sem custo em tempo de execução
    • Em combinação com traits, isso permite abstrações de custo zero
    • Isso importa mais em infraestrutura compartilhada, como middleware, repositórios genéricos, decoders e parsers, do que em código de handlers; no Go, essas áreas frequentemente acabam recorrendo a interface{}/any e type assertions
  • Latência previsível

    • O GC do Go é excelente, concorrente, de baixa latência e bem ajustado para cargas de trabalho típicas de serviços, mas “low-pause” não é “no-pause”
    • Em cenários com muitas alocações, a cauda de latência P99 pode ser pior do que em uma implementação em Rust que não aloca no hot path
    • Em sistemas sensíveis à latência, como trading, leilão em tempo real, proxy de rede e ingestão de alto throughput, a ausência de pausas de GC é uma vantagem real
    • Stephen Blum afirma que, na escala da PubNub, o Rust era necessário para alcançar a capacidade de desempenho por dólar de que precisavam
      • “Go is great at our scale, but we really need something that is going to give us the price-per-dollar performance capacity that we need, and Rust is going to get us there. That’s why basically everything is heading towards Rust these days.”
      • Fonte: Stephen Blum, CTO, PubNub, Rust in Production

Equivalentes em Rust para padrões de Go

  • A forma mais rápida de se familiarizar com Rust é mapear padrões de Go que você já conhece para seus equivalentes em Rust
  • Há um exemplo mais longo implementando o mesmo serviço de backend nas duas linguagens em Shuttle comparison
  • Tratamento de erros: if err != nil vs Result<T, E>

    • Em Go, após os.ReadFile(path) e json.Unmarshal, retorna-se um erro com contexto usando if err != nil
    • Em Rust, isso é composto por fs::read_to_string(path)?, serde_json::from_str(&data)? e Ok(cfg)
    • O operador ? substitui o padrão if err != nil { return err } e, se From<E1> for E2 estiver implementado, também faz a conversão de tipo
    • #[from] de thiserror dá suporte idiomático a essa conversão
  • Nulo: nil vs Option<T>

    • Em Go, GetUser(id string) *User retorna nil quando não encontra o usuário, e se o chamador fizer fmt.Println(u.Name), ocorrerá panic quando for nil
    • Em Rust, get_user(id: &str) -> Option<User> retorna Some(User) ou None
    • let user = get_user("123"); println!("{}", user.name); gera erro de compilação porque user não é User, e sim Option<User>
    • É preciso tratar tanto Some(u) quanto None com match get_user("123")
    • Em Rust seguro, não existe nil, e referências não podem ser nulas
  • Interfaces vs traits

    • Interfaces em Go são estruturais, e um tipo satisfaz uma interface implicitamente
    • Traits em Rust são nominais, e precisam ser implementadas explicitamente
    • A abordagem de Go é boa para duck typing improvisado, enquanto a de Rust é boa para refatoração e discoverability, permitindo encontrar com grep implementações de um trait específico
    • Funções genéricas com trait bound, como fn handle<R: Reader>(r: R), cobrem a maioria dos casos e, com monomorfização, não há dispatch em tempo de execução
    • Para armazenar implementações heterogêneas quando é necessário dispatch em tempo de execução, usa-se Box<dyn Trait> ou Arc<dyn Trait>
  • Goroutine vs task async

    • O modelo de concorrência de Go é simples, como em go doWork(ctx, input), e goroutines são baratas, com o runtime fazendo o escalonamento sobre threads do sistema operacional
    • A grande vantagem de Go é não haver distinção sintática entre código sequencial e código paralelo
    • Em Rust, em serviços de backend, quase sempre se usa async/await sobre o executor tokio
    • Funções async retornam Future e não executam até serem aguardadas com await ou lançadas com spawn
    • O compilador rastreia Send/Sync antes e depois dos pontos de .await, e gera erro de compilação se um valor non-Send for mantido além de um await
    • Como não existe preempção embutida no estilo de goroutine, executar trabalho CPU-bound por muito tempo dentro de uma task async pode deixar o executor sem recursos; é preciso passar isso para tokio::task::spawn_blocking ou rayon
  • context.Context vs CancellationToken

    • Em Go, context.Context é passado para toda chamada bloqueante
    • Em Rust, não há um context.Context embutido, e o equivalente mais próximo para cancelamento é tokio_util::sync::CancellationToken
    • Timeouts são aplicados envolvendo a future com tokio::time::timeout(dur, fut)
    • Deadlines e valores muitas vezes são passados por argumentos explícitos ou via span de tracing, em vez de por um único objeto de contexto
    • Citação de Dave Cheney em The Zen of Go:
      • “Go não tem uma forma de dizer a uma goroutine para encerrar. Não existe função de stop ou kill, e por um bom motivo. Se não podemos ordenar que uma goroutine pare, então precisamos pedir isso a ela, educadamente.”
    • Em Go, esse “pedido educado” é o context.Context propagado por convenção; em Rust, é um CancellationToken ou canal watch, mas o compilador pode avisar quando isso foi omitido
  • Strings: string vs String e &str

    • O string de Go é um slice de bytes UTF-8; ao atribuir, o header é copiado e os bytes subjacentes são compartilhados em uma estrutura imutável
    • Rust divide isso em dois tipos
      • String: possui os dados, é alocado no heap e pode crescer
      • &str: uma view emprestada sobre dados de string de outro lugar, correspondendo na maioria dos casos a um parâmetro string de Go
    • A regra prática é receber &str em argumentos e retornar String ao criar novos dados
    • A separação entre &str e String mostra de forma reduzida o modelo de “borrow vs own” de Rust

Avaliação dos genéricos em Go

  • O Go introduziu genéricos na versão 1.18, em março de 2022, 13 anos após o lançamento da linguagem
  • Os genéricos são úteis, mas são avaliados como não oferecendo plenamente as vantagens esperadas em Rust, Haskell e C++ moderno, enquanto também carregam boa parte das desvantagens dos sistemas de tipos genéricos
  • A biblioteca padrão quase não usa

    • Mesmo 3 anos após a introdução dos genéricos, a biblioteca padrão do Go ainda evita genéricos na maior parte dos casos
    • sort.Slice ainda recebe uma closure func(i, j int) bool em vez de uma constraint cmp.Ordered
    • sync.Map ainda é tipado como any/any
    • Os helpers genéricos existentes se limitam a poucos pacotes, como alguns itens em slices, maps, cmp e sync
    • O compromisso de compatibilidade do Go 1 explica parcialmente a dificuldade de remodelar APIs antigas não genéricas, mas, ao contrário do Rust, Go não usa genéricos como ferramenta principal
    • No Rust, desde o início, genéricos permeiam Option<T>, Result<T, E>, Vec<T>, HashMap<K, V>, Iterator, From/Into, todas as coleções e smart pointers
  • Não há sistema de traits, apenas constraints estruturais

    • Os genéricos em Rust vêm acoplados a traits, que lidam com polimorfismo ad-hoc, supertraits, associated types, blanket impls e coherence
    • As constraints do Go se parecem mais com interfaces, com a adição do operador ~ para membership em type sets
    • Em Go não existem hierarquias de supertraits como trait Ord: Eq + PartialOrd do Rust, associated types como type Item; do Iterator, nem blanket impls como impl<T: Display> ToString for T
    • Em Go não é possível usar métodos com parâmetros de tipo, então formas como func (s Set[T]) Map[U](<https://corrode.dev/learn/migration-guides/go-to-rust/f func(T>) U) Set[U] são impossíveis
    • Quando a abstração passa de “uma função que opera sobre qualquer T com algumas operações” para algo além disso, o Go acaba voltando para any, type assertions, geração de código e reflection em tempo de execução
  • Diferenças na inferência de tipos e na estratégia de implementação

    • Rust propaga informações de tipo por toda a expression, incluindo closures, cadeias de iterators e o operador ?
    • A inferência do Go é mais superficial e normalmente infere type parameters a partir dos argumentos da função, mas não consegue inferir no contexto da posição de retorno, exigindo com frequência type arguments explícitos no ponto de chamada
    • O Go escolheu um caminho intermediário chamado GCShape stenciling and dictionaries para manter tempos de compilação rápidos, mas isso pode introduzir indireção em cada chamada de método de um type parameter
    • Como evidência disso, é citado o artigo da PlanetScale
    • Rust gera código de máquina especializado separadamente para Vec<i32> e Vec<String>, sem dispatch em tempo de execução
    • O custo da monomorfização é o tempo de compilação, e as duas linguagens otimizam objetivos diferentes
  • Não corrige os buracos do sistema de tipos

    • Em Rust, genéricos e traits eliminam a maior parte das situações em que seria necessário Box<dyn Any> ou reflection em tempo de execução
    • Os genéricos do Go não eliminam any, reflect nem os padrões de geração de código dominantes em ORMs, decoders e mocks
    • encoding/json ainda usa reflection, database/sql ainda usa any, e mockgen ainda gera código
    • Os genéricos do Go parecem mais uma nova ferramenta útil para casos restritos, enquanto os genéricos do Rust funcionam como uma base sem a qual a linguagem desmorona

Ecossistema de backend em Rust

  • O ecossistema Rust também já convergiu razoavelmente para algumas “opções padrão” para serviços de backend em geral
  • Principais equivalências:
    • Servidor HTTP: Go net/http, chi, gin, echo, fiber → Rust axum sobre hyper
    • Cliente HTTP: Go net/http, resty → Rust reqwest
    • gRPC: Go google.golang.org/grpc + protoc-gen-go → Rust tonic + prost
    • SQL: Go database/sql, sqlc, sqlx, gorm → Rust sqlx, sea-orm, diesel
    • Migrations: Go golang-migrate, goose → Rust sqlx migrate, refinery
    • JSON: Go encoding/json, sonic, goccy/go-json → Rust serde + serde_json
    • Logging: Go log/slog, zerolog, zap → Rust tracing + tracing-subscriber
    • Métricas: Go prometheus/client_golang → Rust metrics + metrics-exporter-prometheus
    • Configuração: Go viper, koanf → Rust config / config-rs, figment
    • CLI: Go cobra, urfave/cli → Rust clap derive
    • Erros: Go errors, pkg/errors → Rust thiserror para bibliotecas, anyhow para binários
    • Testes: Go testing, testify, gomega → Rust #[test] embutido, rstest, assert_matches
    • Mocking: Go mockgen, moq → No Rust, fakes escritos manualmente são idiomáticos, e mockall também é usado
    • Tarefas em background: Go goroutines + errgroup → Rust tokio::spawn + JoinSet
  • Em um serviço de backend típico, a combinação axum + sqlx + tokio + tracing + serde + clap cobre 90% do que é necessário, segundo o texto

Borrow checker e curva de aprendizado

  • Ao vir de Go para Rust, é preciso partir do princípio de que você vai dar de cara com a parede
  • O runtime de Go cuida de memória e aliasing por você, mas Rust transfere essas decisões para o sistema de tipos, então nas primeiras semanas o compilador pode rejeitar um código que “obviamente deveria funcionar”
  • Padrões com os quais desenvolvedores Go costumam bater de frente:
    • Referências de longa duração: em Go, é natural manter por bastante tempo um *User tirado de um map, mas em Rust alterações no map ficam bloqueadas enquanto esse empréstimo estiver ativo
    • Structs autorreferenciais: em Go, dá para colocar os dados e um iterador sobre esses dados na mesma struct, mas em Rust isso exige Pin, ouroboros ou um redesenho
    • Compartilhar estado mutável entre goroutines: o padrão mu sync.Mutex; data map[K]V de Go vira algo como Arc<Mutex<HashMap<K, V>>> em Rust
    • Retornar referências de funções: entram em cena as anotações de lifetime, um conceito novo para desenvolvedores Go
  • O borrow checker não deve ser visto como um “porteiro” atrapalhando, mas como um mecanismo que revela bugs reais
  • Ele barra em tempo de compilação casos como usar um valor depois que ele foi movido, múltiplas threads mexendo nos mesmos dados ao mesmo tempo, desreferenciar ponteiros nulos ou soltos, ou referências vivendo mais do que os valores
  • Quando você internaliza o conceito de empréstimo, ele deixa de ser algo contra o qual lutar e vira um colaborador; desenvolvedores Rust experientes costumam dizer que o borrow checker passou a ajudá-los entre 4 e 12 semanas
  • O CTO da PubNub, Stephen Blum, disse no Rustacean Station que o primeiro mês “foi como aprender a programar pela primeira vez” e que precisou lidar à força com o borrow checker e lifetimes
  • Ed Page, maintainer do clap, disse em Rustacean Station: clap with Ed Page que o borrow checker o ajudou a focar em problemas de mais alto nível e também pegou pontos em que sua própria análise falhou

Principais dificuldades na transição para Rust

  • Tempo de compilação

    • O tempo de compilação do Rust deve ser encarado como um retrocesso claro em relação ao Go, e um build limpo de release de um serviço de porte médio pode levar vários minutos, ao contrário da compilação quase instantânea do Go
    • Builds incrementais e cargo check são razoáveis, e o tempo de compilação melhorou ano após ano, mas a diferença em relação ao Go é perceptível
    • No loop de edição, use cargo check; se começar a valer a pena, separe em workspaces; e mantenha crates com muitos procedural macros em crates separados para que só sejam recompilados quando mudarem
    • Para mais detalhes, vale consultar dicas para reduzir o tempo de compilação em Rust
  • O problema da coloração assíncrona

    • A separação entre async fn e fn em Rust é uma das maiores regressões de usabilidade para quem vem de Go
    • async trait foi estabilizado a partir do Rust 1.75, mas ainda há arestas ao misturar isso com despacho dinâmico
    • Em algumas situações, acaba-se usando a crate async-trait para mascarar essas partes
  • Ecossistema menor

    • O ecossistema de crates de Rust está crescendo e a qualidade das bibliotecas é alta no geral, mas Go ainda está à frente em algumas áreas adjacentes de backend
    • Áreas em que Go leva vantagem incluem operadores de Kubernetes, SDKs de provedores de nuvem e drivers de banco de dados para certos armazenamentos de nicho
    • Antes de bater o martelo sobre a migração, vale gastar cerca de um dia verificando se existem alternativas em Rust viáveis para as bibliotecas das quais você depende
    • Algumas equipes podem precisar atualizar crates abandonadas de validação de esquema XML ou escrever por conta própria clientes para protocolos menos conhecidos

Estratégias de integração

  • Uma transição bem-sucedida de Go para Rust costuma ser menos um rewrite total e mais uma escolha tática
  • O Principal Engineer da Microsoft, Victor Ciura, disse em Rust in Production: “não é reescrever tudo em Rust por diversão, mas fazer uma escolha tática de usar Rust quando um novo componente se encaixa melhor em Rust”
  • 1. Separar o hot path em um serviço

    • Se um serviço específico continua causando problemas, a migração de menor risco é reescrever só esse serviço em Rust, mantendo-o atrás do mesmo contrato de API
    • O alvo pode ser um serviço com alto uso de CPU, sensível à latência ou com problemas recorrentes de estabilidade
    • Os outros serviços em Go continuam se comunicando via HTTP/gRPC, então não precisam saber qual é a linguagem de implementação interna
    • O CTO da Radar, Jeff Kao, disse em Rust in Production que o texto da Discord sobre a mudança de Go para Rust o fez pensar em tentar o mesmo na Radar
  • 2. Substituir sidecars ou processos worker

    • Workers de background, consumidores de fila, pipelines de coleta e jobs em batch CPU-bound são bons primeiros alvos
    • Em geral, eles têm fronteiras claras de entrada e saída, como filas ou tópicos, e não compartilham estado in-process com o restante do sistema
  • 3. cgo é possível, mas doloroso

    • Dá para chamar Rust a partir de Go via cgo, e há um bom guia sobre isso
    • Em serviços de backend, isso normalmente não é recomendado
    • A complexidade de build e o overhead de FFI muitas vezes anulam os benefícios em comparação com “subir um serviço em Rust e colocá-lo atrás de uma chamada de rede”
    • Para bibliotecas e ferramentas de CLI, isso pode ser mais prático
  • 4. Aplicar o Strangler Pattern atrás do gateway

    • Se houver um gateway de API ou reverse proxy, é possível rotear apenas endpoints específicos para o novo serviço em Rust e deixar o restante em Go
    • Isso funciona especialmente bem quando um contexto delimitado, como autenticação, busca ou pagamentos, é uma boa unidade de migração
    • Esse padrão é chamado de “strangler fig” porque o novo serviço cresce ao redor do serviço existente até substituí-lo por completo

Dicas práticas de migração

  • Comece por um serviço com limites claros; não escolha o serviço mais central e com mais deploys
  • Escolha um serviço cujo contrato com o restante do sistema esteja bem definido e cujo raio de impacto seja pequeno
  • Manter o mesmo contrato de API

    • Se o serviço em Go expõe uma API REST, o serviço em Rust também deve manter os mesmos caminhos, o mesmo formato de JSON e os mesmos wrappers de erro
    • A migração fica invisível para os clientes, e o tráfego pode ser desviado gradualmente via gateway
  • Não transportar os idioms ao pé da letra

    • if err != nil { return err } vira ?
    • O padrão de uma goroutine por requisição só deve virar tokio::spawn quando isso for realmente necessário
    • axum já processa requisições em paralelo
    • Interfaces de método único normalmente viram trait bounds em genéricos, e não Box<dyn Trait>
  • Use o compilador como um pair programmer

    • As mensagens de erro do compilador Rust em geral são de alta qualidade e, se você ler com calma, quase sempre dizem a resposta certa
    • Os membros da equipe que mais sofrem por mais tempo costumam ser os que não veem o compilador como colaborador e preferem brigar com ele
  • Invista em treinamento no início

    • Migrações para Rust feitas “em paralelo” ao aprendizado costumam não terminar bem
    • É preciso reservar tempo real para aprender, com workshops, cursos online e sessões de pair programming sobre a base de código real
    • Quando a equipe ganha fluência, o investimento inicial costuma se pagar várias vezes

Onde Go continua sendo a escolha certa

  • Não é necessário migrar tudo para Rust, e há áreas em que Go é especialmente bom
  • Ferramentas nativas de Kubernetes

    • O ecossistema de operadores, controladores e CRDs é esmagadoramente centrado em Go
  • Utilitários de CLI e ferramentas de desenvolvimento

    • Compilação rápida, cross-compilation fácil e implantação simples são pontos fortes
  • Serviços de cola

    • Em camadas finas de API, proxies e conversores de formato, a proporção de boilerplate de Rust pode não compensar
  • Onde a velocidade da equipe é mais importante do que garantias absolutas de correção

    • Em áreas em que é preciso se mover rápido, Go pode continuar sendo a escolha certa
    • Jon Seager, VP of Engineering da Canonical, diz em Rust in Production que Go é uma escolha muito boa para serviços de rede, que a Canonical tem muito Go e que o Juju também é uma enorme base de código em Go
    • Estratégias híbridas são comuns, e muitas equipes acabam com backends poliglotas, usando Go para serviços “sem graça” e Rust para serviços em que estabilidade e desempenho compensam o esforço extra

Melhorias esperadas

  • Os números variam bastante conforme a carga de trabalho, então devem ser vistos como um guia aproximado, não como promessa
  • Faixas aproximadas de melhoria observadas em migrações de Go para Rust:
    • Uso de CPU: redução de 20~60%
      • Como Go já é eficiente, o ganho tende a ser menos dramático do que ao migrar de Python para Rust
      • Os benefícios vêm da ausência de GC e de loops mais enxutos
    • Memória: redução de 30~50%
      • Principalmente por não haver overhead de GC e por o runtime ser menor
    • Latência P99: muito mais consistente
      • Serviços em Rust tendem a ficar mais estáveis, com menos jitter induzido por GC visto em serviços Go
      • O lado do Go melhorou bastante desde a introdução de GC de baixa latência, mas em alta carga a diferença continua existindo
    • Incidentes em produção: é a área de melhoria mais relatada com convicção pelas equipes
      • Tipos de bug como data races, desreferência de nil e caminhos de erro ausentes, que passam em go test -race e chegam à produção, simplesmente não compilam em Rust
      • Depois da migração para Rust, os plantões de on-call tendem a ficar bem mais tranquilos
  • Andrew Lamb, Staff Engineer da InfluxData, afirma em Rustacean Station: Rebuilding InfluxDB with Rust que, após a reescrita do InfluxDB, não houve necessidade de rastrear crashes, condições estranhas de corrida em multithreading e problemas que antes consumiam muito tempo
  • Ao migrar de Go para Rust, é improvável obter uma melhora de throughput de 10x como às vezes ocorre ao migrar de Python para Rust
  • O benefício real está na redução de “erros absurdos”, em caudas de latência mais estáveis e na capacidade de se expandir para outras áreas, como desenvolvimento embarcado ou programação de sistemas, na mesma linguagem

Observações complementares

  • O sistema de tipos de Rust não elimina todos os bugs de lógica de sincronização, mas tipos que não podem ser compartilhados entre threads sem sincronização não compilam
  • O tipo de problema em que “esqueceram o lock” leva a corrupção silenciosa de dados é algo que o sistema de tipos de Rust pode impedir
  • string em Go é uma sequência imutável de bytes, convencionalmente UTF-8, mas isso não é garantido no nível do tipo
  • A correspondência mais próxima é Go string ↔ Rust &str para uma visão somente leitura, e Go []byte ↔ Rust Vec<u8> para buffers mutáveis
  • String em Rust é a versão expansível com ownership de &str, com a garantia adicional de que o conteúdo é UTF-8 válido
  • Para mais detalhes, consulte Strings, bytes, runes and characters in Go
  • Desde o Go 1.18, funções genéricas e tipos genéricos são possíveis, mas parâmetros de tipo em métodos em si não foram introduzidos
  • Cadeias de iteradores como (0..100).filter(|n| ...).collect() podem parecer estranhas para desenvolvedores Go, mas também é possível usar loops for em Rust, e em código pontual essa costuma ser a escolha certa

Conclusão

  • A transição de Go para Rust é diferente da transição para Rust a partir de Python ou TypeScript
  • Quem vem de Go já conhece as vantagens de tipos estáticos e de linguagens compiladas, então não se trata de abrir mão de tipagem dinâmica ou de um runtime lento
  • A troca central é abandonar nil e ganhar uma base de código mais robusta, menos armadilhas e um compilador mais rigoroso que captura mais erros em tempo de compilação
  • Em troca, a curva de aprendizado é mais íngreme
  • Em serviços dos quais a organização depende, que exigem alta disponibilidade e são críticos para o negócio, como software fundamental, essa troca claramente vale a pena
  • Em outros serviços, Go ainda pode ser a resposta certa
  • O objetivo da migração é posicionar cada problema na linguagem que melhor o resolve

1 comentários

 
GN⁺ 3 시간 전
Comentários do Hacker News
  • Entendo migrar de C/C++ ou Python para Rust por vários motivos, mas para backend web Go parece uma escolha bem adequada
    Uso quase só Rust, mas da última vez que trabalhei com servidor web em Rust, senti que teria sido melhor usar Go
    O texto original aponta que a sintaxe de tratamento de erros em Go é verbosa, e isso é uma crítica válida. Rust também tinha esse problema e depois adicionou a sintaxe ?, que retorna o valor de erro quando há erro. O tratamento de erros em Go é, na maioria das vezes, a forma expandida disso
    Rust não tem um tipo de erro unificado, e há sistemas de erro importantes como io::Error, thiserror e anyhow, o que torna incômodo propagar erros para cima na cadeia de chamadas
    Há coisas que, se ficam de fora de uma linguagem nova, são difíceis de adicionar depois. Tipos de constantes, tipo booleano, tipo de erro, tipo de array multidimensional, tipos de vetores/matrizes de tamanho 2/3/4 e operações padrão são exemplos disso. Se isso não for padronizado no início, acaba-se gastando muito tempo conciliando várias representações do mesmo conceito
    Tirando o tratamento de erros, isso impacta menos no desenvolvimento web, mas em computação numérica, gráficos e modelagem vira um grande sofrimento, porque é preciso aplicar operações padrão a arrays numéricos
    Go tem duas vantagens em serviços web. A primeira são as goroutines, como o texto original menciona, e a segunda são as bibliotecas, ponto que o texto quase não aborda. Go já tem a maioria das bibliotecas necessárias para serviços web, e muitas são usadas internamente no Google, então já sobreviveram a ambientes extremamente severos. Já os crates de Rust muitas vezes são menos maduros e não têm garantia oficial de qualidade

    • Acho que a maior vantagem de Go sobre Rust é a velocidade de compilação
      Além disso, Rust ainda depende bastante de bibliotecas C/C++ em comparação com Go, então compilação cruzada, builds reproduzíveis e geração de binários estáticos costumam virar problema com mais facilidade
      O ponto fraco de Go é que o coletor de lixo é simples demais. Se houver picos de latência, há poucas formas de reagir além de uma reescrita dolorosa
    • Rust de fato tem praticamente um único sistema de erro: o trait Error
      O que foi listado são apenas formas comuns de usá-lo, e usar só Box também funciona perfeitamente. Isso é em grande parte parecido com o que anyhow::Error faz
    • Gostei bastante de Go por um tempo, mas depois de usar mais Swift e Rust recentemente, um compilador que não impede desreferência de ponteiro nulo e não garante segurança de concorrência parece meio pré-histórico
      Dito isso, acho que Go se saiu muito melhor que Rust no lado da biblioteca padrão
    • Concordo. Logo no começo me chamou atenção a parte em que o texto dizia que era para serviços de backend
      Gosto da linguagem Rust e a uso em firmware embarcado e aplicações para PC, mas para backend web ainda uso Python. Isso porque Rust não tem um conjunto de ferramentas no nível de Django ou Rails
      Há coisas parecidas com Flask, mas sem o ecossistema sólido do Flask. Tenho pouca experiência com Go, mas para backend web eu provavelmente escolheria Go em vez de Rust. O motivo é o ecossistema de bibliotecas e frameworks
      Além disso, pelos motivos de sempre, não gosto muito de Async Rust. O ecossistema web de Rust é quase todo praticamente dependente de uso assíncrono
    • Rust não tem três sistemas de erro, mas um só: o trait Error
      io::Error é apenas um dos vários tipos que o implementam, nada de especial. Erros definidos com thiserror também implementam esse trait
      anyhow apenas permite dizer de forma conveniente “algum Error” quando você não quer detalhar no contrato da API o tipo de erro que a função pode emitir
  • Rust facilita mais do que Go escrever código determinístico, então é muito útil quando você precisa de testes de simulação determinística e testes baseados em propriedades
    Recentemente escrevi em Go uma ferramenta de espelhamento de dados de Postgres para Iceberg, https://github.com/polynya-dev/pg2iceberg, mas a portei para Rust porque queria fazer testes de simulação determinística sem brigar com o runtime de Go
    Dito isso, se esse domínio não for importante o bastante para justificar esse tipo de teste, eu escolheria Go em vez de Rust a qualquer momento
    Texto relacionado: https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...

  • Pode soar óbvio e repetitivo, mas minha maior reclamação sobre Rust é a situação do gerenciamento de pacotes, e acho que isso é totalmente resultado da mentalidade dos desenvolvedores
    Gosto da usabilidade do lado de Rust. A abordagem funcional para tipos de dados é bonita. Mas estou trabalhando em paralelo num projeto Rust e num projeto Go, e a árvore de dependências é um bicho completamente diferente
    O projeto Go resolve quase tudo com a biblioteca padrão, mas no projeto Rust eu só pedi rusqlite (sqlite), clap (CLI), ratatui (TUI) e tauri (GUI), e parece que já passaram de 400 dependências. tauri em especial é o principal culpado; mesmo sem ele ainda ficam quase 100, o que parece insano
    Seria muito melhor se houvesse alternativas de crates Rust bem mantidas que lidassem com dependências de forma razoável, mas ainda não encontrei. Eu só não quero soltar um shai hulud no sistema, mas o pessoal do web em Rust parece querer transformar o cargo em algo como o npm nesse aspecto

    • É preciso considerar que muitas bibliotecas Rust são divididas em vários crates, e todos eles entram no grafo de dependências
      Por isso, a contagem de dependências parece maior do que realmente é. Mesmo sendo crates separados, muitas vezes têm o mesmo mantenedor e fazem parte do mesmo repositório Git upstream
      Ainda assim, concordo com a sensação geral. Em Rust há muitos crates meio abandonados em versão 0.x, e com frequência não existe alternativa melhor
    • Acho que a biblioteca padrão é o lugar para onde boas ideias vão morrer
      E então sai a httplib3, depois a httplib4
      Em outras palavras, prefiro muito mais a abordagem de Rust. Para mim, não faz tanta diferença depender da biblioteca padrão ou de outra dependência qualquer. Continua sendo uma dependência
      Achar que, por ser biblioteca padrão, a qualidade é maior ou a manutenção é melhor é uma questão separada
      No fim, tudo depende de recursos. Claro, a biblioteca padrão pode receber mais recursos, mas também pode ficar inchada e impossível de manter
    • Tirando Java, não sei se existe alguma linguagem que tenha na biblioteca padrão equivalentes a rusqlite, clap, ratatui e tauri
      Também é preciso notar que o próprio Tauri é composto por 14 crates, e cada um aparece na árvore de build
      https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
      O Ratatui também são 6
      https://github.com/ratatui/ratatui/blob/main/Cargo.toml
    • Gerenciamento de pacotes é dor de cabeça em quase todas as linguagens e tecnologias
      Ninguém “resolveu” isso, e acho difícil que exista uma solução única no futuro
      Em Go, você precisa confiar que o autor da biblioteca vai seguir corretamente o versionamento semântico, e não dá para fixar versão. Pessoalmente isso me incomoda bastante
      Há alguns contornos. Dá para usar SHA de commit Git para criar uma espécie de versão, ou usar vendoring, que é um cache conhecido de dependências. Só que vendoring traz seus próprios problemas de gestão de cache
      No fim de semana precisei usar um ambiente virtual de Python e a experiência não terminou bem; isso me lembrou por que saí de Python
      O CPAN do Perl, o Maven/Gradle do Java, as gems do Ruby, dep/glide/vgo/modules do Go, o Cargo do Rust, npm/yarn do Node etc. todos têm problemas parecidos
      Até sistemas operacionais entram nisso, com yum/rpm da Redhat, apt do Debian, snap do Ubuntu e por aí vai. Especialmente o snap, que sinceramente não sei por quê
    • Não conheço bem Go, então fiquei curioso sobre qual seria, na biblioteca padrão de Go, o equivalente ao Tauri
      Dependendo do caso de uso, talvez faça sentido manter o frontend em Go e portar só o backend para Rust
  • Este texto parece estranho porque tenta ser ao mesmo tempo um guia de migração e um texto de defesa do Rust
    No fim, se você está decidindo entre Rust e Go, a questão central quase se resume a “você quer um runtime gerenciado?”. Uma geração inteira de programadores Rust convenceu a si mesma de que runtime gerenciado é ruim e que a ausência dele é um recurso importante
    Mas isso está claramente errado. Há mais áreas da programação que querem runtime gerenciado do que áreas que não querem
    Isso não significa que, nesses casos, Go deva ser sempre a escolha padrão. Também há muitos motivos subjetivos para preferir Rust. Quando uso Go, sinto falta de match, mas não sinto falta de tokio nem de Async Rust
    Ambos são escolhas legítimas em quase todos os casos em que você não precisa distorcer à força o espaço do problema. Por exemplo, seria estranho escrever um módulo do kernel Linux em Go
    A briga Rust vs Go parece uma fronteira estranha e constrangedora da nossa área. Uma parte enorme da indústria está construindo sistemas inteiros perfeitamente bem em Python ou Node, enquanto ri dos excêntricos discutindo qual linguagem compilada e estaticamente tipada usar. A pergunta real é Python vs Rust/Go, não Rust vs Go

    • Acho que usar Node com PureScript pode até ser aceitável
      Mas, de forma geral, o pessoal de Rust e Go deveria unir forças contra o mal da tipagem dinâmica. Se type hints agora são considerados boa prática, isso não é praticamente uma admissão de que havia um defeito ali?
      Mesmo type hints bons ainda ficam atrás de inferência de tipos. Inferência de tipos permite deixar muito código intacto ao mudar tipos, ao mesmo tempo em que evita mudanças de tipo não intencionais
    • O lado de Node adotou o TypeScript porque queria tipos compilados estaticamente
      Eu gostaria que TS tivesse um pouco mais de runtime. A única coisa que invejo em Python é que dá para fazer validação de schema JSON em endpoints HTTP de forma muito natural
      Todo o processo de passar por Zod continua sendo uma fonte constante de irritação, e acho que isso existe porque a equipe do TS é dogmática
  • Os sinais de escrita por LLM estão ficando cada vez mais sutis, mas ainda saltam aos olhos. Especialmente a palavra genuine
    Coisas como “This is the area where Go genuinely shines, and it’s worth being precise about why”, “the lack of GC pauses is a genuine selling point”, “Humans are genuinely bad at reasoning about memory”, “There are cases where the borrow checker is genuinely too strict”
    Não acho que o texto todo tenha sido gerado por IA, mas parece ter sido auxiliado por IA. Se foi isso, então o autor genuinely fez um bom trabalho
    Como ninguém mais está mencionando isso, parece que não prejudicou muito o conteúdo em si, mas acho estranho que isso esteja ficando cada vez mais comum e mais difícil de detectar

    • Concordo, mas não sei exatamente por quê. Não sei apontar com precisão o que faz algo soar como gerado por IA
      Quando cheguei em algo como “Go is clearly working for a lot of people,” já comecei a suspeitar de auxílio de IA. Claro que pode não ser isso, e eu posso não ser bom em identificar
      Mais do que um indício específico, ironicamente é uma sensação. Quando um texto “soa” como auxiliado por IA, eu perco o interesse quase na hora, mesmo que o texto em si esteja ok
      Queria que as pessoas ficassem mais à vontade para escrever diretamente do jeito que os próprios pensamentos surgem
    • Totalmente fora do tema, mas it's worth being precise about ... é uma expressão muito mais forte de texto com cara de IA do que o uso de genuine
    • Acho que o texto inteiro foi gerado por IA. O autor pode ter dado um rascunho como entrada e depois ajustado partes da saída
      Por exemplo, este parágrafo passa muito essa impressão: “Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions.”
      Toda frase diz alguma coisa, toda frase é importante e cumpre sua função. Esse tipo de escrita é mais o que se espera de um livro ou artigo muito especializado do que de um post de blog
      E por isso acaba tornando o texto mais difícil e mais chato de ler
    • No último ano, senti que a escrita de LLM tem uma tendência especialmente forte a falar de superfície e, em particular, de camada subjacente
      Não espero que texto gerado por LLM não venha cheio de expressões batidas. Só espero que todos nós mostremos um senso editorial melhor para não ficarmos lendo a mesma voz repetidas vezes
  • Se for um projeto novo, pode escrever em Rust sem problema
    Mas se já existe código, ele funciona e gera receita, então o certo é seguir em frente corrigindo, na linguagem original, só as partes que realmente precisem ser reescritas
    Melhore o sistema de forma pequena e mensurável com uma linguagem que a equipe conhece e em que confia. Fora disso, é debate religioso desperdiçador

    • Se a equipe já lançou com sucesso em C#/Java/Go etc. e trabalha confortavelmente com isso, não vejo motivo para usar Rust
  • Eu já gostava de Rust antes de rodar benchmarks, mas a diferença de eficiência com que a maioria dos LLMs escreve Rust e Go foi muito maior do que eu imaginava. Isso foi ainda mais claro em harnesses no estilo agente, que conseguem corrigir problemas iniciais de ambiente
    Depois de ver isso, virei um evangelizador de Rust bem convicto. Já tive bons resultados escrevendo em Rust ferramentas de processamento em lote para serem chamadas a partir de codebases existentes, mas ainda não tentei uma migração completa de produção
    Acho que os problemas de Go citados no texto, especialmente os ligados a nil, estão sendo cada vez mais contornados com code review minucioso via Codex. Melhor ainda seria se o problema nem existisse, mas para desenvolvedores que dedicam tanto esforço à revisão e compreensão quanto ao design e à implementação, esse tipo de bug de segurança está deixando de ser inevitável
    Os dados por linguagem estão em https://gertlabs.com/rankings?mode=agentic_coding

    • Graças a erros de compilador detalhados e a um sistema de tipos forte, é fácil para um agente lidar com o ciclo corrigir → compilar → corrigir
      Rust coloca o usuário com bastante firmeza nos trilhos. O Codex sempre consegue fazer alguma coisa compilar
      O lado ruim é que às vezes, em situações em que o certo seria falhar por não haver uma abordagem idiomática possível, ele acaba produzindo uma implementação burra que compila e atende ao pedido
    • Do ponto de vista de LLM, o ponto fraco de Rust é o tempo de compilação
      Como LLMs escrevem código mais rápido que humanos, o tempo de espera pela compilação pesa relativamente mais. Em projetos de algum porte, como acima de 100 mil linhas, o fato de Rust compilar cerca de 10 vezes mais devagar começa a virar gargalo
      Se você estiver escrevendo infraestrutura crítica, talvez valha pagar esse custo, mas se estiver construindo um serviço interno que não vai para a internet, a velocidade de desenvolvimento pode ser uma preocupação maior
      Também acho que compilação lenta afeta a velocidade de desenvolvimento humana, mas curiosamente os desenvolvedores raramente tentam quantificar isso
  • Se a principal barreira for a verbosidade, isto, previsto para o Go 1.28, pode reduzir bastante esse problema
    https://github.com/golang/go/issues/12854#issue-110104883

  • A expressão “um serviço do qual a organização depende, que precisa de alta disponibilidade e é crítico para o negócio” é curiosa
    Principalmente quando esse serviço em Rust roda em cima de Kubernetes

  • Já uso Rust e não tenho experiência com Go, então talvez esse texto não seja tão voltado para mim
    Mas há uma coisa que me incomoda. Dizer que corridas de dados em Rust são “capturadas em tempo de compilação” me parece, no mínimo, um pouco exagerado
    Essa formulação pode passar a impressão de que Rust também resolve coisas como starvation de lock ou outros problemas de concorrência. Na prática, não resolve
    Eu sei que corrida de dados é um termo formal com escopo restrito, mas ainda assim acho que isso poderia ser escrito com mais clareza