Rust é uma linguagem de programação que se tornou popular nos últimos anos. De acordo com a Stackoverflow survey de 2025, mesmo ela não sendo a mais usada para "extensive development work", que são os trabalhos mais longos (Rust está em décimo-quarto lugar), ainda é a linguagem com a qual mais pessoas querem trabalhar.
No passado, ela era conhecida por ter funcionalidades de uma linguagem que podia chegar a ser útil em vários domínios, mas nunca chegando neles; uma linguagem para ficar de olho, mas "not there yet" (como o repositório "are we Rust yet?" mostra nos vários domínios onde isso era verdade). Durante esse tempo, ela era pelo menos usável para um simples projeto-experimento. Nós não vamos entrar em detalhes; Vamos apenas focar em descrever essa primeira experiência de usar Rust.
Este texto usa nossa experiência com Rust como um exemplo concreto de como avaliar e escolher uma tecnologia para um projeto. O processo que seguimos — levantar requisitos claros, comparar alternativas com base neles, e validar a escolha com experimentos reais — pode ser aplicado a qualquer decisão tecnológica. Cada equipe trabalhará melhor com ferramentas diferentes, e cada projeto tem seus próprios requisitos que não podemos atender ou prever aqui. Não baseie sua decisão somente na nossa experiência inicial.
O Projeto e os Critérios de Escolha
O ano era 2017. O objetivo era criar uma implementação "simples" de um novo modelo estatístico que não era muito conhecido mas estava ganhando atenção, pois tinha propriedades interessantes quando aplicado a problemas em que outros modelos estatísticos (e modelos de aprendizagem de máquina) tinham sido usados. Se fosse qualquer outro modelo mais conhecido, poderíamos confiar em uma implementação altamente otimizada em SkLearn, ou outra biblioteca de aprendizagem de máquina, mas não havia nada do tipo ainda. No final, escolhemos usar Python para fazer o protótipo, mas não para a implementação final.
Resumidamente, precisávamos de uma implementação melhor, mais rápida, e mais eficiente no custo de memória. O primeiro passo foi definir critérios claros antes de avaliar qualquer linguagem. Isso nos levou ao sempre-presente problema de escolher uma boa linguagem de programação para o projeto. Ela tinha de ser
Definir esses critérios explicitamente — mesmo que apenas em retrospecto — é o que torna uma escolha tecnológica defensável. Nós não os listamos formalmente antes de escolher a linguagem, mas percebemos depois que eles se tornaram as razões mais importantes de escolher Rust. As outras escolhas eram as mais frequentes por serem rápidas e modernas: C, C++, e Go (com Julia aparecendo depois, depois que o projeto já havia começado, e acabou como uma alternativa interessante).
O segundo passo foi eliminar candidatos com base em experiência anterior documentada. C e C++ foram descartados no início por causa de experiências passadas. Especificamente, problemas com ponteiros mutáveis, não saber o que causou esses problemas, e perder alguns dias para descobrir um pequeno erro feito há muito tempo atrás por outra pessoa. Queríamos evitar esse tipo de problema a qualquer custo.
Com Go, existia uma vantagem incomum: uma biblioteca parcialmente desenvolvida, naquela época, tinha o suficiente para começar a trabalhar com esse modelo estatístico. Mas ter uma biblioteca disponível não é critério suficiente para escolher uma tecnologia. Enquanto discutíamos com a equipe, era difícil explicar partes do código que pareciam ser excessivamente complexas em Go. Por essa razão, nossa avaliação irá se focar na legibilidade dessas linguagens.
Um pequeno exemplo
Go tinha boas ferramentas para concorrência, que eram necessárias para o nosso projeto, mas Rust também tinha. Por exemplo, Go podia facilmente fazer uma função concorrente usando Goroutines:
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
E se você quiser evitar a criação explícita de threads, controlar interrupções, etc. Isso é ótimo. Um código similar em Rust seria algo como:
fn say(s: &str) {
for _ in 0..5 {
thread::sleep(Duration::from_millis(100));
println!("{}", s);
}
}
fn main() {
thread::spawn(|| {
say("world");
});
say("hello");
}
Bem parecido, não? Também é simples. E bom o suficiente para começar nossas comparações, e descrever porque a experiência com Rust foi mais desejável.
Aqui está um ponto importante da avaliação: o código em Go cria uma thread? Nem sempre. Ele cria uma lightweight thread, e nós confiamos no executor de Go com isso. O código em Rust cria uma thread? Sim. Nós explicitamente criamos uma thread, e com o que podemos ler no código, nós sempre sabemos se criamos. Ele pode ser um pouco mais lento, ou usar um pouco mais de memória, mas esse nível de controle, explícito e legível, é um aspecto importante para o projeto. Isso ilustra como um mesmo critério — legibilidade — pode se manifestar de formas diferentes dependendo da linguagem, e por que comparar exemplos reais de código é mais útil do que comparar descrições abstratas.
O loop no exemplo em Go usa um índice, enquanto que o de Rust usa uma Range e um iterador, o que significa que temos Ranges nessa linguagem, e um iterador que percorre essa Range, e que são fáceis de ler e entender.
Nós também temos Strings em ambos, mas a função em Rust recebe um valor "&str". O que é isso? Isso é legível? Um tipo chamado "String" é entendível, mas isso é diferente. Para ser mais preciso, "&str" é uma "String slice", e vamos usar isso para descrever melhor esse exemplo (e padrão) de legibilidade.
Lendo as coisas simples
Tanto Go quanto Rust têm o mecanismo de pass-by-value. No nosso caso isso significa que, quando você tem uma String como um argumento para uma função, o valor da String é copiado, e a função recebe a sua própria, nova String para fazer o que precisar. Mas o código em Rust não usa uma String, não é? Se usasse uma String, teríamos algo como:
fn say(s: String) {
Que é um código válido em Rust. Mas o que está acontecendo aqui? Se chamarmos essa função, estamos enviando uma String como argumento, e o valor está sendo copiado, assim como em Go. Lembrando que Strings são compostas de um ponteiro para alguns bytes, e alguns dados complementares, o que não é muito para copiar. A função recebe uma String nova, copiada, e pode fazer quase tudo com isso... exceto algumas coisas como modificá-la.
Imutabilidade é uma boa funcionalidade de se ter quando você já teve experiência com problemas em que um valor foi modificado quando não deveria ter sido (especialmente se esse valor é um ponteiro). Variáveis e parâmetros em Rust são imutáveis por padrão. Strings são imutáveis em Go, o que significa que a função não pode mudar a String de qualquer jeito. Você acha que isso é uma característica apenas de Strings em Go, ou isso se aplica a outras estruturas? Que tal isso:
fn say(mut s: String) {
Nós temos mut, uma palavra reservada para variáveis e parâmetros mutáveis em Rust. Você acha que essa String é imutável? Você acha que essa característica se aplica a outras estruturas, ou apenas Strings? No caso de Rust, fica claro que essa é uma característica da linguagem que pode ser aplicada a qualquer parâmetro ou variável. Se não fosse explicitamente escrito, a variável ou parâmetro é constante (imutável), que é o oposto de Go, onde você deve declarar constantes com a palavra reservada "const"... exceto para Strings. Consegue ver a diferença?
Agora, e a string slice que vimos antes? Um "slice" é uma referência para uma sequência de elementos. No exemplo, era uma referência para uma sequência de caracteres. Também não é mutável (não tem a palavra reservada mut), e isso é suficiente para a função, já que ela não precisa dos outros dados de uma String, e também não a modifica.
Mas e se quiséssemos uma String própria, mutável, ao invés de um slice? Uma String própria pode ser criada com a linha:
let mut the_string = String::from("This is the literal string");
Isso é uma inicialização explícita e direta de uma String, usando um valor string literal como argumento. Com Rust, essas inicializações não são implícitas (pelo menos, nessa primeira experiência com Rust), o que te ajuda a dizer, com confiança, o que os valores que você está usando são. Isso é uma diferença pequena com o que Go faz com string literals nos exemplos anteriores, mas uma diferença grande com o que C e C++ fazem com funções que aceitam Strings (ou outros parâmetros que podem ser implicitamente convertidos).
Ser explícito com inicialização de estruturas, mutabilidade de variáveis, e tipos diferentes mas compatíveis foram as primeiras coisas que Rust fez bem e a experiência inicial foi positiva, mas precisou de um período de aprendizado. Não foi mais simples que Go, e deu a impressão de ser uma linguagem que requer experiência prévia com outras linguagens para ser entendida em seus próprios termos; para mostrar que existe uma razão para essas funcionalidades existirem.
Rust ganhou alguns pontos como nossa escolha, mas a próxima característica é a mais conhecida de Rust, e aquela que garantiu nossa decisão.
Checando borrows
Lembra dos problemas com ponteiros mencionados anteriormente para C/C++? Tanto Rust quanto Go têm sua maneira de resolvê-los. Em Go, o compilador faz uma análise estática e aloca as variáveis no stack ou no heap, como precisar. Se existe, por exemplo, uma função que retorna um ponteiro para uma variável local:
func dangling() *int{
local_int := 20
return &local_int
}
Ele será válido, dado um dos mecanismos mágicos do compilador de Go chamado de "Escape Analysis". É um ponteiro para um inteiro que não será removido pelo coletor de lixo, até que isso seja necessário. Contudo, isso não é válido para Rust.
fn dangling() -> &i32 {
let local_int = 20;
return &local_int;
}
E é importante entender que isso não é apenas uma diferença de funcionalidade, mas uma diferença em estrutura e organização de código. Em Rust, essa característica é chamada de "Ownership", e é a maneira do compilador prevenir não apenas o problema de dangling pointers, mas também o use-after-free. De acordo com o Rust book, é um conjunto de regras que governa como um programa gerencia sua memória, sem sacrificar velocidade de execução para um coletor de lixo.
O livro explica isso com três regras simples:
O que é simples o suficiente para entender. Por exemplo, se você declara um valor usando "let x =", você é o dono daquele valor. Se ele for enviado para uma função como argumento, a função agora é dona do valor. Quando a função acaba, o valor é removido.
Desde que não seja uma cópia de um valor, nós podemos entender quando o dono de um valor muda. Seria bom, mas trabalhoso, ter sempre explícito se (e quando) um valor é copiado, mas Rust tem uma maneira conveniente de discernir isso: se o tipo do valor implementa a Trait "Copy", ele será implicitamente copiado. Para alguns tipos na biblioteca padrão, como o tipo String dos exemplos anteriores, é fácil de verificar, e se precisarmos disso nas nossas próprias estruturas, teremos que implementar o Trait. E já que algumas estruturas podem ser muito grandes para se deixar copiar sem impactar o tempo de execução, essa flexibilidade foi bem-vinda.
Adicionalmente, referências compartilhadas (que são apenas as imutáveis em Rust) podem ser copiadas. Por exemplo, vimos que usar uma estrutura grande como argumento de uma função irá fazer da função seu dono, mas nós podemos nos manter donos desse valor usando uma referência como parâmetro da função, como usamos string slice nos exemplos anteriores. E, para evitar todos os problemas que podem aparecer quando usamos referências, Rust tem algumas regras simples:
Essas regras são fáceis de entender, mas também fáceis de quebrar por acidente, mas, receber uma mensagem irritada do compilador ao invés de passar por uma longa dor de cabeça consertando o problema, concluiu nossa escolha. Não apenas essas regras e restrições previnem erros de referências e ponteiros, mas eles também forçam uma maneira diferente de pensar sobre a estrutura do problema para evitar problemas similares.
Você não pode assumir que toda variável com a qual você está lidando é mutável e pode ser passada entre funções sem problemas, e nem que você pode ter uma referência mutável no contexto e ainda permitir que outras partes do seu programa leiam e modifiquem ela. E o melhor de tudo é que essa referência sempre estará lá, válida, e não será dereferenciada (ou anulada) sem você explicitamente fazer isso e entender os riscos. Data Races são prevenidos, então você é livre para criar threads e mover os valores onde eles precisam ser movidos, mantendo apenas algumas referências mutáveis para evitar mudanças não-intencionais.
Conclusão
Essa foi a primeira parte da nossa experiência com Rust, quando vimos o que a linguagem oferecia, o quão fácil era de aprender, e como programar a solução que precisávamos na época. Foi mais difícil do que o esperado, e a linguagem, mesmo sendo simples de explicar, tem uma curva de aprendizado acentuada para aqueles que não são familiarizados com programação segura para memória e concorrência.
O que este exemplo ilustra sobre escolha de tecnologia vai além de Rust. O processo que seguimos — mesmo que de forma não totalmente consciente na época — envolveu definir critérios concretos ligados ao projeto, eliminar candidatos com base em experiência real e não apenas reputação, comparar alternativas com código concreto, e validar a escolha pelas características que mais importavam para a equipe. Ter uma biblioteca pronta em Go poderia ter sido argumento suficiente para uma decisão apressada, mas os critérios de legibilidade e confiabilidade revelaram que aquela conveniência inicial teria um custo mais tarde.
O prefácio no livro de Rust enfatiza que Rust é fundamentalmente sobre empoderamento. Mesmo quando lidando com uma base de código complexa, e com referências, threads, e gargalos de performance, a linguagem deve te fazer sentir empoderado para desenvolver o que você quiser. Você pode checar a implementação, ver conversões e cópias explícitas (e as implícitas que você escolheu deixar assim), assim como tentar novas abordagens para o seu problema sem medo de chegar a problemas com ponteiros, referências, ou conflitos de concorrência.
Para o nosso experimento, a conclusão desse primeiro passo foi positiva, e, pelo menos por agora, parece que os outros programadores também gostaram (e querem mais) da sua experiência com Rust. E, independente de qual tecnologia você escolher no seu próximo projeto, o mais importante é que a escolha seja guiada por critérios claros — não por familiaridade, conveniência ou hype.