Go e Rust são frequentemente comparados como linguagens modernas de sistemas. Ambas compilam para código nativo, possuem excelente ferramental e focam em desempenho. Se você já programa em Go e quer aprender Rust, este guia mapeia os conceitos que você já conhece para seus equivalentes em Rust, destacando as diferenças fundamentais de design entre as duas linguagens.
Visão Geral: Filosofias Diferentes
| Aspecto | Go | Rust |
|---|---|---|
| Gerenciamento de memória | Garbage Collector | Ownership + Borrow Checker |
| Concorrência | Goroutines + channels | async/await + threads + channels |
| Tratamento de erros | error interface, if err != nil | Result<T, E>, operador ? |
| Polimorfismo | Interfaces (implícitas) | Traits (explícitas) |
| Genéricos | Sim (desde Go 1.18) | Sim (desde o início) |
| Null safety | nil (pode causar panic) | Option<T> (verificado em compilação) |
Variáveis e Tipos Básicos
As duas linguagens usam tipagem estática, mas Rust é mais rigoroso com mutabilidade e oferece mais tipos numéricos.
| Conceito | Go | Rust |
|---|---|---|
| Variável | var x int = 10 | let x: i32 = 10; |
| Inferência | x := 10 | let x = 10; |
| Mutável | Todas são mutáveis | let mut x = 10; |
| Constante | const Pi = 3.14 | const PI: f64 = 3.14; |
| Zero value | Sim (ex: 0, "", nil) | Sem zero values |
Go:
package main
import "fmt"
func main() {
nome := "Rust Brasil"
idade := 5
ativo := true
fmt.Printf("%s tem %d anos, ativo: %t\n", nome, idade, ativo)
}
Rust:
fn main() {
let nome = "Rust Brasil";
let idade = 5;
let ativo = true;
println!("{} tem {} anos, ativo: {}", nome, idade, ativo);
}
A diferença fundamental: em Go, todas as variáveis são mutáveis. Em Rust, variáveis são imutáveis por padrão e você deve optar explicitamente pela mutabilidade com mut.
Structs
Ambas as linguagens usam structs, mas Rust não tem campos exportados por letra maiúscula – usa pub em vez disso.
Go:
type Usuario struct {
Nome string
Email string
Idade int
}
func NovoUsuario(nome, email string, idade int) Usuario {
return Usuario{
Nome: nome,
Email: email,
Idade: idade,
}
}
func (u Usuario) Saudacao() string {
return fmt.Sprintf("Olá, meu nome é %s", u.Nome)
}
Rust:
pub struct Usuario {
pub nome: String,
pub email: String,
pub idade: u32,
}
impl Usuario {
pub fn novo(nome: &str, email: &str, idade: u32) -> Self {
Usuario {
nome: nome.to_string(),
email: email.to_string(),
idade,
}
}
pub fn saudacao(&self) -> String {
format!("Olá, meu nome é {}", self.nome)
}
}
Interfaces vs Traits
Go usa interfaces implícitas – se um tipo tem os métodos certos, ele implementa a interface automaticamente. Rust usa traits com implementação explícita.
Go:
type Falante interface {
Falar() string
}
type Cachorro struct {
Nome string
}
// Implementação implícita -- não há declaração "implements"
func (c Cachorro) Falar() string {
return c.Nome + " diz: Au au!"
}
type Gato struct {
Nome string
}
func (g Gato) Falar() string {
return g.Nome + " diz: Miau!"
}
func fazerFalar(f Falante) {
fmt.Println(f.Falar())
}
Rust:
trait Falante {
fn falar(&self) -> String;
}
struct Cachorro {
nome: String,
}
// Implementação explícita
impl Falante for Cachorro {
fn falar(&self) -> String {
format!("{} diz: Au au!", self.nome)
}
}
struct Gato {
nome: String,
}
impl Falante for Gato {
fn falar(&self) -> String {
format!("{} diz: Miau!", self.nome)
}
}
fn fazer_falar(f: &dyn Falante) {
println!("{}", f.falar());
}
// Ou com generics (despacho estático, mais performático)
fn fazer_falar_generico<T: Falante>(f: &T) {
println!("{}", f.falar());
}
Rust oferece duas formas de polimorfismo: despacho dinâmico com dyn Trait (semelhante a interfaces Go) e despacho estático com generics (sem custo em runtime).
Tratamento de Erros: if err != nil vs Result<T, E>
Este é um dos maiores contrastes. Go usa o padrão (valor, error) com checagem manual. Rust usa o tipo Result<T, E> com o operador ?.
Go:
import (
"fmt"
"os"
"strconv"
)
func lerArquivoComoNumero(caminho string) (int, error) {
dados, err := os.ReadFile(caminho)
if err != nil {
return 0, fmt.Errorf("erro ao ler arquivo: %w", err)
}
numero, err := strconv.Atoi(string(dados))
if err != nil {
return 0, fmt.Errorf("erro ao converter: %w", err)
}
return numero, nil
}
func main() {
num, err := lerArquivoComoNumero("numero.txt")
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println("Número:", num)
}
Rust:
use std::fs;
use std::num::ParseIntError;
use std::io;
#[derive(Debug)]
enum MeuErro {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for MeuErro {
fn from(e: io::Error) -> Self { MeuErro::Io(e) }
}
impl From<ParseIntError> for MeuErro {
fn from(e: ParseIntError) -> Self { MeuErro::Parse(e) }
}
fn ler_arquivo_como_numero(caminho: &str) -> Result<i32, MeuErro> {
let dados = fs::read_to_string(caminho)?; // propaga erro de IO
let numero: i32 = dados.trim().parse()?; // propaga erro de parse
Ok(numero)
}
fn main() {
match ler_arquivo_como_numero("numero.txt") {
Ok(num) => println!("Número: {}", num),
Err(e) => println!("Erro: {:?}", e),
}
}
O operador ? em Rust elimina o repetitivo if err != nil do Go. Na prática, muitos projetos Rust usam a crate anyhow ou thiserror para simplificar ainda mais o tratamento de erros.
nil vs Option<T>
Go usa nil para ponteiros, slices, maps, channels e interfaces. Isso pode causar panics em runtime. Rust usa Option<T>, que é verificado em tempo de compilação.
Go:
func buscarUsuario(id int) *Usuario {
if id == 1 {
return &Usuario{Nome: "Ana"}
}
return nil // pode causar panic se não verificado
}
func main() {
u := buscarUsuario(2)
if u != nil {
fmt.Println(u.Nome)
}
// Se esquecer o check: panic: nil pointer dereference
}
Rust:
fn buscar_usuario(id: u32) -> Option<Usuario> {
if id == 1 {
Some(Usuario::novo("Ana", "ana@email.com", 30))
} else {
None
}
}
fn main() {
// O compilador OBRIGA o tratamento
match buscar_usuario(2) {
Some(u) => println!("{}", u.nome),
None => println!("Usuário não encontrado"),
}
// Ou com if let
if let Some(u) = buscar_usuario(1) {
println!("{}", u.nome);
}
// Ou com unwrap_or
let nome = buscar_usuario(2)
.map(|u| u.nome)
.unwrap_or_else(|| String::from("Desconhecido"));
}
Goroutines vs Async/Threads
Go tem goroutines integradas na linguagem. Rust oferece threads nativas do SO e async/await com um runtime como Tokio.
Go (goroutines + channels):
func main() {
ch := make(chan string, 2)
go func() {
time.Sleep(1 * time.Second)
ch <- "resultado 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch <- "resultado 2"
}()
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Rust (async com Tokio):
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(2);
let tx1 = tx.clone();
tokio::spawn(async move {
sleep(Duration::from_secs(1)).await;
tx1.send("resultado 1").await.unwrap();
});
tokio::spawn(async move {
sleep(Duration::from_secs(2)).await;
tx.send("resultado 2").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}
Rust (threads nativas + mpsc):
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
thread::sleep(Duration::from_secs(1));
tx1.send("resultado 1").unwrap();
});
thread::spawn(move || {
thread::sleep(Duration::from_secs(2));
tx.send("resultado 2").unwrap();
});
for msg in rx {
println!("{}", msg);
}
}
| Conceito | Go | Rust |
|---|---|---|
| Concorrência leve | go func() | tokio::spawn(async { }) |
| Threads do SO | go func() (com GOMAXPROCS) | std::thread::spawn |
| Channels | make(chan T) | mpsc::channel() / tokio::sync::mpsc |
| Mutex | sync.Mutex | std::sync::Mutex<T> |
| WaitGroup | sync.WaitGroup | tokio::task::JoinSet / join! |
Uma diferença importante: o Mutex<T> do Rust encapsula os dados protegidos, tornando impossível acessar os dados sem adquirir o lock. Em Go, o mutex e os dados são separados, o que depende da disciplina do programador.
Slices vs Vec e Slices em Rust
Go tem slices como tipo fundamental. Rust separa Vec<T> (alocado no heap, growable) e slices &[T] (referência a uma sequência).
Go:
nums := []int{1, 2, 3, 4, 5}
nums = append(nums, 6)
fatia := nums[1:3] // [2, 3]
// Iterar
for i, v := range nums {
fmt.Printf("índice %d: valor %d\n", i, v)
}
Rust:
let mut nums = vec![1, 2, 3, 4, 5];
nums.push(6);
let fatia = &nums[1..3]; // [2, 3]
// Iterar
for (i, v) in nums.iter().enumerate() {
println!("índice {}: valor {}", i, v);
}
Sistema de Pacotes: go mod vs Cargo
| Tarefa | Go | Rust |
|---|---|---|
| Iniciar projeto | go mod init modulo | cargo new projeto |
| Arquivo de config | go.mod | Cargo.toml |
| Lock file | go.sum | Cargo.lock |
| Adicionar dependência | go get pacote@versão | cargo add pacote |
| Compilar | go build | cargo build |
| Executar | go run . | cargo run |
| Testar | go test ./... | cargo test |
| Formatar código | gofmt | rustfmt / cargo fmt |
| Linter | golangci-lint | cargo clippy |
| Documentação | godoc | cargo doc |
| Repositório central | proxy.golang.org | crates.io |
Go (go.mod):
module github.com/usuario/projeto
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)
Rust (Cargo.toml):
[package]
name = "projeto"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4"
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
Cargo é mais integrado que o ferramental Go: compilação, testes, documentação, formatação, linting e gerenciamento de dependências estão todos sob um único comando.
Ponteiros vs Referências
Go usa ponteiros (*T) com semântica simples. Rust usa referências (&T, &mut T) com o sistema de ownership.
Go:
func incrementar(x *int) {
*x++
}
func main() {
valor := 10
incrementar(&valor)
fmt.Println(valor) // 11
}
Rust:
fn incrementar(x: &mut i32) {
*x += 1;
}
fn main() {
let mut valor = 10;
incrementar(&mut valor);
println!("{}", valor); // 11
}
A regra fundamental do Rust: você pode ter muitas referências imutáveis (&T) OU uma referência mutável (&mut T) – nunca ambas ao mesmo tempo. Isso previne data races em tempo de compilação.
Enums: O Superpoder do Rust
Go não tem enums no mesmo sentido que Rust. Go usa iota para constantes enumeradas. Rust tem enums algébricos que podem carregar dados.
Go:
type Status int
const (
Pendente Status = iota
EmProgresso
Concluido
)
// Para dados associados, precisa de struct separada
type Resultado struct {
Sucesso bool
Valor string
Erro string
}
Rust:
enum Status {
Pendente,
EmProgresso,
Concluido,
}
// Enums podem carregar dados diferentes em cada variante
enum Resultado {
Sucesso(String),
Erro { codigo: u32, mensagem: String },
Carregando,
}
fn processar(r: Resultado) {
match r {
Resultado::Sucesso(valor) => println!("OK: {}", valor),
Resultado::Erro { codigo, mensagem } => {
println!("Erro {}: {}", codigo, mensagem)
}
Resultado::Carregando => println!("Aguarde..."),
}
}
Os tipos Option<T> e Result<T, E> do Rust são, na verdade, enums comuns da biblioteca padrão. Esse recurso substitui muitos padrões que em Go exigem structs, interfaces ou múltiplos valores de retorno.
Conclusão
Go e Rust compartilham objetivos de desempenho e segurança, mas com filosofias distintas. Go prioriza simplicidade e facilidade de aprendizado. Rust prioriza garantias em tempo de compilação e controle sobre a memória.
Dicas para a transição:
- Ownership e borrowing – esse é o maior salto conceitual vindo de Go. O garbage collector não existe em Rust; em vez disso, o compilador verifica o uso de memória em tempo de compilação.
- Enums algébricos – aproveite esse recurso. Ele substitui muitos padrões que em Go exigem múltiplos tipos ou interfaces.
- O operador
?– quando dominarResulte?, você nunca mais vai sentir falta deif err != nil. - Cargo é seu melhor amigo – ele integra tudo que em Go está espalhado entre
go build,go test,gofmt,golint, etc. - Use
clippydesde o início –cargo clippyé essencial para aprender Rust idiomático.
A transição de Go para Rust é uma das mais suaves entre linguagens, graças às semelhanças em filosofia: ambas favorecem explicitação, composição sobre herança e ferramental integrado. Bem-vindo ao Rust!