Rust para desenvolvedores Go: guia prático
Se você escreve Go, vai se sentir em casa no Rust mais do que imagina. As duas linguagens compilam para um único binário rápido. As duas têm structs com métodos no lugar de classes, não têm herança, devolvem erros como valores em vez de usar exceções, têm um formatador padrão que todo mundo usa e uma ferramenta de linha de comando que compila, testa e baixa pacotes. Uma struct do Go com alguns métodos vira Rust quase linha por linha.
O que é novo tem a ver, principalmente, com memória e com ser explícito. O Rust não tem garbage collector (coletor de lixo): cada valor tem um único dono, e o compilador verifica o ownership (posse) e o borrowing (empréstimo) antes de o programa rodar. Não existe nil (você usa Option), e o if err != nil vira Result e o operador ?. As interfaces continuam existindo (elas se chamam traits), mas um tipo precisa dizer com todas as letras que implementa uma. Conte com passar a maior parte do seu tempo de estudo no ownership. O resto vai parecer um Go mais rigoroso e mais detalhado.
Por que desenvolvedores Go aprendem Rust? Normalmente por um destes motivos: um serviço em que as pausas do garbage collector ou o uso de memória começam a pesar; trabalho pesado de CPU; código que roda sem sistema operacional (embarcado) ou no navegador (WebAssembly); ou simplesmente para o compilador pegar data races e quebras do tipo nil antes da produção. Para uma comparação mais ampla do Rust com Go, C++ e Python, veja Rust vs outras linguagens. Esta página vai mais fundo e foi escrita para você, que programa em Go.
Conceitos da linguagem: do Go para o Rust
Seção intitulada “Conceitos da linguagem: do Go para o Rust”Use esta tabela como um guia de conversação. A última coluna leva à lição deste site que explica o lado do Rust.
| Go | Rust | Saiba mais |
|---|---|---|
| Garbage collector | Ownership e borrowing, verificados em tempo de compilação; a memória é liberada quando o dono sai de escopo | Ownership, Borrowing |
struct + métodos |
struct para os dados + um bloco impl para os métodos |
Structs |
Receiver por valor func (a Account) |
&self (lê, sem cópia) ou self (toma o ownership) |
Structs |
Receiver por ponteiro func (a *Account) |
&mut self (pode alterar o valor) |
Structs |
Função construtora NewAccount(...) |
Uma função associada, por convenção Account::new(...) |
Structs |
| Letra maiúscula = exportado | A palavra-chave pub |
Módulos |
Zero values (var c Config) |
Não há zero values implícitos; todo campo precisa ser preenchido. #[derive(Default)] + Config::default() |
Structs, Traits comuns |
| Embedding de structs | Não existe embedding. Coloque a struct num campo e escreva pequenos métodos que repassam a chamada, ou compartilhe comportamento com traits | Traits, Padrões de projeto |
interface (satisfeita implicitamente) |
trait (implementado explicitamente com impl Trait for Type) |
Traits |
[]Shape com tipos diferentes |
Vec<Box<dyn Shape>> (um trait object) |
Trait objects |
any / interface{} + type switch |
Um enum com uma variante por caso, e depois match |
Enums, Match |
Genéricos func Max[T cmp.Ordered](a, b T) T |
Genéricos com trait bounds: fn max<T: PartialOrd>(a: T, b: T) -> T |
Genéricos, Genéricos avançados |
Ponteiro nil, map nil, interface nil |
Option<T>: ou Some(valor) ou None |
Option |
v, ok := m[k] |
m.get(&k) devolve um Option |
HashMap |
(T, error) + if err != nil { return err } |
Result<T, E> + o operador ? |
Result |
errors.New, sentinel errors, errors.Is / errors.As |
Um enum de erro sobre o qual você faz match (o crate thiserror ajuda) |
Erros personalizados, anyhow e thiserror |
fmt.Errorf("...: %w", err) |
anyhow com .context("..."), ou map_err |
anyhow e thiserror |
panic / recover |
panic! para bugs de verdade; recuperar é raro (std::panic::catch_unwind) |
Panic |
defer |
Drop, que roda quando um valor sai de escopo (RAII) |
Escopo, Ponteiros inteligentes |
go f() |
std::thread::spawn(...), ou tokio::spawn(...) para tarefas async |
Concorrência, Async/Await |
chan T, make(chan T, n) |
std::sync::mpsc::channel() (sem limite) ou sync_channel(n); tokio::sync::mpsc em código async |
Canais (channels) |
select |
tokio::select! em código async, ou o select! do crossbeam-channel |
Async a fundo |
sync.Mutex ao lado dos dados |
O Mutex<T> envolve os dados; ele destrava sozinho quando o guard é descartado |
Estado compartilhado |
sync.WaitGroup |
JoinHandle::join(), ou std::thread::scope |
Concorrência |
sync.Once |
std::sync::OnceLock / LazyLock |
Truques da biblioteca padrão |
context.Context |
Não há um equivalente único: descarte (drop) um future para cancelá-lo, tokio::time::timeout, ou o CancellationToken do tokio-util |
Async a fundo |
Valores func e closures |
Closures, descritas pelos traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut e FnOnce |
for i, v := range s |
for (i, v) in s.iter().enumerate() |
For em Rust, Iteradores |
O único laço for |
loop, while e for |
Loops em Rust |
Iteradores range-over-func (iter.Seq) |
O trait Iterator, com adaptadores como filter, map, sum, collect |
Iteradores, Iteradores personalizados |
switch |
match (o compilador confere se você tratou todos os casos) |
Match, Pattern matching |
const + iota |
Um enum (opcionalmente com números explícitos) |
Enums |
:= |
let (imutável por padrão; let mut para permitir mudanças) |
Variáveis |
string (bytes, normalmente UTF-8) |
String (com dono) e &str (emprestada), sempre UTF-8 válido |
Strings |
rune, for _, r := range s |
char, for c in s.chars() |
Strings |
[]byte |
Vec<u8> ou &[u8] |
Slices |
fmt.Println, %v, %+v |
println!, {} (Display), {:?} e {:#?} (Debug, com #[derive(Debug)]) |
Saída em Rust |
String() string (a interface Stringer) |
Implemente Display (e o .to_string() vem de graça) |
Traits comuns |
Struct tags `json:"user_name"` |
Atributos: #[serde(rename = "user_name")] |
JSON com Serde |
Conversões int(x), float64(n) |
as para números, From/Into, e TryFrom quando a conversão pode falhar |
Conversão de tipos, Conversões |
| O overflow de inteiros dá a volta em silêncio | Panic nos builds de debug, dá a volta em release; checked_add, wrapping_add e saturating_add dizem o que você quer |
Overflow de inteiros |
*T e &x |
Referências &T e &mut T (nunca nulas, não podem viver mais que o valor); Box<T> para um valor no heap com dono |
Borrowing, Ponteiros inteligentes |
| Pacotes | Módulos (mod) dentro de um crate; use no lugar de import |
Módulos |
go generate |
Macros e scripts de build build.rs |
Macros, Macros procedurais |
Pacote unsafe |
Blocos unsafe e ponteiros brutos (raw pointers) |
Unsafe em Rust |
| cgo | extern "C" e FFI (o bindgen gera bindings a partir de headers C) |
FFI: chamando C |
Build tags //go:build linux |
#[cfg(target_os = "linux")] e features do Cargo |
Features do Cargo e cfg |
Coleções
Seção intitulada “Coleções”O Go tem slices e maps embutidos na linguagem. O Rust os tem na biblioteca padrão, e mais alguns.
| Go | Rust | Saiba mais |
|---|---|---|
[]T que é seu e recebe append |
Vec<T> |
Vetores |
[]T passado para uma função que só lê |
&[T] (um slice emprestado de um Vec ou de um array) |
Slices |
[N]T |
[T; N] |
Arrays |
append(s, x) / append(s, other...) |
v.push(x) / v.extend(other) |
Vetores |
s[a:b] |
&v[a..b] |
Slices |
len(s), cap(s), make([]T, 0, n) |
v.len(), v.capacity(), Vec::with_capacity(n) |
Vetores |
map[K]V |
HashMap<K, V> |
HashMap |
map[K]struct{} ou map[K]bool usado como conjunto |
HashSet<T> |
HashSet |
| Juntar e ordenar as chaves do map antes de imprimir | BTreeMap<K, V> (as chaves ficam ordenadas) |
HashMap |
container/list |
VecDeque<T> (push_back / pop_front) |
VecDeque e BinaryHeap |
container/heap (implementar 5 métodos) |
BinaryHeap<T>: só push e pop; o maior primeiro, envolva em Reverse para o menor primeiro |
VecDeque e BinaryHeap |
slices.Sort, sort.Slice |
v.sort(), v.sort_by(...), v.sort_by_key(...) |
Ordenação e busca |
slices.Contains, slices.Index |
v.contains(&x), v.iter().position(...) |
Vetores |
strings.Builder |
Uma String com push_str, ou write! |
Strings |
sync.Map |
Mutex<HashMap<K, V>>, RwLock, ou o crate dashmap |
Estado compartilhado |
Duas coisas para saber. Primeiro, assim como os maps do Go, um HashMap do Rust não tem ordem fixa, então ordene antes de imprimir ou use um BTreeMap. Segundo, a famosa surpresa dos slices do Go (dois slices dividindo o mesmo array por baixo, e um append alterando o outro) não tem como acontecer no Rust: enquanto uma parte do seu código tem um empréstimo &mut de um Vec, mais ninguém pode olhar para ele.
Ferramentas: do go para o cargo
Seção intitulada “Ferramentas: do go para o cargo”Boa notícia: você está acostumado com uma ferramenta que faz tudo, e o Rust também tem uma. O Cargo cobre go build, go run, go test, go mod e go install.
| Go | Rust | Observações |
|---|---|---|
Comando go |
cargo |
Compila, executa, testa, adiciona pacotes, publica. Veja Cargo e crates |
go mod init |
cargo new my_app (cargo new --lib para uma biblioteca) |
Cria a pasta, o Cargo.toml, o src/main.rs e um repositório git |
go.mod |
Cargo.toml |
|
go.sum |
Cargo.lock |
Criado e atualizado para você |
go get github.com/gin-gonic/gin |
cargo add axum |
|
go get -u |
cargo update |
Atualiza dentro das faixas de versão do Cargo.toml |
| pkg.go.dev | crates.io para os pacotes, docs.rs para a documentação da API | |
go build / go run / go test |
cargo build / cargo run / cargo test |
Acrescente --release para um build otimizado; builds de debug são bem mais lentos na execução |
go vet, staticcheck, golangci-lint |
cargo clippy |
Oficial, com centenas de lints úteis |
gofmt / goimports |
cargo fmt (rustfmt) |
Um estilo padrão, igualzinho ao gofmt |
go doc |
cargo doc --open |
Os exemplos de código nos comentários de documentação rodam como testes. Veja Documentação |
go test -bench |
criterion (rodado com cargo bench) |
Veja Desempenho |
go test -cover |
cargo llvm-cov ou cargo tarpaulin |
|
go test -race |
Não é preciso em código seguro: data races são erros de compilação. O Miri verifica código unsafe |
|
go test -fuzz |
cargo fuzz |
Instale com cargo install cargo-fuzz |
| testify | assert! e assert_eq! embutidos; pretty_assertions para diffs mais bonitos |
Veja Testes em Rust e Testes avançados |
| gomock / mockery | mockall, ou fakes escritos à mão que implementam o seu trait |
|
go install pkg@latest |
cargo install name |
|
go work (workspaces) |
Um workspace do Cargo | Um Cargo.toml no topo lista os crates membros |
Linha toolchain no go.mod |
rustup e um arquivo rust-toolchain.toml |
Veja Ferramentas do Rust |
GOOS=linux GOARCH=arm64 go build |
rustup target add aarch64-unknown-linux-gnu + cargo build --target ... |
Muitas vezes precisa de um linker para o alvo; o cross ou o cargo-zigbuild facilitam |
| govulncheck | cargo audit (avisos do RustSec), cargo deny |
|
| gopls | rust-analyzer | Funciona no VS Code, Neovim, Helix, Zed e outros |
| GoLand | JetBrains RustRover | |
| Delve | rust-gdb / rust-lldb, ou o CodeLLDB no VS Code |
|
| pprof | cargo flamegraph, perf, ou qualquer profiler nativo |
|
| air (recarregamento automático) | bacon ou cargo watch |
Um Cargo.toml pequeno ao lado do go.mod que você conhece:
module example.com/orders-api
go 1.23
require github.com/gin-gonic/gin v1.10.0[package]name = "orders-api"version = "0.1.0"edition = "2021"
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }Uma diferença que você vai notar logo: a biblioteca padrão do Go inclui servidor HTTP, JSON, templates e criptografia. A biblioteca padrão do Rust é menor de propósito, então coisas como JSON, HTTP, async e números aleatórios vêm de crates. Adicionar um é um único cargo add, e um punhado de crates (serde, tokio, clap, regex, rand) é usado por quase todo mundo.
Bibliotecas e frameworks: “O que usamos no lugar de…?”
Seção intitulada “Bibliotecas e frameworks: “O que usamos no lugar de…?””Essa é a primeira pergunta prática. “A gente usa Gin e GORM. Qual é a versão em Rust?” Aqui estão os equivalentes mais próximos. Como no Go, o Rust normalmente tem duas ou três boas opções da comunidade em vez de uma resposta oficial.
Web e rede
Seção intitulada “Web e rede”| Go | Rust | Observações |
|---|---|---|
| Gin, Echo, Chi, Fiber | axum (construímos uma API com ele em Projeto: API web com Axum), actix-web, Rocket | axum e actix-web são muito usados em produção |
Servidor do net/http |
hyper (baixo nível), normalmente usado através do axum |
Não há servidor HTTP na biblioteca padrão |
Middleware de http.Handler |
Camadas do tower e o tower-http (CORS, compressão, tracing, timeouts) |
O axum usa o tower diretamente |
Cliente do net/http |
reqwest |
Assíncrono por padrão, com um cliente bloqueante opcional. Veja Redes em Rust |
| grpc-go | tonic (com prost para protobuf) |
|
| gorilla/websocket | tokio-tungstenite, ou a feature ws do axum |
|
html/template, text/template |
askama (verificado em tempo de compilação), tera, minijinja |
|
net (TCP/UDP) |
std::net, ou tokio::net para código async |
Veja Redes em Rust |
| errgroup | tokio::task::JoinSet |
| Go | Rust | Observações |
|---|---|---|
encoding/json + struct tags |
serde + serde_json |
Também lida com TOML (crate toml) e outros formatos. Veja JSON com Serde |
database/sql + pgx |
sqlx (assíncrono, SQL puro conferido com o seu banco de dados em tempo de compilação), tokio-postgres |
Não tem relação com o jmoiron/sqlx do Go, apesar do nome |
| sqlc | As macros query! do sqlx |
Os dois partem do SQL que você escreve e devolvem resultados tipados |
| GORM | SeaORM (assíncrono, no estilo de entidades), Diesel (query builder com tipos verificados) | Veja Bancos de dados |
| go-sqlite3 | rusqlite |
|
| go-redis | redis |
CLI, configuração, logging e ferramentas do dia a dia
Seção intitulada “CLI, configuração, logging e ferramentas do dia a dia”| Go | Rust | Observações |
|---|---|---|
cobra, urfave/cli, flag |
clap (os argumentos saem de uma struct, com derive) |
Veja Argumentos de linha de comando e Projeto: CLI com clap |
| viper | config ou figment |
Combine arquivos e variáveis de ambiente em camadas e depois desserialize com o serde |
log/slog, zap, zerolog |
tracing + tracing-subscriber, ou log + env_logger |
Logging estruturado. Veja Logging |
regexp |
regex |
A mesma ideia do RE2 do Go: casamento em tempo linear, sem backreferences. Veja Expressões regulares |
time |
std::time para durações; chrono ou jiff para datas e fusos horários |
Veja Datas e horas |
embed |
include_str! / include_bytes!, ou rust-embed para pastas inteiras |
|
| bubbletea | ratatui |
Interfaces no terminal |
| client_golang do Prometheus | prometheus ou metrics |
|
sync/atomic |
std::sync::atomic |
Desktop, jogos e embarcados
Seção intitulada “Desktop, jogos e embarcados”| Go | Rust | Observações |
|---|---|---|
| Wails | Tauri (interface web + backend em Rust) | Ideia bem parecida. Veja Apps com interface gráfica |
| Fyne | egui, Iced, Slint |
Não existe um toolkit de interface gráfica oficial |
| Ebitengine | macroquad, Bevy |
O Bevy está antes da 1.0 e muda a API entre versões. Veja Desenvolvimento de jogos |
| TinyGo | embedded-hal, Embassy |
O Rust roda sem sistema operacional e sem heap. Veja Rust embarcado |
GOOS=js GOARCH=wasm |
wasm-bindgen, Leptos, Yew, Dioxus |
Binários pequenos, sem GC para embutir. Veja WebAssembly |
Lado a lado: structs e métodos (quase iguais)
Seção intitulada “Lado a lado: structs e métodos (quase iguais)”É aqui que os desenvolvedores Go se sentem em casa. Aqui está um pequeno tipo de conta em Go:
type Account struct { Owner string // exportado: letra maiúscula balance int // não exportado}
func NewAccount(owner string, balance int) *Account { return &Account{Owner: owner, balance: balance}}
func (a *Account) Deposit(amount int) { a.balance += amount }func (a *Account) Balance() int { return a.balance }func (a *Account) String() string { return fmt.Sprintf("%s: %d", a.Owner, a.balance) }
func main() { account := NewAccount("Ada", 100) account.Deposit(50) fmt.Println(account) fmt.Println("balance is", account.Balance())}E em Rust:
use std::fmt;
// `pub` faz o papel da letra maiúscula do Go.pub struct Account { pub owner: String, balance: i64, // privado, como um campo em minúscula no Go}
impl Account { // Como o NewAccount do Go: uma função comum. `new` é só uma convenção. pub fn new(owner: &str, balance: i64) -> Self { Account { owner: owner.to_string(), balance } }
// Como um receiver por ponteiro: este método altera a conta. pub fn deposit(&mut self, amount: i64) { self.balance += amount; }
// Só lê. Nenhuma cópia é feita, diferente de um receiver por valor no Go. pub fn balance(&self) -> i64 { self.balance }}
// O método String() do Go (a interface Stringer) vira Display.impl fmt::Display for Account { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}: {}", self.owner, self.balance) }}
fn main() { let mut account = Account::new("Ada", 100); account.deposit(50); println!("{account}"); println!("o saldo é {}", account.balance());}Saída:
Ada: 150o saldo é 150O formato é o mesmo: dados numa struct, métodos ligados a ela, um construtor que é só uma função. As diferenças são pequenas, mas importantes. Os métodos ficam juntos num bloco impl. O receiver diz exatamente o que o método faz: &self lê, &mut self altera, e o self simples toma o valor e o consome. E como deposit altera a conta, a variável precisa ser declarada com let mut. No Go, qualquer um que tenha um *Account pode alterá-lo; no Rust, o direito de alterar algo fica visível no código.
Lado a lado: interfaces implícitas viram traits explícitos
Seção intitulada “Lado a lado: interfaces implícitas viram traits explícitos”No Go, um tipo satisfaz uma interface só por ter os métodos certos:
type Shape interface { Area() float64 Name() string}
type Circle struct{ R float64 }type Rect struct{ W, H float64 }
// Nenhum "implements" em lugar nenhum: ter os métodos basta.func (c Circle) Area() float64 { return math.Pi * c.R * c.R }func (c Circle) Name() string { return "circle" }func (r Rect) Area() float64 { return r.W * r.H }func (r Rect) Name() string { return "rect" }
func describe[T Shape](s T) { fmt.Printf("%s: %.2f\n", s.Name(), s.Area())}
func main() { describe(Circle{R: 1})
shapes := []Shape{Circle{R: 1}, Rect{W: 2, H: 3}} total := 0.0 for _, s := range shapes { total += s.Area() } fmt.Printf("total area: %.2f\n", total)}Em Rust, você escreve a ligação com impl Shape for Circle:
use std::f64::consts::PI;
trait Shape { fn area(&self) -> f64; fn name(&self) -> &str;}
struct Circle { r: f64,}
struct Rect { w: f64, h: f64,}
// Esta linha é a parte que o Go deixa de fora.impl Shape for Circle { fn area(&self) -> f64 { PI * self.r * self.r } fn name(&self) -> &str { "círculo" }}
impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } fn name(&self) -> &str { "retângulo" }}
// Genérico, como o describe[T Shape](s T) do Go.fn describe<T: Shape>(s: &T) { println!("{}: {:.2}", s.name(), s.area());}
fn main() { describe(&Circle { r: 1.0 });
// Como o []Shape do Go: tipos diferentes por trás de um mesmo trait. let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { r: 1.0 }), Box::new(Rect { w: 2.0, h: 3.0 }), ]; let total: f64 = shapes.iter().map(|s| s.area()).sum(); println!("área total: {total:.2}");}Saída:
círculo: 3.14área total: 9.14Ser explícito custa uma linha por tipo, e compra algumas coisas que o Go não faz. Um trait pode ter métodos padrão, então os tipos ganham comportamento compartilhado de graça. Você pode implementar um trait seu para tipos que você não escreveu, até i32 ou String (o Go só deixa adicionar métodos a tipos do seu próprio pacote). E o describe genérico é compilado uma vez para cada tipo concreto, então a chamada não tem busca em tempo de execução. O Box<dyn Shape> é o estilo das interfaces do Go: um valor que aponta para “algum tipo que implementa Shape”, com o método encontrado em tempo de execução. Veja Traits e Trait objects.
Lado a lado: nil vira Option
Seção intitulada “Lado a lado: nil vira Option”No Go, um ponteiro que pode faltar é nil, e você precisa lembrar de verificar:
type User struct { Name string Manager *User // nil significa "sem gerente"}
func findUser(users []*User, name string) *User { for _, u := range users { if u.Name == name { return u } } return nil}
func main() { ada := &User{Name: "Ada"} users := []*User{ada, {Name: "Bob", Manager: ada}}
for _, name := range []string{"Bob", "Ada", "Eve"} { u := findUser(users, name) if u == nil { fmt.Println(name + ": no such user") continue } boss := "nobody" if u.Manager != nil { boss = u.Manager.Name } fmt.Println(name, "reports to", boss) }}Em Rust, “pode estar faltando” faz parte do tipo. Um Option<&User> é ou Some(user) ou None, e o compilador não deixa você usar o usuário antes de tratar o None:
struct User { name: String, manager: Option<String>, // Go: Manager *User, que pode ser nil}
fn find_user<'a>(users: &'a [User], name: &str) -> Option<&'a User> { users.iter().find(|u| u.name == name)}
fn main() { let users = vec![ User { name: "Ada".to_string(), manager: None }, User { name: "Bob".to_string(), manager: Some("Ada".to_string()) }, ];
for name in ["Bob", "Ada", "Eve"] { match find_user(&users, name) { Some(user) => { let boss = user.manager.as_deref().unwrap_or("ninguém"); println!("{name} responde a {boss}"); } None => println!("{name}: usuário não encontrado"), } }}Saída:
Bob responde a AdaAda responde a ninguémEve: usuário não encontradoA mesma ideia substitui as buscas com comma-ok do Go. if email, ok := emails[name]; ok { ... } vira if let Some(email) = emails.get(name) { ... }. Um cuidado: no Go, emails["missing"] devolve quietinho o zero value "". No Rust, emails["missing"] causa um panic, então use .get() sempre que a chave puder não existir. O 'a em find_user é um lifetime: ele diz ao compilador que o &User devolvido aponta para dentro de users. Você vai conhecer os lifetimes logo.
Lado a lado: if err != nil vira Result e ?
Seção intitulada “Lado a lado: if err != nil vira Result e ?”O tratamento de erros do Go é famoso pelo padrão de três linhas:
func parsePort(s string) (int, error) { n, err := strconv.Atoi(s) if err != nil { return 0, fmt.Errorf("invalid port %q: %w", s, err) } if n < 1 || n > 65535 { return 0, fmt.Errorf("port %d out of range", n) } return n, nil}
func address(host, port string) (string, error) { p, err := parsePort(port) if err != nil { return "", err } return fmt.Sprintf("%s:%d", host, p), nil}
func main() { for _, port := range []string{"8080", "abc", "70000", "0"} { addr, err := address("localhost", port) if err != nil { fmt.Println("error:", err) continue } fmt.Println("listening on", addr) }}O Rust mantém a ideia (erros são valores que você devolve), mas dobra o padrão inteiro num único caractere, o ?:
fn parse_port(s: &str) -> Result<u16, String> { // Um u16 não cabe 70000, então o parse() já pega o "fora do intervalo" para nós. let port: u16 = s.parse().map_err(|e| format!("porta inválida {s:?}: {e}"))?; if port == 0 { return Err("a porta 0 é reservada".to_string()); } Ok(port)}
fn address(host: &str, port: &str) -> Result<String, String> { // `?` significa: se isto for um erro, devolva-o agora mesmo. let port = parse_port(port)?; Ok(format!("{host}:{port}"))}
fn main() { for port in ["8080", "abc", "70000", "0"] { match address("localhost", port) { Ok(addr) => println!("escutando em {addr}"), Err(e) => println!("erro: {e}"), } }}Saída:
escutando em localhost:8080erro: porta inválida "abc": invalid digit found in stringerro: porta inválida "70000": number too large to fit in target typeerro: a porta 0 é reservadaAs mensagens “invalid digit found in string” (dígito inválido encontrado na string) e “number too large to fit in target type” (número grande demais para o tipo de destino) continuam em inglês porque vêm da biblioteca padrão do Rust. Há uma diferença mais profunda do que economizar linhas. No Go, n, err := strconv.Atoi(s) dá os dois valores, e nada impede você de usar n quando err não é nil. Um Result é ou o valor ou o erro, então você não consegue chegar ao valor sem tratar o erro antes. E se você ignorar um Result por completo, o compilador avisa.
Em projetos de verdade, os erros String dão lugar a algo mais rico. Um enum de erro (muitas vezes escrito com o crate thiserror) faz o papel dos sentinel errors e do errors.Is: quem chama faz match na variante. O crate anyhow oferece .context("loading config")?, que é o fmt.Errorf("loading config: %w", err) do Rust. Veja Erros personalizados e anyhow e thiserror.
Lado a lado: goroutines e channels viram threads, mpsc e tarefas
Seção intitulada “Lado a lado: goroutines e channels viram threads, mpsc e tarefas”Aqui está o clássico fan-in do Go: inicie algumas goroutines, mande os resultados por um channel e feche-o quando elas terminarem:
func main() { results := make(chan int) var wg sync.WaitGroup
for _, n := range []int{1, 2, 3} { wg.Add(1) go func(n int) { defer wg.Done() results <- n * n }(n) }
go func() { wg.Wait() close(results) }()
total := 0 for r := range results { total += r } fmt.Println("total:", total)}A versão em Rust, com threads e um canal da biblioteca padrão, é um pouco mais curta:
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
for n in [1, 2, 3] { let tx = tx.clone(); // cada thread recebe o próprio sender thread::spawn(move || { tx.send(n * n).unwrap(); }); }
// Descarte o nosso próprio sender. Quando todos os senders somem, o canal // é fechado e o laço abaixo termina: nada de WaitGroup. drop(tx);
let total: i32 = rx.iter().sum(); println!("total: {total}");}Saída:
total: 14Algumas coisas funcionam de outro jeito. mpsc significa “multi-producer, single-consumer” (vários produtores, um consumidor): você pode clonar o sender, mas não o receiver (para channels com vários leitores, como no Go, use o crossbeam-channel). O mpsc::channel() não tem limite, algo que o Go não tem; o mpsc::sync_channel(n) é o make(chan T, n) do Go, e o sync_channel(0) é um canal sem buffer. Ninguém chama close: o canal fecha quando o último sender é descartado. O move entrega a cada thread a própria cópia de n e o próprio tx, e é assim que o Rust garante que as threads nunca compartilhem dados de forma insegura. Veja Canais (channels) e Concorrência.
O thread::spawn inicia uma thread real do sistema operacional, que é mais pesada que uma goroutine. Isso é tranquilo para alguns poucos workers. Para milhares de conexões simultâneas, o Rust usa tarefas async num runtime como o tokio, que é bem mais parecido com as goroutines:
#[tokio::main]async fn main() { let mut handles = Vec::new();
for n in [1, 2, 3] { // Como `go func() { ... }()`, mas você recebe um handle de volta. handles.push(tokio::spawn(async move { n * n })); }
let mut total = 0; for handle in handles { total += handle.await.unwrap(); } println!("total: {total}");}Saída:
total: 14Sendo sincero: este é um ponto em que o Go é mais simples. No Go, qualquer função pode bloquear, e o runtime cuida disso. No Rust, você marca funções como async, chama com .await e escolhe um runtime. A recompensa é que o compilador se recusa a compilar código em que duas threads poderiam alterar o mesmo dado ao mesmo tempo, então o go test -race não teria nada para encontrar. Veja Async/Await, Async a fundo e Estado compartilhado para o Arc<Mutex<T>>, a versão Rust de uma struct com um sync.Mutex ao lado dos dados.
Lado a lado: defer vira Drop
Seção intitulada “Lado a lado: defer vira Drop”No Go, você agenda a limpeza com defer, e as chamadas adiadas rodam em ordem inversa quando a função retorna:
func process() { fmt.Println("open file") defer fmt.Println("close file")
fmt.Println("lock mutex") defer fmt.Println("unlock mutex")
fmt.Println("working")}
func main() { process() fmt.Println("done")}No Rust, a limpeza pertence ao próprio valor. Quando um valor sai de escopo, a implementação de Drop dele roda, e os valores são descartados em ordem inversa, igualzinho aos seus defers:
struct Guard(&'static str);
impl Drop for Guard { fn drop(&mut self) { println!("{}", self.0); }}
fn process() { println!("abrir arquivo"); let _file = Guard("fechar arquivo");
println!("travar mutex"); let _lock = Guard("destravar mutex");
println!("trabalhando");} // _lock é descartado aqui, depois _file
fn main() { process(); println!("pronto");}Saída:
abrir arquivotravar mutextrabalhandodestravar mutexfechar arquivoprontoEm código de verdade, você raramente escreve um Drop: um File se fecha sozinho, um MutexGuard se destrava sozinho, um TcpStream se desconecta sozinho. Não tem nada para esquecer. Duas diferenças em relação ao defer valem a pena conhecer. O Drop roda no fim do bloco, e não da função, então a armadilha do Go de defers se acumulando dentro de um laço não existe: cada volta do laço limpa o que é seu. E cuidado com let _ = Guard(...): o _ sozinho não guarda o valor, então ele é descartado na hora. Use um nome como _lock.
O que vai fazer você tropeçar vindo do Go
Seção intitulada “O que vai fazer você tropeçar vindo do Go”Todo mundo que vem do Go passa pelos mesmos buracos. Conhecer todos com antecedência ajuda.
-
Passar um valor pode movê-lo. No Go, passar um slice para uma função copia um pequeno cabeçalho, e os dois lados continuam usando o slice. No Rust, passar um
Veco move para dentro da função, e você não pode mais usá-lo depois:fn total(prices: Vec<u32>) -> u32 {prices.iter().sum()}fn main() {let prices = vec![3, 4, 5];let sum = total(prices);println!("{sum} from {} items", prices.len());}error[E0382]: borrow of moved value: `prices`--> src/main.rs:8:37|6 | let prices = vec![3, 4, 5];| ------ move occurs because `prices` has type `Vec<u32>`, which does not implement the `Copy` trait7 | let sum = total(prices);| ------ value moved here8 | println!("{sum} from {} items", prices.len());| ^^^^^^ value borrowed here after moveO compilador diz (em inglês) que
pricesfoi movido na linha 7 (“value moved here”) e usado depois na linha 8 (“value borrowed here after move”). A correção é o hábito do Go em versão Rust: faça a função pegar emprestado um slice,fn total(prices: &[u32]), e chame comtotal(&prices). Leia primeiro Ownership; essa página explica essa regra. A página do E0382 cobre esse erro em detalhes. -
O borrow checker. Você pode ter muitos leitores (
&T) ou um único escritor (&mut T) ao mesmo tempo, nunca os dois. Código Go em que muitas structs guardam ponteiros para uma mesma struct compartilhada e a alteram precisa de outro design em Rust, ou de smart pointers (ponteiros inteligentes) comoRc<RefCell<T>>ouArc<Mutex<T>>. Prepare-se para algumas semanas frustrantes. Depois fica bem mais fácil. -
Sem zero values.
let x: i32;seguido deprintln!("{x}")é um erro de compilação, e todo campo de uma struct precisa ser preenchido quando você a cria. Se você quer valores padrão no estilo do Go, deriveDefaulte escrevaConfig { port: 8080, ..Default::default() }. -
Implementar um trait é explícito. Ter um método
areanão transforma o seu tipo numShape. Você precisa deimpl Shape for Circle. A vantagem: você encontra todas as implementações só buscando por elas. -
Stringvs&str. O Go tem um único tipostring. O Rust tem dois:Stringé dona do texto,&strpega o texto emprestado. Funções normalmente recebem&str, structs normalmente guardamString. E você não consegue indexar uma string coms[0]como no Go; uses.chars()ous.as_bytes()[0]. Veja Strings. -
Coisas não usadas geram avisos, não erros. O Go se recusa a compilar com um import ou uma variável sem uso. O Rust só avisa, e você pode começar um nome com
_para dizer “sem uso de propósito”. -
Overflow de inteiros. O Go dá a volta em silêncio. O Rust causa um panic nos builds de debug e dá a volta nos builds de release, então escolha
checked_add,wrapping_addousaturating_addquando o overflow for possível. Literais inteiros sãoi32por padrão, e índices sãousize. Veja Overflow de inteiros. -
Uma biblioteca padrão menor. Nada de servidor HTTP, JSON ou runtime async na
std. Você vai fazercargo adddeles. No começo parece estranho, mas os crates populares são estáveis e bem documentados. -
Async precisa de um runtime, e se espalha. Uma
async fnnão faz nada até receber um await dentro de um runtime como o tokio, e só código async pode usar.await. O Go não tem essa divisão. Comece com threads simples; parta para o async quando precisar de muitas conexões simultâneas. -
Tempo de compilação. O Go foi feito para compilar muito rápido. O Rust compila mais devagar, principalmente em builds limpos com muitos crates. Use o
cargo checkenquanto edita; ele pula a geração de código e é bem mais rápido. -
Compilar para outra plataforma pede um pouco de configuração.
GOOS=linux go buildsimplesmente funciona. No Rust, você adiciona o alvo com o rustup e muitas vezes precisa de um linker para ele também. Ferramentas como ocrosscuidam disso para você.
Go ou Rust? Quando escolher cada um
Seção intitulada “Go ou Rust? Quando escolher cada um”Os dois são boas escolhas, e muitos times usam os dois. Aqui vai uma regra prática justa.
Escolha Go quando você quer entregar serviços de rede e ferramentas internas rápido, o seu time valoriza um código que qualquer pessoa consegue ler depois de uma semana, builds rápidos importam muito, ou você trabalha no mundo cloud-native (Kubernetes, Docker, Terraform), onde Go é a língua comum. A biblioteca padrão do Go e a concorrência simples dele deixam muitos programas de servidor fáceis de escrever.
Escolha Rust quando você precisa de latência previsível, sem pausas de garbage collection, de pouco uso de memória, ou de cada gota de CPU (parsers, bancos de dados, engines de jogos, vídeo, compressão). O Rust também se encaixa melhor em código sem sistema operacional, em WebAssembly e em bibliotecas que você quer chamar a partir de outras linguagens. E vale a pena quando uma quebra causada por uma data race ou por uma verificação de nil esquecida sai cara. O Discord escreveu um post bem conhecido sobre a migração de um serviço de Go para Rust (em inglês) por causa de picos de latência causados pela garbage collection.
O custo do Rust é o tempo de aprendizado e os builds mais lentos. O custo do Go é um garbage collector e bugs que aparecem em tempo de execução em vez de em tempo de compilação. Qual custo dói mais depende do seu projeto.
Uma trilha de estudo para desenvolvedores Go
Seção intitulada “Uma trilha de estudo para desenvolvedores Go”Você não precisa ler este site na ordem. Como desenvolvedor Go, este caminho pula o que você já sabe e investe tempo onde o Rust é diferente:
- Começando com Rust e Ferramentas do Rust: instale o Rust, configure o rust-analyzer e rode
cargo new. - Variáveis e Tipos de dados: passe os olhos, principalmente para aprender
let mute os tipos numéricos. - Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
- Strings:
Stringvs&str. - Structs e Enums, depois Match e Pattern matching. Enums com dados substituem muitos type switches do Go.
- Option e Result, depois Erros personalizados e anyhow e thiserror.
- Traits, Genéricos, Traits comuns e Trait objects.
- Closures e Iteradores: o Rust usa cadeias de iteradores muito mais que o Go.
- Vetores, HashMap e HashSet.
- Módulos, Cargo e crates e Testes em Rust.
- Lifetimes e Ponteiros inteligentes.
- Concorrência, Canais (channels), Estado compartilhado e depois Async/Await.
- JSON com Serde e Logging, e depois construa o projeto de API web.
Prática
Seção intitulada “Prática”Os dois exercícios trazem um trecho de Go. Converta para Rust.
1. Converta um contador de palavras
Seção intitulada “1. Converta um contador de palavras”Este programa Go conta palavras e as imprime em ordem alfabética. Converta para Rust. Dica: use um HashMap<&str, u32> e *counts.entry(word).or_insert(0) += 1 no lugar do counts[w]++ do Go. O text.split_whitespace() é o strings.Fields.
func main() { text := "the cat and the dog and the bird"
counts := map[string]int{} for _, w := range strings.Fields(text) { counts[w]++ }
keys := make([]string, 0, len(counts)) for k := range counts { keys = append(keys, k) } sort.Strings(keys)
for _, k := range keys { fmt.Printf("%s: %d\n", k, counts[k]) }}Ver solução
use std::collections::HashMap;
fn main() { let text = "the cat and the dog and the bird";
let mut counts: HashMap<&str, u32> = HashMap::new(); for word in text.split_whitespace() { *counts.entry(word).or_insert(0) += 1; }
let mut keys: Vec<&str> = counts.keys().copied().collect(); keys.sort();
for key in keys { println!("{key}: {}", counts[key]); }}Saída:
and: 2bird: 1cat: 1dog: 1the: 3O Rust não tem um zero value em que se apoiar aqui, então o entry(word).or_insert(0) diz “comece em 0 se a palavra for nova”. Indexar com counts[key] é seguro porque toda chave veio do próprio mapa. Mais curto ainda: use um BTreeMap no lugar do HashMap, e as chaves já saem ordenadas, então você pode dispensar o vetor keys. (O texto ficou em inglês porque a saída depende da ordem alfabética.)
2. Converta um sentinel error
Seção intitulada “2. Converta um sentinel error”Este código Go devolve um sentinel error e o verifica com errors.Is. Converta para que withdraw devolva um Result<(), WithdrawError>, em que WithdrawError é um enum com duas variantes. Dica: implemente fmt::Display para o enum, para poder imprimi-lo, e faça match na variante onde o Go usa errors.Is.
var ErrInsufficientFunds = errors.New("insufficient funds")
type Account struct{ Balance int }
func (a *Account) Withdraw(n int) error { if n <= 0 { return fmt.Errorf("invalid amount %d", n) } if n > a.Balance { return ErrInsufficientFunds } a.Balance -= n return nil}
func main() { acc := &Account{Balance: 100} for _, n := range []int{30, 0, 500} { if err := acc.Withdraw(n); err != nil { if errors.Is(err, ErrInsufficientFunds) { fmt.Println("declined:", err) } else { fmt.Println("error:", err) } continue } fmt.Println("ok, balance", acc.Balance) }}Ver solução
use std::fmt;
enum WithdrawError { InvalidAmount(i64), InsufficientFunds,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InvalidAmount(n) => write!(f, "valor inválido {n}"), WithdrawError::InsufficientFunds => write!(f, "saldo insuficiente"), } }}
struct Account { balance: i64,}
impl Account { fn withdraw(&mut self, n: i64) -> Result<(), WithdrawError> { if n <= 0 { return Err(WithdrawError::InvalidAmount(n)); } if n > self.balance { return Err(WithdrawError::InsufficientFunds); } self.balance -= n; Ok(()) }}
fn main() { let mut acc = Account { balance: 100 }; for n in [30, 0, 500] { match acc.withdraw(n) { Ok(()) => println!("ok, saldo {}", acc.balance), Err(e @ WithdrawError::InsufficientFunds) => println!("recusado: {e}"), Err(e) => println!("erro: {e}"), } }}Saída:
ok, saldo 70erro: valor inválido 0recusado: saldo insuficienteO sentinel error e o erro do fmt.Errorf viram duas variantes de um mesmo enum, e o errors.Is vira um braço do match. O e @ WithdrawError::InsufficientFunds casa com a variante e ainda entrega o valor inteiro como e, para você imprimir. Um bônus legal: se você adicionar uma terceira variante depois, o compilador mostra cada match que precisa ser atualizado. O crate thiserror pode escrever a implementação de Display para você.
- Muita coisa do Go vem junto: structs com métodos, nada de herança, erros como valores, um formatador padrão e uma ferramenta só para compilar e testar.
- As grandes mudanças são ownership no lugar de um garbage collector,
Optionno lugar denil, eResult+?no lugar deif err != nil. - Interfaces viram traits, e implementar um é explícito (
impl Trait for Type). Odefervira oDropautomático no fim de um escopo. - Goroutines e channels viram
thread::spawn+mpscpara poucos workers, ou tarefas do tokio para muitos; o compilador elimina as data races. - O Cargo substitui
go build,go testego mod; o clippy e o rustfmt substituem ogo vete ogofmt. No lugar do Gin, veja o axum ou o actix-web; no lugar do GORM, o SeaORM ou o Diesel; no lugar do pgx e do sqlc, o sqlx; e no lugar do cobra, o clap.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. No Go, um tipo satisfaz uma interface só por ter os métodos certos. Como um tipo do Rust implementa um trait?
2. O que o ? faz depois de uma chamada que devolve um Result?
3. Com o std::sync::mpsc, quando o for msg in rx (ou o rx.iter()) para?
4. O seu time Go constrói APIs HTTP com o Gin. Qual crate do Rust é o mais parecido?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- The Rust Programming Language (em inglês): o livro oficial. Os capítulos 4 (ownership), 9 (erros), 10 (genéricos e traits) e 16 (concorrência) são os que mais importam para quem vem do Go.
- Tokio tutorial (em inglês): o runtime assíncrono que a maioria dos servidores Rust usa, o mais perto do escalonador de goroutines do Go.
- Documentação do axum (em inglês): o framework web para olhar primeiro se você vem do Gin, do Echo ou do Chi.
- Why Discord is switching from Go to Rust (em inglês): a história real de um time que migrou um serviço sensível à latência de Go para Rust.
- Are We Web Yet? (em inglês): um panorama da comunidade sobre as bibliotecas e os frameworks web do Rust.