Rust para Programadores Go: Transição | Rust Brasil

Guia de transição de Go para Rust: concorrência, tratamento de erros, tipos e ferramentas comparados lado a lado.

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

AspectoGoRust
Gerenciamento de memóriaGarbage CollectorOwnership + Borrow Checker
ConcorrênciaGoroutines + channelsasync/await + threads + channels
Tratamento de erroserror interface, if err != nilResult<T, E>, operador ?
PolimorfismoInterfaces (implícitas)Traits (explícitas)
GenéricosSim (desde Go 1.18)Sim (desde o início)
Null safetynil (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.

ConceitoGoRust
Variávelvar x int = 10let x: i32 = 10;
Inferênciax := 10let x = 10;
MutávelTodas são mutáveislet mut x = 10;
Constanteconst Pi = 3.14const PI: f64 = 3.14;
Zero valueSim (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);
    }
}
ConceitoGoRust
Concorrência levego func()tokio::spawn(async { })
Threads do SOgo func() (com GOMAXPROCS)std::thread::spawn
Channelsmake(chan T)mpsc::channel() / tokio::sync::mpsc
Mutexsync.Mutexstd::sync::Mutex<T>
WaitGroupsync.WaitGrouptokio::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

TarefaGoRust
Iniciar projetogo mod init modulocargo new projeto
Arquivo de configgo.modCargo.toml
Lock filego.sumCargo.lock
Adicionar dependênciago get pacote@versãocargo add pacote
Compilargo buildcargo build
Executargo run .cargo run
Testargo test ./...cargo test
Formatar códigogofmtrustfmt / cargo fmt
Lintergolangci-lintcargo clippy
Documentaçãogodoccargo doc
Repositório centralproxy.golang.orgcrates.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:

  1. 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.
  2. Enums algébricos – aproveite esse recurso. Ele substitui muitos padrões que em Go exigem múltiplos tipos ou interfaces.
  3. O operador ? – quando dominar Result e ?, você nunca mais vai sentir falta de if err != nil.
  4. Cargo é seu melhor amigo – ele integra tudo que em Go está espalhado entre go build, go test, gofmt, golint, etc.
  5. Use clippy desde o iníciocargo 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!