Convert Go code to idiomatic Rust. Use when migrating Go projects to Rust, translating Go patterns to idiomatic Rust, or refactoring Go codebases. Extends meta-convert-dev with Go-to-Rust specific patterns.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
convert-golang-rust
description
Convert Go code to idiomatic Rust. Use when migrating Go projects to Rust, translating Go patterns to idiomatic Rust, or refactoring Go codebases. Extends meta-convert-dev with Go-to-Rust specific patterns.
Convert Go to Rust
Convert Go code to idiomatic Rust. This skill extends meta-convert-dev with Go-to-Rust specific type mappings, idiom translations, and tooling.
use std::fs;
use std::path::Path;
fn read_file(path: &Path) -> Result<Vec<u8>, std::io::Error> {
fs::read(path)
.map_err(|e| std::io::Error::new(
e.kind(),
format!("failed to read {}: {}", path.display(), e)
))
}
Why this translation:
Rust's Result<T, E> encodes success/failure in the type system
The ? operator propagates errors ergonomically
Error wrapping uses map_err instead of manual checks
Borrowed &Path instead of owned String for efficiency
Pattern 2: Nil Checking
Go:
func getUserName(user *User) string {
if user == nil {
return "Anonymous"
}
if user.Name == "" {
return "Anonymous"
}
return user.Name
}
Rust:
fn get_user_name(user: Option<&User>) -> &str {
user.and_then(|u| {
if u.name.is_empty() {
None
} else {
Some(u.name.as_str())
}
})
.unwrap_or("Anonymous")
}
// Or more idiomatically with pattern matching:
fn get_user_name(user: Option<&User>) -> &str {
match user {
Some(u) if !u.name.is_empty() => &u.name,
_ => "Anonymous",
}
}
Why this translation:
Option<T> makes nullability explicit in the type system
Combinators like and_then and unwrap_or are idiomatic
Pattern matching with guards is more readable
Borrowed references avoid unnecessary cloning
Pattern 3: Interface Implementation
Go:
type Reader interface {
Read(p []byte) (n int, err error)
}
type FileReader struct {
path string
}
func (f *FileReader) Read(p []byte) (int, error) {
// Implementation
return len(p), nil
}
func processReader(r Reader) error {
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
return err
}
// Process n bytes
return nil
}
Rust:
use std::io::{self, Read};
trait Reader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>;
}
struct FileReader {
path: String,
}
impl Reader for FileReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
// Implementation
Ok(buf.len())
}
}
fn process_reader<R: Reader>(r: &mut R) -> io::Result<()> {
let mut buf = vec![0u8; 1024];
let n = r.read(&mut buf)?;
// Process n bytes
Ok(())
}
Why this translation:
Rust traits are explicitly implemented with impl Trait for Type
Generic functions use trait bounds (<R: Reader>)
Mutable borrows (&mut) make mutation explicit
The ? operator replaces verbose error checking
Pattern 4: Goroutines and Channels
Go:
func processItems(items []string) []string {
results := make(chan string, len(items))
for _, item := range items {
go func(s string) {
results <- processItem(s)
}(item)
}
var processed []string
for i := 0; i < len(items); i++ {
processed = append(processed, <-results)
}
return processed
}
Rust:
use tokio::task;
async fn process_items(items: Vec<String>) -> Vec<String> {
let handles: Vec<_> = items
.into_iter()
.map(|item| {
task::spawn(async move {
process_item(item).await
})
})
.collect();
let mut processed = Vec::new();
for handle in handles {
if let Ok(result) = handle.await {
processed.push(result);
}
}
processed
}
Why this translation:
Tokio's async/await is more explicit about async boundaries
task::spawn creates async tasks similar to goroutines
Awaiting task handles is more type-safe than channels
Move semantics avoid accidental captures
Alternative: Use channels with tokio::sync::mpsc for Go-like patterns
Pattern 5: Defer for Cleanup
Go:
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// Work with file
return process(f)
}
Rust:
use std::fs::File;
use std::path::Path;
fn process_file(path: &Path) -> std::io::Result<()> {
let f = File::open(path)?;
// Work with file
process(&f)
// f.close() called automatically via Drop
}
// Or with explicit scope control:
fn process_file_scoped(path: &Path) -> std::io::Result<()> {
let result = {
let f = File::open(path)?;
process(&f)
}; // f dropped here
result
}
Why this translation:
Rust's RAII (Drop trait) automatically cleans up resources
No need for explicit defer statements
Scope-based cleanup is deterministic
More type-safe than runtime defer
Pattern 6: Type Assertions and Type Switches
Go:
func handleValue(v interface{}) string {
switch val := v.(type) {
case string:
return fmt.Sprintf("string: %s", val)
case int:
return fmt.Sprintf("int: %d", val)
case error:
return fmt.Sprintf("error: %v", val)
default:
return "unknown"
}
}
Rust:
// Use enums instead of interface{} for type-safe variants
enum Value {
String(String),
Int(i32),
Error(String),
}
fn handle_value(v: Value) -> String {
match v {
Value::String(s) => format!("string: {}", s),
Value::Int(i) => format!("int: {}", i),
Value::Error(e) => format!("error: {}", e),
}
}
// Or use trait objects with downcasting (less idiomatic):
use std::any::Any;
fn handle_any(v: &dyn Any) -> String {
if let Some(s) = v.downcast_ref::<String>() {
format!("string: {}", s)
} else if let Some(i) = v.downcast_ref::<i32>() {
format!("int: {}", i)
} else {
"unknown".to_string()
}
}
Why this translation:
Rust enums are type-safe alternatives to interface{}
Pattern matching exhaustively handles all variants
Compiler ensures all cases are covered
Avoid Any downcasting when possible (use enums instead)
Pattern 7: Method Receivers (Value vs Pointer)
Go:
type Counter struct {
count int
}
// Pointer receiver (mutates)
func (c *Counter) Increment() {
c.count++
}
// Value receiver (doesn't mutate)
func (c Counter) Value() int {
return c.count
}
Rust makes mutability explicit: &self, &mut self, self
Borrowing prevents accidental copies
Consuming methods (self) transfer ownership
More explicit control over mutation
Pattern 8: Struct Embedding (Composition)
Go:
type Base struct {
ID int
}
func (b *Base) GetID() int {
return b.ID
}
type Derived struct {
Base
Name string
}
func main() {
d := Derived{
Base: Base{ID: 1},
Name: "test",
}
fmt.Println(d.GetID()) // Method promoted from Base
}
Rust:
struct Base {
id: i32,
}
impl Base {
fn get_id(&self) -> i32 {
self.id
}
}
struct Derived {
base: Base,
name: String,
}
impl Derived {
// Explicit delegation (no automatic promotion)
fn get_id(&self) -> i32 {
self.base.get_id()
}
}
// Or use Deref trait for automatic field access:
use std::ops::Deref;
impl Deref for Derived {
type Target = Base;
fn deref(&self) -> &Self::Target {
&self.base
}
}
Rust doesn't have built-in context; use timeout or cancellation tokens
tokio::select! provides cancellation semantics
Explicit timeout durations instead of context deadlines
More type-safe cancellation handling
Pattern 10: Variadic Functions
Go:
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3, 4))
}
Rust:
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
fn main() {
println!("{}", sum(&[1, 2, 3, 4]));
}
// Or with macro for syntax sugar:
macro_rules! sum {
($($x:expr),*) => {
{
let nums = [$($x),*];
nums.iter().sum::<i32>()
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3, 4));
}
Why this translation:
Rust doesn't have variadic functions; use slices
Macros can provide variadic-like syntax
Iterator methods are more idiomatic than loops
More explicit about allocation
Error Handling
Go Error Interface → Rust Result Type
Go's error interface and multiple return values translate to Rust's Result<T, E> enum.
Philosophy Shift:
Go: Errors are values, checked explicitly via if err != nil
Rust: Errors are types, propagated with ? operator
Basic Error Translation
Go:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
}
Rust:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct DivisionError;
impl fmt::Display for DivisionError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "division by zero")
}
}
impl Error for DivisionError {}
fn divide(a: f64, b: f64) -> Result<f64, DivisionError> {
if b == 0.0 {
Err(DivisionError)
} else {
Ok(a / b)
}
}
fn main() -> Result<(), Box<dyn Error>> {
let result = divide(10.0, 0.0)?;
println!("{}", result);
Ok(())
}
Error Wrapping and Context
Go:
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
return &cfg, nil
}
Rust:
use std::fs;
use std::path::Path;
use serde::Deserialize;
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("failed to read config: {0}")]
ReadFailed(#[from] std::io::Error),
#[error("failed to parse config: {0}")]
ParseFailed(#[from] serde_json::Error),
}
#[derive(Deserialize)]
struct Config {
// fields
}
fn read_config(path: &Path) -> Result<Config, ConfigError> {
let data = fs::read_to_string(path)?;
let cfg: Config = serde_json::from_str(&data)?;
Ok(cfg)
}
Custom Error Types
Go:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message)
}
func validate(user *User) error {
if user.Email == "" {
return &ValidationError{
Field: "email",
Message: "cannot be empty",
}
}
return nil
}
Rust:
use thiserror::Error;
#[derive(Debug, Error)]
#[error("validation failed for {field}: {message}")]
struct ValidationError {
field: String,
message: String,
}
fn validate(user: &User) -> Result<(), ValidationError> {
if user.email.is_empty() {
return Err(ValidationError {
field: "email".to_string(),
message: "cannot be empty".to_string(),
});
}
Ok(())
}
Error Handling Best Practices
Go Pattern
Rust Pattern
Notes
if err != nil { return nil, err }
? operator
Automatic propagation
fmt.Errorf("context: %w", err)
map_err(|e| ...)
Add context
Multiple error types
enum with #[from]
Type-safe error conversion
errors.Is(err, target)
match on error type
Pattern matching
errors.As(err, &target)
downcast_ref (rare)
Usually use enums instead
Sentinel errors
Unit variants in enum
enum Error { NotFound }
Concurrency Patterns
Goroutines → Async Tasks
Philosophy Shift:
Go: Goroutines are cheap threads, blocking is fine
Rust: Async tasks are cooperative, blocking requires special handling
Basic Goroutine Translation
Go:
func fetchAll(urls []string) []Result {
results := make(chan Result, len(urls))
for _, url := range urls {
go func(u string) {
results <- fetch(u)
}(url)
}
var out []Result
for i := 0; i < len(urls); i++ {
out = append(out, <-results)
}
return out
}
Rust (with Tokio):
use tokio::task;
async fn fetch_all(urls: Vec<String>) -> Vec<Result> {
let handles: Vec<_> = urls
.into_iter()
.map(|url| task::spawn(async move { fetch(&url).await }))
.collect();
let mut out = Vec::new();
for handle in handles {
if let Ok(result) = handle.await {
out.push(result);
}
}
out
}
// Or using futures::join_all:
use futures::future::join_all;
async fn fetch_all(urls: Vec<String>) -> Vec<Result> {
join_all(urls.iter().map(|url| fetch(url))).await
}
Channels Translation
Go:
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}
func consumer(ch <-chan int) {
for val := range ch {
fmt.Println(val)
}
}
func main() {
ch := make(chan int, 5)
go producer(ch)
consumer(ch)
}
Rust:
use tokio::sync::mpsc;
async fn producer(tx: mpsc::Sender<i32>) {
for i in 0..10 {
let _ = tx.send(i).await;
}
// Channel closes when tx is dropped
}
async fn consumer(mut rx: mpsc::Receiver<i32>) {
while let Some(val) = rx.recv().await {
println!("{}", val);
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(5);
tokio::spawn(async move {
producer(tx).await;
});
consumer(rx).await;
}
Select Statement Translation
Go:
func waitForFirst(ch1, ch2 <-chan string) string {
select {
case msg := <-ch1:
return msg
case msg := <-ch2:
return msg
case <-time.After(1 * time.Second):
return "timeout"
}
}
func workerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Rust:
use tokio::sync::mpsc;
use tokio::task;
async fn worker_pool(
mut jobs: mpsc::Receiver<Job>,
results: mpsc::Sender<Result>,
num_workers: usize,
) {
let mut handles = vec![];
for _ in 0..num_workers {
let mut jobs = jobs.clone();
let results = results.clone();
let handle = task::spawn(async move {
while let Some(job) = jobs.recv().await {
let result = process(job).await;
let _ = results.send(result).await;
}
});
handles.push(handle);
}
drop(jobs); // Close the receiver
for handle in handles {
let _ = handle.await;
}
// results sender dropped, channel closes
}
Sync Primitives Translation
Go
Rust
Notes
sync.Mutex
std::sync::Mutex
Blocking mutex
sync.RWMutex
std::sync::RwLock
Reader-writer lock
sync.WaitGroup
Manual with channels or JoinHandle
No direct equivalent
sync.Once
std::sync::Once
One-time initialization
sync.Cond
std::sync::Condvar
Condition variable
atomic.*
std::sync::atomic::*
Atomic operations
chan T
tokio::sync::mpsc
Async channels
chan T (sync)
std::sync::mpsc
Blocking channels
Async Best Practices
Don't block in async: Use tokio::task::spawn_blocking for CPU-bound work
Use tokio::select! carefully: Branches are not evaluated in order
Handle task panics: Use JoinHandle::await and check results
Use Arc for shared state: Wrap with Mutex or RwLock for mutation
Memory & Ownership
Garbage Collection → Ownership System
Philosophy Shift:
Go: GC handles memory, share references freely
Rust: Ownership rules enforced at compile time, explicit sharing
Ownership Rules in Rust
Each value has exactly one owner
When the owner goes out of scope, the value is dropped
Values can be borrowed (referenced) immutably or mutably
Only one mutable borrow OR multiple immutable borrows at a time
Basic Ownership Translation
Go:
func process() {
data := []int{1, 2, 3, 4, 5}
// Pass by reference (slice is a reference type)
modifyData(data)
// data is modified
fmt.Println(data)
}
func modifyData(d []int) {
d[0] = 99
}
Rust:
fn process() {
let mut data = vec![1, 2, 3, 4, 5];
// Pass mutable borrow
modify_data(&mut data);
// data is modified
println!("{:?}", data);
}
fn modify_data(d: &mut Vec<i32>) {
d[0] = 99;
}
Shared Ownership (Reference Counting)
Go:
type Cache struct {
data map[string]*Value
}
func (c *Cache) Get(key string) *Value {
return c.data[key] // Returns pointer, GC handles lifetime
}
func main() {
cache := &Cache{data: make(map[string]*Value)}
val := cache.Get("key")
// val can outlive cache in Go (GC prevents dangling pointers)
}
Rust:
use std::sync::Arc;
use std::collections::HashMap;
struct Cache {
data: HashMap<String, Arc<Value>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<Arc<Value>> {
self.data.get(key).cloned() // Clone the Arc, not the Value
}
}
fn main() {
let cache = Cache { data: HashMap::new() };
let val = cache.get("key");
// val holds a reference count to Value
}
Move Semantics
Go:
func transfer() {
data := []int{1, 2, 3}
// Both variables point to same backing array
newData := data
// Both can be used
fmt.Println(data)
fmt.Println(newData)
}
Rust:
fn transfer() {
let data = vec![1, 2, 3];
// Ownership moved to new_data
let new_data = data;
// Error: data is no longer valid
// println!("{:?}", data); // Compile error!
println!("{:?}", new_data); // OK
}
// To keep using data, clone it:
fn transfer_with_clone() {
let data = vec![1, 2, 3];
let new_data = data.clone(); // Explicit copy
println!("{:?}", data); // OK
println!("{:?}", new_data); // OK
}
Borrowing Rules
Go:
func example() {
data := []int{1, 2, 3}
// Can pass to multiple functions
read1(data)
read2(data)
modify(data)
}
func read1(d []int) { /* read only */ }
func read2(d []int) { /* read only */ }
func modify(d []int) { d[0] = 99 }
Rust:
fn example() {
let mut data = vec![1, 2, 3];
// Multiple immutable borrows OK
read1(&data);
read2(&data);
// Mutable borrow (must be exclusive)
modify(&mut data);
// Cannot have immutable and mutable borrows simultaneously:
// read1(&data);
// modify(&mut data); // Compile error!
}
fn read1(d: &Vec<i32>) { /* read only */ }
fn read2(d: &Vec<i32>) { /* read only */ }
fn modify(d: &mut Vec<i32>) { d[0] = 99; }
Lifetime Annotations
Go:
type Parser struct {
source string
}
func (p *Parser) NextToken() string {
// Returns slice of source string
// GC ensures source outlives the token
return p.source[0:5]
}
Does data need to be shared across threads?
├─ YES → Arc<T> (thread-safe reference counting)
│ ├─ Needs mutation? → Arc<Mutex<T>> or Arc<RwLock<T>>
│ └─ Read-only? → Arc<T>
└─ NO → Single-threaded sharing
├─ Needs mutation? → Rc<RefCell<T>>
├─ Read-only? → Rc<T>
└─ Exclusive ownership? → Box<T> or owned value
Common Pitfalls
1. Trying to Return Borrowed References Without Lifetimes
Problem:
// Go allows this easily:
// func getData() *Data {
// d := Data{}
// return &d // GC keeps this alive
// }
// Rust compile error:
fn get_data() -> &Data {
let d = Data::new();
&d // Error: returns reference to local variable
}
Solution:
// Return owned value instead:
fn get_data() -> Data {
Data::new()
}
// Or use Box for heap allocation:
fn get_data() -> Box<Data> {
Box::new(Data::new())
}
// Go goroutines can block freely:
// go func() {
// result := expensiveComputation() // Blocking is fine
// ch <- result
// }()
// Rust async tasks should not block:
tokio::spawn(async {
let result = expensive_computation(); // Blocks the runtime!
result
});
Solution:
// Use spawn_blocking for CPU-bound work:
tokio::spawn(async {
let result = tokio::task::spawn_blocking(|| {
expensive_computation()
})
.await
.unwrap();
result
});
4. Misusing unwrap() and expect()
Problem:
// Go forces explicit error handling:
// val, err := doSomething()
// if err != nil { ... }
// Rust makes it easy to panic:
fn process() {
let val = do_something().unwrap(); // Panics on error!
}
Solution:
// Propagate errors with ?:
fn process() -> Result<(), Error> {
let val = do_something()?;
Ok(())
}
// Or handle explicitly:
fn process() {
match do_something() {
Ok(val) => { /* use val */ },
Err(e) => { /* handle error */ },
}
}
// Use &str for parameters when not consuming:
fn greet(name: &str) -> String {
format!("Hello, {}", name)
}
let greeting = greet("World"); // No unnecessary allocation
6. Mutex Deadlocks
Problem:
// Go: defer mu.Unlock() prevents deadlocks
// Rust: forgetting to drop lock guards
fn update(counter: &Mutex<i32>) {
let mut count = counter.lock().unwrap();
*count += 1;
// count guard still held!
let other = counter.lock().unwrap(); // Deadlock!
}
Solution:
// Drop lock guard explicitly or use scopes:
fn update(counter: &Mutex<i32>) {
{
let mut count = counter.lock().unwrap();
*count += 1;
} // count guard dropped here
let other = counter.lock().unwrap(); // OK
}
7. Ignoring Result/Option Types
Problem:
// Compiler warns but doesn't error:
fn main() {
do_something(); // Warning: unused Result
}
Solution:
// Handle or explicitly ignore:
fn main() {
let _ = do_something(); // Explicitly ignore
// Or handle:
if let Err(e) = do_something() {
eprintln!("Error: {}", e);
}
}
8. Overusing Arc<Mutex>
Problem:
// Wrapping everything in Arc<Mutex> because it compiles:
struct App {
config: Arc<Mutex<Config>>, // Config never changes!
cache: Arc<Mutex<Cache>>, // Could use RwLock
}
Solution:
// Use appropriate wrapper for the access pattern:
struct App {
config: Arc<Config>, // Read-only, no mutex needed
cache: Arc<RwLock<Cache>>, // Many readers, few writers
counter: Arc<AtomicUsize>, // Atomic operations are faster
}
Tooling
Translation and Analysis
Tool
Purpose
Notes
c2rust
C to Rust transpiler
Produces unsafe Rust, needs manual cleanup
go2rs
Experimental Go→Rust
Very limited, not production-ready
Manual translation
Most reliable
Use this skill as guide
Rust Ecosystem Equivalents
Category
Go
Rust
Notes
HTTP Client
net/http
reqwest
Async-first
HTTP Server
net/http
axum, actix-web
Framework-based
JSON
encoding/json
serde_json
Type-safe serialization
CLI Parsing
flag, cobra
clap
Derive-based API
Logging
log, zap
tracing, log
Structured logging
Testing
testing
Built-in cargo test
Similar experience
Benchmarking
testing
criterion
Statistical analysis
Async Runtime
Built-in goroutines
tokio, async-std
Explicit runtime
Database
database/sql
sqlx, diesel
Type-safe queries
Error Handling
errors
thiserror, anyhow
Rich error types
Configuration
viper
config, figment
Type-safe configs
Templates
text/template
tera, handlebars
Compile-time checks
Channels
chan
tokio::sync::mpsc
Async-aware
Context
context.Context
Manual (timeout, cancel token)
No built-in equivalent
Reflection
reflect
Limited (Any, procedural macros)
Prefer static typing
Development Tools
Tool
Purpose
cargo
Build system and package manager
rustfmt
Code formatter (like gofmt)
clippy
Linter (more strict than golint)
rust-analyzer
LSP server for IDE support
cargo-watch
Auto-recompile on file changes
cargo-expand
Expand macros for debugging
miri
Detect undefined behavior
Migration Strategy
Start with types: Translate struct definitions and interfaces to Rust types
Port tests: Convert test cases to Rust (validates correctness)
Translate functions: Convert Go functions to Rust, starting with pure functions
Handle concurrency: Replace goroutines/channels with async/await
Refine ownership: Optimize borrows and eliminate unnecessary clones
Benchmark: Compare performance and iterate
Examples
Example 1: Simple - HTTP Client Request
Before (Go):
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUser(id int) (*User, error) {
resp, err := http.Get(fmt.Sprintf("https://api.example.com/users/%d", id))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var user User
if err := json.Unmarshal(body, &user); err != nil {
return nil, err
}
return &user, nil
}
After (Rust):
use serde::Deserialize;
use reqwest;
#[derive(Debug, Deserialize)]
struct User {
id: i32,
name: String,
}
async fn get_user(id: i32) -> Result<User, reqwest::Error> {
let url = format!("https://api.example.com/users/{}", id);
let user = reqwest::get(&url)
.await?
.error_for_status()?
.json::<User>()
.await?;
Ok(user)
}
Key Changes:
async/await instead of blocking calls
reqwest crate for ergonomic HTTP
serde for type-safe JSON deserialization
? operator for error propagation
No manual body reading (handled by .json())
Example 2: Medium - Concurrent File Processing
Before (Go):
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
)
type FileResult struct {
Path string
Lines int
Err error
}
func countLines(path string) (int, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return 0, err
}
lines := 0
for _, b := range data {
if b == '\n' {
lines++
}
}
return lines, nil
}
func processDirectory(dir string) ([]FileResult, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.txt"))
if err != nil {
return nil, err
}
results := make(chan FileResult, len(files))
var wg sync.WaitGroup
for _, file := range files {
wg.Add(1)
go func(path string) {
defer wg.Done()
lines, err := countLines(path)
results <- FileResult{
Path: path,
Lines: lines,
Err: err,
}
}(file)
}
go func() {
wg.Wait()
close(results)
}()
var allResults []FileResult
for result := range results {
allResults = append(allResults, result)
}
return allResults, nil
}
After (Rust):
use std::path::{Path, PathBuf};
use tokio::fs;
use tokio::task;
use glob::glob;
#[derive(Debug)]
struct FileResult {
path: PathBuf,
lines: usize,
error: Option<String>,
}
async fn count_lines(path: &Path) -> Result<usize, std::io::Error> {
let data = fs::read(path).await?;
Ok(data.iter().filter(|&&b| b == b'\n').count())
}
async fn process_directory(dir: &Path) -> Result<Vec<FileResult>, Box<dyn std::error::Error>> {
let pattern = dir.join("*.txt").display().to_string();
let files: Vec<PathBuf> = glob(&pattern)?
.filter_map(Result::ok)
.collect();
let handles: Vec<_> = files
.into_iter()
.map(|path| {
task::spawn(async move {
let lines_result = count_lines(&path).await;
FileResult {
path: path.clone(),
lines: lines_result.as_ref().map(|&l| l).unwrap_or(0),
error: lines_result.err().map(|e| e.to_string()),
}
})
})
.collect();
let mut all_results = Vec::new();
for handle in handles {
if let Ok(result) = handle.await {
all_results.push(result);
}
}
Ok(all_results)
}
// Alternative with futures::join_all:
use futures::future::join_all;
async fn process_directory_v2(dir: &Path) -> Result<Vec<FileResult>, Box<dyn std::error::Error>> {
let pattern = dir.join("*.txt").display().to_string();
let files: Vec<PathBuf> = glob(&pattern)?
.filter_map(Result::ok)
.collect();
let futures = files.into_iter().map(|path| async move {
let lines_result = count_lines(&path).await;
FileResult {
path: path.clone(),
lines: lines_result.as_ref().map(|&l| l).unwrap_or(0),
error: lines_result.err().map(|e| e.to_string()),
}
});
Ok(join_all(futures).await)
}
Key Changes:
tokio::task::spawn instead of goroutines
Async file I/O with tokio::fs
Iterator methods for concise code
Type-safe error handling with Result
No manual channel management (collect into Vec)
Example 3: Complex - Web Server with Middleware
Before (Go):
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"time"
)
type Server struct {
db Database
cache Cache
}
type Database interface {
GetUser(ctx context.Context, id string) (*User, error)
CreateUser(ctx context.Context, user *User) error
}
type Cache interface {
Get(key string) (interface{}, bool)
Set(key string, value interface{}, ttl time.Duration)
}
type User struct {
ID string `json:"id"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type CreateUserRequest struct {
Email string `json:"email"`
}
func (s *Server) loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("Started %s %s", r.Method, r.URL.Path)
next(w, r)
log.Printf("Completed in %v", time.Since(start))
}
}
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id parameter", http.StatusBadRequest)
return
}
// Check cache first
if cached, ok := s.cache.Get("user:" + id); ok {
user := cached.(*User)
json.NewEncoder(w).Encode(user)
return
}
// Fetch from database
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
user, err := s.db.GetUser(ctx, id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Cache the result
s.cache.Set("user:"+id, user, 5*time.Minute)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
user := &User{
ID: generateID(),
Email: req.Email,
CreatedAt: time.Now(),
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
if err := s.db.CreateUser(ctx, user); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
server := &Server{
db: &PostgresDB{},
cache: &RedisCache{},
}
http.HandleFunc("/users", server.loggingMiddleware(server.getUser))
http.HandleFunc("/users/create", server.loggingMiddleware(server.createUser))
log.Fatal(http.ListenAndServe(":8080", nil))
}
func generateID() string {
return "user-123" // Simplified
}