Migrando de Go para Rust
(corrode.dev)- 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,ResulteSend/Sync - O borrow checker de Rust e
async/awaitcriam 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
nile 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
cargooferece, como ferramenta principal, um conjunto mais amplo de funções correspondentes às ferramentas de Gogo.mod/go.sum→Cargo.toml/Cargo.lock: configuração do projeto e manifesto de dependênciasgo get/go mod tidy→cargo add/cargo update: adição e resolução de dependênciasgo build→cargo build: compilaçãogo run .→cargo run: executar após compilargo test ./...→cargo test: testesgo vet ./...→cargo clippy: lint, sendo queClippyé muito mais opinativo quevetgofmt/goimports→cargo fmt: formatador automático sem configuraçãogolangci-lint run→cargo clippy -- -D warnings: modo de lint rigorosogo doc→cargo doc --open: geração e visualização de documentação de APIpprof→cargo flamegraph/samply: profiling de CPUgovulncheck→cargo audit: verificação de vulnerabilidades baseada em banco de dados de avisos
- Em Go, muitas vezes ferramentas de terceiros como
golangci-lint,mockgen,airegoreleasersã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 decargo watchecargo nextest, e depois funcionam como ferramentas nativas, como emcargo nextest gofmterustfmttê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,
nilexiste amplamente; em Rust, não há nulo, eOption<T>é o substituto no nível de tipo - Tratamento de erros: Go usa a interface
erroreif err != nil { ... }; Rust usaResult<T, E>, o operador?e pattern matching completo - Concorrência: Go usa CSP com goroutines e channels; Rust usa
async/await, channels e threads sobretokio - Cancelamento: em Go,
context.Contexté baseado em convenção; em Rust, a passagem é explícita e verificada por tipos, com recursos comoCancellationToken - Corridas de dados: Go detecta de forma probabilística em runtime com
-race; Rust detecta em tempo de compilação comSend/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/Synce 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
nile 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 comomap[T]struct{} -
Pânicos de
nilem 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,
Findretorna(*User, error)e, em caso de “not found”,errorénil, mas a verificação deusercontinua sendo responsabilidade do chamador user.Account.Notify()pode travar quandouserouAccountfornil- Linters como
nilawayestaticcheck, 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 casoNone
- 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
-
Data races que o
-racenão detectougo 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
SendeSync, e tentar compartilhar umHashMapcomum 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 comfmt.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
errcheckegolangci-lintcapturam a maioria das omissões no tratamento de erros, e que oif err != nilexplí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 viathiserror::Errore#[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
matchprecisam ser atualizados
- O
-
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{}/anye 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 != nilvsResult<T, E>- Em Go, após
os.ReadFile(path)ejson.Unmarshal, retorna-se um erro com contexto usandoif err != nil - Em Rust, isso é composto por
fs::read_to_string(path)?,serde_json::from_str(&data)?eOk(cfg) - O operador
?substitui o padrãoif err != nil { return err }e, seFrom<E1> for E2estiver implementado, também faz a conversão de tipo #[from]dethiserrordá suporte idiomático a essa conversão
- Em Go, após
-
Nulo:
nilvsOption<T>- Em Go,
GetUser(id string) *Userretornanilquando não encontra o usuário, e se o chamador fizerfmt.Println(u.Name), ocorrerá panic quando fornil - Em Rust,
get_user(id: &str) -> Option<User>retornaSome(User)ouNone let user = get_user("123"); println!("{}", user.name);gera erro de compilação porqueusernão éUser, e simOption<User>- É preciso tratar tanto
Some(u)quantoNonecommatch get_user("123") - Em Rust seguro, não existe
nil, e referências não podem ser nulas
- Em Go,
-
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
grepimplementaçõ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>ouArc<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/awaitsobre o executortokio - Funções async retornam
Futuree não executam até serem aguardadas com await ou lançadas com spawn - O compilador rastreia
Send/Syncantes e depois dos pontos de.await, e gera erro de compilação se um valor non-Sendfor 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_blockingourayon
- O modelo de concorrência de Go é simples, como em
-
context.ContextvsCancellationToken- Em Go,
context.Contexté passado para toda chamada bloqueante - Em Rust, não há um
context.Contextembutido, 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.Contextpropagado por convenção; em Rust, é umCancellationTokenou canalwatch, mas o compilador pode avisar quando isso foi omitido
- Em Go,
-
Strings:
stringvsStringe&str- O
stringde 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âmetrostringde Go
- A regra prática é receber
&strem argumentos e retornarStringao criar novos dados - A separação entre
&streStringmostra de forma reduzida o modelo de “borrow vs own” de Rust
- O
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.Sliceainda recebe uma closurefunc(i, j int) boolem vez de uma constraintcmp.Orderedsync.Mapainda é tipado comoany/any- Os helpers genéricos existentes se limitam a poucos pacotes, como alguns itens em
slices,maps,cmpesync - 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 + PartialOrddo Rust, associated types comotype Item;doIterator, nem blanket impls comoimpl<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
Tcom algumas operações” para algo além disso, o Go acaba voltando paraany, 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>eVec<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
- Rust propaga informações de tipo por toda a expression, incluindo closures, cadeias de iterators e o operador
-
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,reflectnem os padrões de geração de código dominantes em ORMs, decoders e mocks encoding/jsonainda usa reflection,database/sqlainda usaany, emockgenainda 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
- Em Rust, genéricos e traits eliminam a maior parte das situações em que seria necessário
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→ Rustaxumsobrehyper - Cliente HTTP: Go
net/http,resty→ Rustreqwest - gRPC: Go
google.golang.org/grpc+protoc-gen-go→ Rusttonic+prost - SQL: Go
database/sql,sqlc,sqlx,gorm→ Rustsqlx,sea-orm,diesel - Migrations: Go
golang-migrate,goose→ Rustsqlx migrate,refinery - JSON: Go
encoding/json,sonic,goccy/go-json→ Rustserde+serde_json - Logging: Go
log/slog,zerolog,zap→ Rusttracing+tracing-subscriber - Métricas: Go
prometheus/client_golang→ Rustmetrics+metrics-exporter-prometheus - Configuração: Go
viper,koanf→ Rustconfig/ config-rs,figment - CLI: Go
cobra,urfave/cli→ Rustclapderive - Erros: Go
errors,pkg/errors→ Rustthiserrorpara bibliotecas,anyhowpara 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, emockalltambém é usado - Tarefas em background: Go goroutines +
errgroup→ Rusttokio::spawn+JoinSet
- Servidor HTTP: Go
- Em um serviço de backend típico, a combinação
axum+sqlx+tokio+tracing+serde+clapcobre 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
*Usertirado 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,ouroborosou um redesenho - Compartilhar estado mutável entre goroutines: o padrão
mu sync.Mutex; data map[K]Vde Go vira algo comoArc<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
- Referências de longa duração: em Go, é natural manter por bastante tempo um
- 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 checksã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 fnefnem 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-traitpara mascarar essas partes
- A separação entre
-
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::spawnquando isso for realmente necessário axumjá 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 -racee 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
- Tipos de bug como data races, desreferência de nil e caminhos de erro ausentes, que passam em
- Uso de CPU: redução de 20~60%
- 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
stringem 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&strpara uma visão somente leitura, e Go[]byte↔ RustVec<u8>para buffers mutáveis Stringem 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 loopsforem 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
nile 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
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 dissoRust não tem um tipo de erro unificado, e há sistemas de erro importantes como
io::Error,thiserroreanyhow, o que torna incômodo propagar erros para cima na cadeia de chamadasHá 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
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
ErrorO que foi listado são apenas formas comuns de usá-lo, e usar só
Boxtambém funciona perfeitamente. Isso é em grande parte parecido com o queanyhow::ErrorfazDito isso, acho que Go se saiu muito melhor que Rust no lado da biblioteca padrão
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
Errorio::Erroré apenas um dos vários tipos que o implementam, nada de especial. Erros definidos comthiserrortambém implementam esse traitanyhowapenas 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 emitirRust 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) etauri(GUI), e parece que já passaram de 400 dependências.tauriem especial é o principal culpado; mesmo sem ele ainda ficam quase 100, o que parece insanoSeria 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
cargoem algo como onpmnesse aspectoPor 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
E então sai a
httplib3, depois ahttplib4Em 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
rusqlite,clap,ratatuietauriTambé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
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ê
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 detokionem de Async RustAmbos 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
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
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
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
it's worth being precise about ...é uma expressão muito mais forte de texto com cara de IA do que o uso de genuinePor 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
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
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ávelOs dados por linguagem estão em https://gertlabs.com/rankings?mode=agentic_coding
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
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