Hello World & Build Tools
Hello, World
fn main() {
println!("Hello, World!");
} package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
} Go uses
fmt.Println where Rust uses the println! macro. In Go, fmt is an explicit import from the standard library — no implicit prelude. The package main declaration and the func main() entry point are mandatory for an executable program; missing either is a build error.Build tools
// Cargo (built into Rust):
// cargo new myproject — create new project
// cargo build — debug build → target/debug/
// cargo build --release — optimized → target/release/
// cargo run — build + run
// cargo test — run tests
// cargo add serde — add a dependency (Cargo.toml + Cargo.lock)
// cargo fmt — format code (rustfmt)
// cargo clippy — lint // Go toolchain (part of the go binary):
// go mod init myproject — initialize a module (go.mod + go.sum)
// go build ./... — compile all packages
// go run main.go — compile + run one file
// go test ./... — run all tests
// go get github.com/pkg — add a dependency
// go fmt ./... — format code (gofmt built in)
// go vet ./... — lint
// go build -o mybin . — output named binary Both languages ship a single tool that handles building, testing, formatting, and dependency management. Go's
go fmt is opinionated and non-configurable — there is no style debate, only one canonical format. Cargo uses Cargo.toml (TOML) for manifest and Cargo.lock for reproducible builds; Go uses go.mod and go.sum. Go compiles substantially faster than Rust for most programs.Formatted output
fn main() {
let name = "Alice";
let score = 42;
let ratio = 0.75;
println!("{} scored {} ({:.1}%)", name, score, ratio * 100.0);
eprintln!("debug: score={}", score); // stderr
} package main
import (
"fmt"
"os"
)
func main() {
name := "Alice"
score := 42
ratio := 0.75
fmt.Printf("%s scored %d (%.1f%%)\n", name, score, ratio*100)
fmt.Fprintf(os.Stderr, "debug: score=%d\n", score) // writes to stderr
fmt.Println(fmt.Sprintf("built: %s=%d", name, score))
} Go uses C-style
%s, %d, %f format verbs in fmt.Printf, while Rust uses positional {} placeholders. Go's %v is the universal verb — it formats any value in a default way, similar to Rust's {:?}. fmt.Sprintf returns a formatted string (like Rust's format!). fmt.Fprintf writes to any io.Writer, like os.Stderr. % in a format string must be doubled as %% in both languages.Variables & Types
Type inference (:= vs let)
fn main() {
let count = 0; // i32
let ratio = 3.14; // f64
let message = "hello"; // &str
println!("{} {} {}", count, ratio, message);
} package main
import "fmt"
func main() {
count := 0 // int
ratio := 3.14 // float64
message := "hello" // string
fmt.Println(count, ratio, message)
} Go's
:= short variable declaration infers the type and declares the variable in one step — equivalent to Rust's let with inference. Go's default integer type is int (platform-sized, typically 64-bit), while Rust defaults to i32. The := syntax is only valid inside functions; package-level variables require the var keyword with an explicit type.Mutability — all variables are mutable
fn main() {
let count = 0; // immutable by default
// count += 1; // compile error
let mut total = 0; // must opt into mutation
total += 1;
println!("{}", total);
} package main
import "fmt"
func main() {
total := 0 // all Go variables are mutable
total += 1
fmt.Println(total)
// There is no const-binding at function scope — use const for true constants:
const limit = 100
fmt.Println(limit)
} In Rust, bindings are immutable by default and you must write
let mut to allow mutation — a key safety guarantee that prevents accidental modification. Go has no equivalent: every local variable is mutable after declaration. Package-level const is compile-time constant in both languages, but Go has no way to declare an immutable local binding. Coming from Rust, this feels like losing a safety net.Integer types
fn main() {
let small: i8 = 127;
let medium: i32 = 2_000_000;
let large: i64 = i64::MAX;
let size: usize = 100; // for indexing
println!("{} {} {} {}", small, medium, large, size);
} package main
import (
"fmt"
"math"
)
func main() {
var small int8 = 127
var medium int32 = 2_000_000
var large int64 = math.MaxInt64
var size int = 100 // int is the indexing type in Go
fmt.Println(small, medium, large, size)
} Go's integer types mirror Rust's naming but drop the prefix:
int8, int16, int32, int64, uint8, … vs Rust's i8, i16, i32, i64, u8, …. Go's int is the general-purpose type (like Rust's isize); Rust defaults to i32. Go has byte (alias for uint8) and rune (alias for int32, for Unicode code points). Both languages allow digit separators: 1_000_000.Zero values — no uninitialized variables
fn main() {
// Rust: you cannot read an uninitialized variable — compile error.
// Every variable must be initialized before use.
let count: i32;
// println!("{}", count); // error: used before initialization
let initialized = 0; // must supply a value
println!("{}", initialized);
} package main
import "fmt"
func main() {
// Go: every type has a zero value; declaration without init is valid.
var count int // 0
var ratio float64 // 0.0
var message string // ""
var found bool // false
var pointer *int // nil
fmt.Println(count, ratio, message, found, pointer)
} Go guarantees every variable is initialized to its type's zero value on declaration — there are no uninitialized variables and no undefined behavior from reading them. Rust achieves the same safety differently: it is a compile-time error to read a variable before assigning it. Go's zero values (
0, 0.0, "", false, nil) are idiomatic and relied upon — a var counter int counter starts at zero without an explicit = 0.Pointers — safe, no arithmetic
fn main() {
let mut value = 42;
let reference = &mut value; // mutable reference — borrow checker enforces exclusive access
*reference += 1;
println!("{}", value); // 43
} package main
import "fmt"
func main() {
value := 42
pointer := &value // take address with &
*pointer += 1 // dereference with *
fmt.Println(value) // 43
// new() allocates a zero-initialized value on the heap
heap := new(int)
*heap = 100
fmt.Println(*heap)
} Go pointers use the same
& and * syntax as Rust references, but there is no borrow checker, no lifetimes, and no exclusivity rules. Multiple goroutines can hold pointers to the same value — the compiler does not prevent data races (use sync.Mutex or channels instead). Go has no pointer arithmetic; unsafe pointer casting requires the unsafe package. Go pointers are garbage-collected, so there is no use-after-free.Type conversions — explicit casts
fn main() {
let count: i32 = 42;
let total: i64 = count as i64; // explicit widening
let ratio: f64 = count as f64; // int to float
let back: i32 = ratio as i32; // float to int (truncates)
println!("{} {} {}", total, ratio, back);
} package main
import "fmt"
func main() {
count := 42
total := int64(count) // explicit widening — T(expr) syntax
ratio := float64(count) // int to float
back := int(ratio) // float to int (truncates)
fmt.Println(total, ratio, back)
} Both languages require explicit type conversions — no implicit numeric coercion. Go's syntax is
T(expr) (a function-call-like cast), while Rust uses the as keyword. Overflow behavior differs: Rust's as truncates for narrowing casts; Go panics in debug mode for out-of-range conversions to smaller integer types. Neither language has Rust's try_from checked conversion in the standard library — Go callers handle range errors manually.Strings
String types — one vs two
fn main() {
let borrowed: &str = "hello"; // string slice — no allocation
let owned: String = String::from("world");
let combined: String = format!("{} {}", borrowed, owned);
println!("{}", combined);
} package main
import "fmt"
func main() {
// Go has one string type — always UTF-8, always immutable, reference-counted.
literal := "hello"
built := fmt.Sprintf("%s %s", literal, "world")
fmt.Println(built)
// A string is a (pointer, length) pair — similar to Rust &str.
// There is no separate owned-string type like Rust's String.
length := len(literal)
fmt.Println(length)
} Rust has two string types:
&str (borrowed slice, no allocation) and String (owned heap buffer). Go has one: string, which is immutable and internally a pointer+length pair, similar to &str. String operations in Go that produce new strings always allocate a new value. There is no distinct "owned string" type — concatenation or formatting creates a new string directly.Common string methods
fn main() {
let text = " Hello, World! ";
println!("{}", text.trim());
println!("{}", text.to_lowercase());
println!("{}", text.contains("World"));
println!("{}", text.replace("World", "Go"));
println!("{}", text.trim().len());
} package main
import (
"fmt"
"strings"
)
func main() {
text := " Hello, World! "
fmt.Println(strings.TrimSpace(text))
fmt.Println(strings.ToLower(text))
fmt.Println(strings.Contains(text, "World"))
fmt.Println(strings.ReplaceAll(text, "World", "Go"))
fmt.Println(len(strings.TrimSpace(text)))
} In Rust, string methods are called on the value directly (
text.trim(), text.contains()). In Go, string operations are free functions in the strings package — strings.TrimSpace, strings.Contains, strings.ReplaceAll. Go's len(s) returns the number of bytes, not Unicode characters; use utf8.RuneCountInString(s) for character count.Split and join
fn main() {
let sentence = "one two three";
let words: Vec<&str> = sentence.split(' ').collect();
println!("{:?}", words);
let rejoined = words.join(", ");
println!("{}", rejoined);
} package main
import (
"fmt"
"strings"
)
func main() {
sentence := "one two three"
words := strings.Split(sentence, " ")
fmt.Println(words)
rejoined := strings.Join(words, ", ")
fmt.Println(rejoined)
} Both
Split and Join live in Go's strings package as free functions, while Rust provides them as methods on the str and slice types. Go's strings.Split always returns a []string — unlike Rust's split, which returns a lazy iterator that must be .collect()ed. fmt.Println with a slice prints [one two three], not the Rust debug format ["one", "two", "three"].Iterating over characters
fn main() {
let greeting = "Hello, 世界";
for character in greeting.chars() {
print!("{} ", character);
}
println!();
println!("chars: {}", greeting.chars().count());
println!("bytes: {}", greeting.len());
} package main
import (
"fmt"
"unicode/utf8"
)
func main() {
greeting := "Hello, 世界"
for _, character := range greeting { // range yields (byte_index, rune)
fmt.Printf("%c ", character)
}
fmt.Println()
fmt.Println("runes:", utf8.RuneCountInString(greeting))
fmt.Println("bytes:", len(greeting))
} Rust's
.chars() iterator yields Unicode scalar values (Rust's char). Go's for _, r := range string also yields Unicode code points (rune, alias for int32) with their byte index. In both languages, len returns byte count, not character count. Go's utf8.RuneCountInString gives character count, like Rust's .chars().count(). The blank identifier _ discards the byte index.Collections
Fixed-size arrays
fn main() {
let scores: [i32; 5] = [10, 20, 30, 40, 50];
let total: i32 = scores.iter().sum();
println!("total: {}", total);
println!("first: {}", scores[0]);
} package main
import "fmt"
func main() {
scores := [5]int{10, 20, 30, 40, 50}
total := 0
for _, value := range scores {
total += value
}
fmt.Println("total:", total)
fmt.Println("first:", scores[0])
} Go and Rust both have fixed-size arrays with the size as part of the type. Rust writes
[i32; 5] (type then size); Go writes [5]int (size then type). Go arrays are values — assigning one copies the entire backing array. In practice, Go code rarely uses fixed arrays directly; slices ([]T) are the idiomatic choice, equivalent to Rust's Vec<T> or &[T].Slices — the idiomatic dynamic collection
fn main() {
let mut numbers: Vec<i32> = vec![1, 2, 3];
numbers.push(4);
numbers.push(5);
println!("len={} cap={}", numbers.len(), numbers.capacity());
let middle = &numbers[1..4]; // &[i32] slice
println!("{:?}", middle);
} package main
import "fmt"
func main() {
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
numbers = append(numbers, 5)
fmt.Printf("len=%d cap=%d\n", len(numbers), cap(numbers))
middle := numbers[1:4] // []int slice — shares backing array
fmt.Println(middle)
} Go's
[]T slice is the closest equivalent to Rust's Vec<T> or &[T]. A slice is a (pointer, length, capacity) triple. Growing with append may allocate a new backing array if capacity is exceeded — the slice header returned by append may point to new memory, so always reassign: numbers = append(numbers, value). Slicing with [1:4] shares the backing array; modifying a sub-slice modifies the original, which differs from Rust's non-overlapping borrow rules.Maps (HashMap vs map)
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert("Alice".to_string(), 95);
scores.insert("Bob".to_string(), 80);
if let Some(&score) = scores.get("Alice") {
println!("Alice: {}", score);
}
println!("total players: {}", scores.len());
} package main
import "fmt"
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 80,
}
if score, found := scores["Alice"]; found {
fmt.Println("Alice:", score)
}
fmt.Println("total players:", len(scores))
} Go's
map[K]V is a built-in type — no import needed. Map literals use map[K]V{ key: value } syntax. Lookup returns two values: value, ok := m[key] — the ok-idiom is idiomatic Go for checking presence, unlike Rust's Option. Accessing a missing key returns the zero value for V (not a panic); this silently introduces bugs if you forget to check ok.Iteration with index
fn main() {
let names = vec!["Alice", "Bob", "Carol"];
for name in &names {
println!("{}", name);
}
for (index, name) in names.iter().enumerate() {
println!("{}: {}", index, name);
}
} package main
import "fmt"
func main() {
names := []string{"Alice", "Bob", "Carol"}
for _, name := range names {
fmt.Println(name)
}
for index, name := range names {
fmt.Printf("%d: %s\n", index, name)
}
} Go's
range over a slice yields two values per iteration: the index and a copy of the element. The blank identifier _ discards the index when only the value is needed. Unlike Rust's iter().enumerate(), there is no separate method call — range always provides the index. For maps, range yields key–value pairs; for strings, it yields byte-index and rune pairs.Pre-allocating slices with make
fn main() {
// Vec::with_capacity pre-allocates without setting length:
let mut items: Vec<i32> = Vec::with_capacity(10);
for i in 0..5 {
items.push(i * i);
}
println!("{:?}", items); // [0, 1, 4, 9, 16]
// Collect into a Vec from an iterator:
let squares: Vec<i32> = (0..5).map(|x| x * x).collect();
println!("{:?}", squares);
} package main
import "fmt"
func main() {
// make([]T, length, capacity) — pre-allocates backing array:
items := make([]int, 0, 10)
for i := 0; i < 5; i++ {
items = append(items, i*i)
}
fmt.Println(items) // [0 1 4 9 16]
// make with non-zero length fills with zero values:
zeroed := make([]int, 5)
fmt.Println(zeroed) // [0 0 0 0 0]
} Go's
make([]T, length, capacity) pre-allocates a slice with the given backing array size — equivalent to Rust's Vec::with_capacity. The length and capacity arguments are separate: make([]int, 0, 10) creates an empty slice with room for 10 elements, while make([]int, 5) creates a slice of five zero-initialized elements. Go has no iterator chains like map/filter/collect — use a for loop instead.Control Flow
if — statement in Go, expression in Rust
fn main() {
let score = 75;
// if is an expression in Rust:
let grade = if score >= 60 { "pass" } else { "fail" };
println!("{}", grade);
} package main
import "fmt"
func main() {
score := 75
// if is a statement in Go — cannot be used as an expression:
var grade string
if score >= 60 {
grade = "pass"
} else {
grade = "fail"
}
fmt.Println(grade)
// Go does allow an init statement before the condition:
if result := score * 2; result > 100 {
fmt.Println("over 100")
}
} Rust treats
if as an expression that yields a value from the last expression in each arm. Go's if is a statement — it cannot produce a value inline. Use a pre-declared variable and assign inside each branch. Go adds a convenient init clause before the condition (if err := call(); err != nil) that is idiomatic for error checks. Go does not require parentheses around the condition; Rust does not use them either.for loops — the only loop keyword in Go
fn main() {
for i in 0..5 {
print!("{} ", i);
}
println!();
let mut counter = 0;
loop { // infinite loop with break
if counter == 3 { break; }
counter += 1;
}
println!("counter: {}", counter);
} package main
import "fmt"
func main() {
for i := 0; i < 5; i++ {
fmt.Printf("%d ", i)
}
fmt.Println()
counter := 0
for { // bare for = infinite loop
if counter == 3 {
break
}
counter++
}
fmt.Println("counter:", counter)
} Go has only one loop keyword —
for — which covers all loop shapes: C-style three-clause (for init; cond; post), while-style (for condition), infinite (for {}), and range-based. Rust uses for for iterators, while for conditions, and loop for infinite loops. Go has no range expression syntax like Rust's 0..5; numeric loops require the full for i := 0; i < 5; i++ form.switch vs match
fn main() {
let value = 3;
let description = match value {
1 => "one",
2 | 3 => "two or three",
4..=9 => "four to nine",
_ => "other",
};
println!("{}", description);
} package main
import "fmt"
func main() {
value := 3
var description string
switch value {
case 1:
description = "one"
case 2, 3: // comma separates alternatives
description = "two or three"
default:
description = "other"
}
fmt.Println(description)
// switch with no expression = cleaner if-else chain:
switch {
case value < 0:
fmt.Println("negative")
case value == 0:
fmt.Println("zero")
default:
fmt.Println("positive")
}
} Go's
switch and Rust's match both check conditions without fall-through by default (unlike C). Key differences: Go switch is a statement (cannot produce a value inline); Rust match is an expression. Go uses commas to separate case alternatives (case 2, 3:) where Rust uses |. Go has no range syntax in cases. A switch {} with no expression acts as a cleaner if/else if/else chain.break and continue with labels
fn main() {
'outer: for row in 0..3 {
for col in 0..3 {
if row == 1 && col == 1 {
break 'outer;
}
println!("({}, {})", row, col);
}
}
} package main
import "fmt"
func main() {
outer:
for row := 0; row < 3; row++ {
for col := 0; col < 3; col++ {
if row == 1 && col == 1 {
break outer
}
fmt.Printf("(%d, %d)\n", row, col)
}
}
} Both Go and Rust support labeled
break and continue to target an outer loop. The syntax differs: Rust uses 'label (lifetime-style with a tick prefix), while Go uses a plain identifier before the loop with no special prefix. Go labels are written on a separate line above the for. In Go, break and continue without a label target the innermost loop, the same as Rust.Panics and recovery
fn main() {
// Rust panics terminate the thread (or the process if single-threaded).
// Use .expect() for programmer errors:
let items: Vec<i32> = vec![1, 2, 3];
let first = items.first().expect("slice should not be empty");
println!("{}", first);
// Never use panic! for expected errors — use Result instead.
} package main
import "fmt"
func safeOperation() (result int, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered from panic: %v", recovered)
}
}()
panic("something went wrong")
}
func main() {
result, err := safeOperation()
if err != nil {
fmt.Println("caught:", err)
} else {
fmt.Println("result:", result)
}
} Both languages have a
panic mechanism for unrecoverable programmer errors. Go adds recover() — a function called inside a deferred function that catches a panic and allows the goroutine to continue; this is how Go libraries prevent panics from escaping to callers. Rust has no equivalent to recover — a panic unwinds the stack and the thread terminates. In both languages, panics should be reserved for bugs; use Result in Rust or (value, error) in Go for expected failures.Functions & Closures
Function definitions
fn add(first: i32, second: i32) -> i32 {
first + second // implicit return — last expression
}
fn main() {
println!("{}", add(3, 4));
} package main
import "fmt"
func add(first int, second int) int {
return first + second // explicit return required
}
// Adjacent same-type params can share the type:
func multiply(first, second int) int {
return first * second
}
func main() {
fmt.Println(add(3, 4))
fmt.Println(multiply(3, 4))
} Go function syntax is
func name(param Type) ReturnType — no -> arrow. Go requires an explicit return; there are no implicit-return expressions. Adjacent parameters of the same type can share a single type annotation: func add(first, second int). Functions at the top level are always visible within the package; exported functions (visible outside the package) start with an uppercase letter.Multiple return values
fn min_max(numbers: &[i32]) -> (i32, i32) {
let minimum = *numbers.iter().min().unwrap();
let maximum = *numbers.iter().max().unwrap();
(minimum, maximum)
}
fn main() {
let (minimum, maximum) = min_max(&[3, 1, 4, 1, 5, 9]);
println!("min={} max={}", minimum, maximum);
} package main
import "fmt"
func minMax(numbers []int) (int, int) {
minimum, maximum := numbers[0], numbers[0]
for _, value := range numbers[1:] {
if value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
}
return minimum, maximum
}
func main() {
minimum, maximum := minMax([]int{3, 1, 4, 1, 5, 9})
fmt.Printf("min=%d max=%d\n", minimum, maximum)
} Go has first-class multiple return values — a function can return any number of values without wrapping them in a tuple or struct. This is used heavily for the
(value, error) error-handling pattern. Rust uses tuples for multiple returns: (T, U) which must be destructured on the calling side. Go also supports named return values — variables declared in the signature, used by a bare return — though this is best reserved for short functions.Closures and first-class functions
fn apply(operation: impl Fn(i32) -> i32, value: i32) -> i32 {
operation(value)
}
fn main() {
let multiplier = 3;
let triple = |x| x * multiplier; // captures multiplier
println!("{}", apply(triple, 7)); // 21
} package main
import "fmt"
func apply(operation func(int) int, value int) int {
return operation(value)
}
func main() {
multiplier := 3
triple := func(x int) int { // anonymous function captures multiplier
return x * multiplier
}
fmt.Println(apply(triple, 7)) // 21
} Go closures use the
func(params) ReturnType { body } syntax — the same keyword as named functions, just without a name. They capture variables by reference, not by value: if the outer variable changes after the closure is created, the closure sees the updated value. Rust closures with |params| syntax are more explicit about capture modes — move forces value capture. Go's function types are written as func(int) int rather than Rust's Fn(i32) -> i32 trait bound.Variadic functions
fn sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
fn main() {
// Rust uses slice arguments, not true variadic syntax.
println!("{}", sum(&[1, 2, 3, 4, 5]));
} package main
import "fmt"
func sum(numbers ...int) int {
total := 0
for _, number := range numbers {
total += number
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spread an existing slice with ...:
values := []int{10, 20, 30}
fmt.Println(sum(values...)) // 60
} Go supports true variadic functions with
...T as the last parameter — the argument is received as a []T slice. Call it with a spread using slice... to pass an existing slice. Rust does not have variadic functions for user-defined code; the idiomatic alternative is a slice argument (&[T]). Rust's println! and format! are macros, not functions, which is why they accept varying argument counts.Structs & Methods
Struct definitions
struct Point {
x: f64,
y: f64,
}
fn main() {
let origin = Point { x: 0.0, y: 0.0 };
let corner = Point { x: 3.0, y: 4.0 };
println!("({}, {})", corner.x, corner.y);
let _ = origin;
} package main
import "fmt"
type Point struct {
X float64 // uppercase = exported (public)
Y float64
}
func main() {
origin := Point{X: 0, Y: 0}
corner := Point{X: 3, Y: 4}
fmt.Printf("(%g, %g)\n", corner.X, corner.Y)
_ = origin
} Go struct definitions use
type Name struct { ... }. Field visibility is controlled by capitalization — uppercase fields are exported (public), lowercase fields are unexported (package-private). Rust controls visibility with pub keyword per field. Go struct literals always use named fields (Point{X: 3, Y: 4}), though positional initialization is technically allowed for unexported fields — named form is strongly preferred.Methods — receivers vs impl
struct Point { x: f64, y: f64 }
impl Point {
fn new(x: f64, y: f64) -> Self { Self { x, y } }
fn distance(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let point = Point::new(3.0, 4.0);
println!("{}", point.distance());
} package main
import (
"fmt"
"math"
)
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
func (point Point) Distance() float64 { // value receiver
return math.Sqrt(point.X*point.X + point.Y*point.Y)
}
func main() {
point := NewPoint(3, 4)
fmt.Println(point.Distance())
} Go methods are defined with a receiver before the function name:
func (p Point) Method(). There is no impl block — methods can be declared anywhere in the same package. A value receiver (p Point) copies the struct, like Rust's &self but without borrowing; a pointer receiver (p *Point) allows mutation, like Rust's &mut self. Constructor functions like NewPoint are idiomatic in Go — there is no special Self::new syntax.Struct embedding — composition without inheritance
// Rust has no struct embedding; use composition + delegation or traits.
struct Animal { name: String }
struct Dog { inner: Animal, breed: String }
impl Dog {
fn name(&self) -> &str { &self.inner.name }
}
fn main() {
let dog = Dog {
inner: Animal { name: "Rex".to_string() },
breed: "Labrador".to_string(),
};
println!("{}", dog.name());
} package main
import "fmt"
type Animal struct {
Name string
}
func (animal Animal) Speak() string {
return animal.Name + " says hello"
}
type Dog struct {
Animal // embedded — Dog promotes Animal's fields and methods
Breed string
}
func main() {
dog := Dog{
Animal: Animal{Name: "Rex"},
Breed: "Labrador",
}
fmt.Println(dog.Name) // promoted field
fmt.Println(dog.Speak()) // promoted method
fmt.Println(dog.Breed)
} Go's struct embedding promotes the embedded type's fields and methods to the outer struct —
dog.Name and dog.Speak() work without any delegation boilerplate. This is Go's answer to inheritance: composition with automatic promotion, not subtyping. Rust achieves the same effect with explicit delegation (implementing a trait or method that forwards to the inner field). Embedding is not subtyping in Go — a Dog is not a Animal; you cannot pass a Dog where an Animal is expected.Struct field tags (JSON serialization)
// Rust uses #[derive] macros + the serde crate for serialization.
// (Requires adding serde and serde_json to Cargo.toml — cannot run here.)
// use serde::{Serialize, Deserialize};
// #[derive(Serialize, Deserialize)]
// struct Person { name: String, age: u32 } package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
func main() {
person := Person{Name: "Alice", Age: 30}
data, _ := json.Marshal(person)
fmt.Println(string(data))
var decoded Person
json.Unmarshal(data, &decoded)
fmt.Println(decoded.Name, decoded.Age)
} Go struct tags are string literals in backticks after a field declaration that carry metadata for packages like
encoding/json. The json:"name" tag controls the JSON key name; omitempty omits the field when it is the zero value. This is a runtime reflection feature — the JSON package reads tags at runtime using reflect. Rust's serde crate achieves the same result at compile time via derive macros, which is faster but requires an explicit dependency.Interfaces
Interfaces — implicit vs explicit trait impl
trait Speaker {
fn speak(&self) -> String;
}
struct Dog { name: String }
impl Speaker for Dog {
fn speak(&self) -> String {
format!("{} says: Woof!", self.name)
}
}
fn make_sound(speaker: &dyn Speaker) {
println!("{}", speaker.speak());
}
fn main() {
let dog = Dog { name: "Rex".to_string() };
make_sound(&dog);
} package main
import "fmt"
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
// Dog satisfies Speaker implicitly — no "implements Speaker" declaration needed.
func (dog Dog) Speak() string {
return dog.Name + " says: Woof!"
}
func makeSound(speaker Speaker) {
fmt.Println(speaker.Speak())
}
func main() {
dog := Dog{Name: "Rex"}
makeSound(dog)
} Go interfaces are satisfied implicitly — if a type has all the methods the interface requires, it satisfies the interface. There is no
impl Speaker for Dog declaration. Rust requires explicit impl Trait for Type blocks. Go's implicit satisfaction enables retroactive implementation: you can make an existing type (even one from another package) satisfy a new interface you define, without modifying the original type. This is structural typing, while Rust uses nominal typing.Dynamic dispatch with interfaces
trait Shape {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } }
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } }
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 5.0 }),
Box::new(Rectangle { width: 3.0, height: 4.0 }),
];
for shape in &shapes {
println!("{:.2}", shape.area());
}
} package main
import (
"fmt"
"math"
)
type Shape interface {
Area() float64
}
type Circle struct{ Radius float64 }
type Rectangle struct{ Width, Height float64 }
func (circle Circle) Area() float64 { return math.Pi * circle.Radius * circle.Radius }
func (rect Rectangle) Area() float64 { return rect.Width * rect.Height }
func main() {
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 3, Height: 4},
}
for _, shape := range shapes {
fmt.Printf("%.2f\n", shape.Area())
}
} Go interfaces enable dynamic dispatch through a (type, pointer) pair at runtime — similar to Rust's
Box<dyn Trait>. Go does not require Box or any heap allocation for the interface value itself (though the concrete value may be heap-allocated). A Go interface variable holds a pointer to the concrete value and a pointer to the method table. Interface slices ([]Shape) are idiomatic in Go; Rust requires explicit boxing (Vec<Box<dyn Shape>>) for heterogeneous collections.The any / interface{} type
use std::any::Any;
fn print_value(value: &dyn Any) {
if let Some(integer) = value.downcast_ref::<i32>() {
println!("i32: {}", integer);
} else if let Some(text) = value.downcast_ref::<String>() {
println!("String: {}", text);
}
}
fn main() {
print_value(&42_i32);
print_value(&"hello".to_string());
} package main
import "fmt"
func printValue(value any) { // any = interface{} — accepts any type
switch typed := value.(type) {
case int:
fmt.Println("int:", typed)
case string:
fmt.Println("string:", typed)
default:
fmt.Printf("unknown: %T\n", typed)
}
}
func main() {
printValue(42)
printValue("hello")
printValue(3.14)
} Go's
any (an alias for interface{}, introduced in Go 1.18) is the universal type — any value satisfies the empty interface. The type switch (switch v := x.(type)) is Go's equivalent of Rust's downcast_ref. Go's type switch is cleaner syntactically; Rust's std::any::Any requires explicit downcast_ref calls. Overusing any sacrifices compile-time type safety in Go just as overusing dyn Any does in Rust.Interface embedding (composition)
use std::fmt;
// Rust: combine multiple trait bounds with +
fn describe<T: fmt::Display + fmt::Debug>(value: T) {
println!("Display: {}", value);
println!("Debug: {:?}", value);
}
fn main() {
describe(42_i32);
} package main
import (
"fmt"
"io"
)
// ReadWriter embeds both Reader and Writer interfaces.
// Any type implementing both Read and Write satisfies ReadWriter.
type ReadWriter interface {
io.Reader
io.Writer
}
func copyAll(source io.Reader, destination io.Writer) (int64, error) {
return io.Copy(destination, source)
}
func main() {
// bytes.Buffer satisfies ReadWriter — it has both Read and Write methods.
// Demonstrate with simple Stringer:
type Stringer interface {
String() string
}
_ = fmt.Stringer(nil) // fmt.Stringer is an embedded-style interface
fmt.Println("io.ReadWriter embeds io.Reader + io.Writer")
_ = copyAll // show the func is defined
} Go interfaces can embed other interfaces — the resulting interface requires all methods from all embedded interfaces. This is how Go's standard library composes I/O abstractions:
io.ReadWriter embeds io.Reader and io.Writer. In Rust, the equivalent is trait bounds with +: T: Read + Write. Go's embedding applies at the interface definition level, not just at the call site, which gives the composition a name that can appear in type signatures.nil vs Option
nil vs Option<T>
fn find_user(identifier: u32) -> Option<String> {
if identifier == 1 {
Some("Alice".to_string())
} else {
None
}
}
fn main() {
match find_user(1) {
Some(name) => println!("found: {}", name),
None => println!("not found"),
}
// Compiler forces you to handle the None case.
} package main
import "fmt"
func findUser(identifier int) *string {
if identifier == 1 {
name := "Alice"
return &name
}
return nil // pointers, slices, maps, channels, funcs, and interfaces can be nil
}
func main() {
user := findUser(1)
if user != nil {
fmt.Println("found:", *user)
} else {
fmt.Println("not found")
}
// Nothing stops you from dereferencing user without checking — nil panic at runtime.
} Rust replaces nullable values with
Option<T>, and the compiler forces every call site to handle the None case — you cannot accidentally ignore the absence of a value. Go uses nil, which is the zero value for pointers, slices, maps, channels, functions, and interfaces. A nil check is idiomatic but not enforced by the compiler: forgetting to check triggers a nil pointer dereference panic at runtime. This is the most significant safety difference between the two languages.The ok-idiom (map lookup, type assertion)
use std::collections::HashMap;
fn main() {
let mut scores: HashMap<&str, i32> = HashMap::new();
scores.insert("Alice", 95);
// Option forces handling the missing-key case:
match scores.get("Bob") {
Some(&score) => println!("Bob: {}", score),
None => println!("Bob not found"),
}
// unwrap_or for a default:
let score = scores.get("Bob").copied().unwrap_or(0);
println!("Bob's score: {}", score);
} package main
import "fmt"
func main() {
scores := map[string]int{"Alice": 95}
// Two-value map lookup — ok is false if key is absent:
score, found := scores["Bob"]
if found {
fmt.Println("Bob:", score)
} else {
fmt.Println("Bob not found")
}
// Without the ok-check, missing key returns zero value (0 for int):
defaultScore := scores["Bob"] // 0 — silently, no error
fmt.Println("default:", defaultScore)
} Go's two-value map lookup
value, ok := m[key] is the idiomatic way to distinguish "key absent" from "key present with zero value". Without the ok check, a missing key silently returns the zero value — 0 for integers, "" for strings. Rust's HashMap::get returns Option<&V>, so the absent case is impossible to ignore. The same value, ok pattern applies to type assertions in Go: typed, ok := value.(ConcreteType).Nil slices and maps
fn main() {
// Rust: Option wraps a Vec when the collection itself can be absent.
let maybe_items: Option<Vec<i32>> = None;
let items = maybe_items.unwrap_or_default(); // empty Vec
println!("{:?}", items);
println!("len: {}", items.len());
} package main
import "fmt"
func main() {
// Go: a nil slice and a nil map behave like empty ones for most reads.
var items []int // nil slice
fmt.Println(items) // []
fmt.Println(len(items)) // 0
// Appending to a nil slice works — append allocates:
items = append(items, 1, 2, 3)
fmt.Println(items)
// But writing to a nil map panics:
var scores map[string]int // nil map
fmt.Println(scores["Alice"]) // ok — returns zero value
// scores["Bob"] = 99 // PANIC: assignment to entry in nil map
scores = make(map[string]int) // initialize before writing
scores["Bob"] = 99
fmt.Println(scores["Bob"])
} Go nil slices behave like empty slices for reads —
len(nil_slice) is 0 and range over it is a no-op. Appending to a nil slice allocates and works correctly. Nil maps also safely return zero values on read, but writing to a nil map panics at runtime. Always initialize a map with make(map[K]V) or a map literal before writing to it. This asymmetry between slices (nil-safe for append) and maps (nil-unsafe for writes) is a common Go gotcha for Rust programmers.Error Handling
Result<T,E> vs (T, error)
use std::num::ParseIntError;
fn parse_port(input: &str) -> Result<u16, ParseIntError> {
let number: u16 = input.parse()?;
Ok(number)
}
fn main() {
match parse_port("8080") {
Ok(port) => println!("port: {}", port),
Err(err) => println!("error: {}", err),
}
} package main
import (
"fmt"
"strconv"
)
func parsePort(input string) (uint16, error) {
number, err := strconv.ParseUint(input, 10, 16)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", input, err)
}
return uint16(number), nil
}
func main() {
port, err := parsePort("8080")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("port:", port)
} Go's error handling pattern is to return
(value, error) as two separate return values, check with if err != nil, and return 0, err or nil, err to propagate. Rust encodes success and failure in Result<T, E> — a sum type — and propagates with the ? operator. Go's approach is more verbose but no special syntax is required. The compiler does not force you to check the error return, making it easier to accidentally ignore failures.Error propagation — ? vs if err != nil
use std::fs;
use std::num::ParseIntError;
fn read_count(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let text = fs::read_to_string(path)?; // ? propagates the error
let count = text.trim().parse::<i32>()?;
Ok(count * 2)
}
fn main() {
match read_count("count.txt") {
Ok(value) => println!("value: {}", value),
Err(err) => println!("error: {}", err),
}
} package main
import (
"fmt"
"os"
"strconv"
"strings"
)
func readCount(path string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, fmt.Errorf("readCount: %w", err)
}
count, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return 0, fmt.Errorf("readCount: %w", err)
}
return count * 2, nil
}
func main() {
value, err := readCount("count.txt")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("value:", value)
} Rust's
? operator propagates errors with one character; Go requires an explicit if err != nil { return 0, err } block at every fallible call site. The verbosity is intentional in Go: explicit error paths are visible in the source, and Go wraps errors with fmt.Errorf("context: %w", err) to add context at each level. %w wraps the error, allowing callers to unwrap it with errors.Is or errors.As.Custom error types
use std::fmt;
#[derive(Debug)]
struct AppError { code: u32, message: String }
impl fmt::Display for AppError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "error {}: {}", self.code, self.message)
}
}
impl std::error::Error for AppError {}
fn main() {
let err = AppError { code: 404, message: "not found".to_string() };
println!("{}", err);
} package main
import "fmt"
// Implement the error interface: just one method, Error() string.
type AppError struct {
Code int
Message string
}
func (err *AppError) Error() string {
return fmt.Sprintf("error %d: %s", err.Code, err.Message)
}
func riskyOperation() error {
return &AppError{Code: 404, Message: "not found"}
}
func main() {
err := riskyOperation()
if err != nil {
fmt.Println(err)
// Type-assert to access fields:
if appErr, ok := err.(*AppError); ok {
fmt.Println("code:", appErr.Code)
}
}
} Go's
error is a built-in interface with one method: Error() string. Implement it on a pointer receiver and the type satisfies error — no derive macros, no trait registration. Rust requires implementing both Display and Error (and typically Debug). To access fields of a Go error, type-assert it: err.(*AppError). The standard library's errors.As(err, &target) performs an unwrapping type assertion that follows the error chain.Error inspection — errors.Is / errors.As
use std::io;
fn might_fail() -> Result<(), io::Error> {
Err(io::Error::new(io::ErrorKind::NotFound, "file missing"))
}
fn main() {
let result = might_fail();
if let Err(err) = &result {
if err.kind() == io::ErrorKind::NotFound {
println!("not found: {}", err);
}
}
} package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type DetailedError struct {
Path string
Err error
}
func (detailed *DetailedError) Error() string {
return fmt.Sprintf("%s: %v", detailed.Path, detailed.Err)
}
func (detailed *DetailedError) Unwrap() error { return detailed.Err }
func riskyOperation() error {
return &DetailedError{Path: "/tmp/file", Err: ErrNotFound}
}
func main() {
err := riskyOperation()
if errors.Is(err, ErrNotFound) { // unwraps the chain
fmt.Println("was a not-found error")
}
var detailed *DetailedError
if errors.As(err, &detailed) { // finds DetailedError in the chain
fmt.Println("path:", detailed.Path)
}
} errors.Is(err, target) traverses the error chain (via Unwrap()) to check if any error in the chain equals the target — equivalent to checking the root cause. errors.As(err, &target) traverses the chain to find an error that can be assigned to the target type. These were added in Go 1.13 and are the correct way to inspect wrapped errors. Implement Unwrap() error on custom error types to make them chainable. Rust uses .source() on the Error trait for the same purpose.Goroutines & Channels
Goroutines — lightweight concurrency
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("hello from thread");
});
handle.join().unwrap();
println!("main done");
} package main
import (
"fmt"
"sync"
)
func main() {
var group sync.WaitGroup
group.Add(1)
go func() {
defer group.Done()
fmt.Println("hello from goroutine")
}()
group.Wait()
fmt.Println("main done")
} A goroutine is started with the
go keyword before a function call — no handle is returned. Goroutines are multiplexed onto OS threads by the Go runtime (typically GOMAXPROCS threads by default). They start with a ~2 KB stack that grows dynamically. Unlike Rust's thread::spawn, which returns a JoinHandle, goroutines are fire-and-forget; use sync.WaitGroup or a channel to synchronize. The Go runtime scheduler preempts goroutines cooperatively at function calls.Channels — communicating between goroutines
use std::sync::mpsc;
use std::thread;
fn main() {
let (sender, receiver) = mpsc::channel::<i32>();
thread::spawn(move || {
for i in 0..5 {
sender.send(i).unwrap();
}
});
for received in receiver {
println!("received: {}", received);
}
} package main
import "fmt"
func main() {
channel := make(chan int)
go func() {
for i := 0; i < 5; i++ {
channel <- i // send to channel
}
close(channel) // close signals no more values
}()
for value := range channel { // receive until closed
fmt.Println("received:", value)
}
} Go channels are typed conduits for values between goroutines.
make(chan T) creates an unbuffered channel — a send blocks until a receiver is ready. make(chan T, n) creates a buffered channel. Rust's std::sync::mpsc is the equivalent but requires separate sender/receiver variables. A for value := range channel loop receives until the channel is closed. The Go philosophy is "communicate by sharing memory, not share memory by communicating" — prefer channels over mutexes for data transfer.select — multiplexing channels
// Rust's std has no equivalent — use tokio::select! or crossbeam::select!
// (requires external crates).
// The pattern: wait on multiple async tasks or channels at once,
// acting on whichever completes first.
fn main() {
println!("(select requires an async runtime or crossbeam in Rust)");
} package main
import (
"fmt"
"time"
)
func main() {
channelA := make(chan string, 1)
channelB := make(chan string, 1)
go func() { time.Sleep(10 * time.Millisecond); channelA <- "from A" }()
go func() { channelB <- "from B" }()
for i := 0; i < 2; i++ {
select {
case message := <-channelA:
fmt.Println(message)
case message := <-channelB:
fmt.Println(message)
}
}
} Go's
select statement waits on multiple channel operations and executes whichever case is ready first; if multiple are ready simultaneously, one is chosen at random. It is the Go counterpart of Rust's tokio::select! macro, but built into the language without requiring an async runtime. A select with a default case is non-blocking — it runs immediately if no channel is ready.Mutex — shared mutable state
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut guard = counter.lock().unwrap();
*guard += 1;
}));
}
for handle in handles { handle.join().unwrap(); }
println!("{}", *counter.lock().unwrap()); // 5
} package main
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
counter := 0
var group sync.WaitGroup
for i := 0; i < 5; i++ {
group.Add(1)
go func() {
defer group.Done()
mutex.Lock()
counter++
mutex.Unlock()
}()
}
group.Wait()
fmt.Println(counter) // 5
} Go's
sync.Mutex protects shared data just like Rust's Mutex<T>. The critical difference is that Go's mutex is independent of the data it protects — there is nothing preventing another goroutine from accessing counter without holding the lock. Rust's Mutex<T> wraps the data inside the mutex and grants access only through a guard, making lock-bypass a compile-time error. The Go race detector (go test -race) catches data races at runtime.Generics
Generic functions (type parameters)
fn largest<T: PartialOrd>(slice: &[T]) -> &T {
let mut result = &slice[0];
for item in slice {
if item > result { result = item; }
}
result
}
fn main() {
let numbers = vec![3, 1, 4, 1, 5, 9];
println!("{}", largest(&numbers));
let words = vec!["apple", "zebra", "mango"];
println!("{}", largest(&words));
} package main
import (
"cmp"
"fmt"
)
func largest[T cmp.Ordered](slice []T) T {
result := slice[0]
for _, item := range slice[1:] {
if item > result {
result = item
}
}
return result
}
func main() {
numbers := []int{3, 1, 4, 1, 5, 9}
fmt.Println(largest(numbers))
words := []string{"apple", "zebra", "mango"}
fmt.Println(largest(words))
} Go added generics in Go 1.18. The syntax is
func Name[T Constraint](param T) T where the type parameter list uses square brackets before the argument list. cmp.Ordered is a type constraint (from Go 1.21) equivalent to Rust's PartialOrd — it covers all ordered numeric types and strings. Go's constraints are interfaces; Rust uses trait bounds. Go infers the type argument at call sites, just like Rust monomorphization.Generic structs
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self { Stack { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
}
fn main() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
println!("{:?}", stack.pop()); // Some(2)
} package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (stack *Stack[T]) Push(item T) {
stack.items = append(stack.items, item)
}
func (stack *Stack[T]) Pop() (T, bool) {
if len(stack.items) == 0 {
var zero T
return zero, false
}
last := stack.items[len(stack.items)-1]
stack.items = stack.items[:len(stack.items)-1]
return last, true
}
func main() {
stack := &Stack[int]{}
stack.Push(1)
stack.Push(2)
value, ok := stack.Pop()
fmt.Println(value, ok) // 2 true
} Go generic struct syntax is
type Name[T Constraint] struct { ... }. Methods on a generic type reference the type parameter: func (s *Stack[T]) Push(item T). Because Go has no Option<T>, Pop returns (T, bool) — the zero value for T plus a boolean. Getting the zero value for a generic type requires var zero T; there is no Default::default() equivalent unless the constraint requires it.Type constraints (interface as constraint)
use std::ops::Add;
use std::fmt::Display;
fn sum_and_print<T: Add<Output = T> + Display + Copy>(values: &[T]) -> T {
let total = values.iter().copied().fold(values[0], |accumulator, x| accumulator + x);
println!("sum: {}", total);
total
}
fn main() {
sum_and_print(&[1, 2, 3, 4, 5]);
sum_and_print(&[1.5, 2.5, 3.0]);
} package main
import (
"fmt"
)
// Constraint: any numeric type that supports +
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func sumAndPrint[T Number](values []T) T {
var total T
for _, value := range values {
total += value
}
fmt.Println("sum:", total)
return total
}
func main() {
sumAndPrint([]int{1, 2, 3, 4, 5})
sumAndPrint([]float64{1.5, 2.5, 3.0})
} Go constraints are interface types that can list specific types using the union syntax (
~int | ~int64 | float64). The ~T syntax means "any type whose underlying type is T" — it allows custom types like type Celsius float64 to satisfy a ~float64 constraint. Rust uses trait bounds (T: Add + Display); Go uses interface constraints. The golang.org/x/exp/constraints package (moving into cmp/slices/maps in Go 1.21+) provides common pre-built constraints.defer & Cleanup
defer — cleanup at function exit
use std::fs::File;
use std::io::Write;
fn main() {
// Rust: cleanup happens automatically when values go out of scope (Drop trait).
{
let mut file = File::create("/tmp/rust_demo.txt").expect("create failed");
file.write_all(b"hello").expect("write failed");
println!("wrote file");
} // file is dropped here — OS handle closed automatically
println!("file closed by Drop");
} package main
import (
"fmt"
"os"
)
func writeFile() error {
file, err := os.Create("/tmp/go_demo.txt")
if err != nil {
return err
}
defer file.Close() // runs when writeFile returns — even on error paths
_, err = file.WriteString("hello")
return err
}
func main() {
if err := writeFile(); err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("wrote file")
os.Remove("/tmp/go_demo.txt")
} Rust's
Drop trait provides automatic cleanup — when an owning variable goes out of scope, the destructor runs. Go has no destructor mechanism; instead, defer expr schedules a call to run when the enclosing function returns, regardless of which return path is taken (including after a panic). Placing defer file.Close() immediately after opening a file is idiomatic Go — it keeps the close physically close to the open, and the cleanup cannot be forgotten even if code is added later.Multiple defers execute in LIFO order
fn main() {
// Rust: cleanup order follows LIFO (last in, first out) via drop order.
struct Resource(&'static str);
impl Drop for Resource {
fn drop(&mut self) { println!("dropping: {}", self.0); }
}
let first = Resource("first");
let second = Resource("second");
let third = Resource("third");
let _ = (first, second, third); // dropped in reverse: third, second, first
} package main
import "fmt"
func main() {
defer fmt.Println("deferred: first")
defer fmt.Println("deferred: second")
defer fmt.Println("deferred: third")
fmt.Println("running main body")
// Output order:
// running main body
// deferred: third (last defer, runs first)
// deferred: second
// deferred: first (first defer, runs last)
} Deferred calls execute in last-in, first-out (LIFO) order — the last
defer registered runs first. This matches Rust's drop order for local variables (last declared, first dropped). The LIFO order is important for paired resource management: mutex.Lock(); defer mutex.Unlock() ensures the lock is always released, and multiple locks are released in the correct reverse order to avoid deadlocks.defer modifying named return values
// Rust has no equivalent — named return modification in defer is Go-specific.
// Rust uses closures or helper structs to post-process return values.
fn with_logging<T, F: FnOnce() -> T>(operation: F) -> T {
let result = operation();
println!("operation complete");
result
}
fn main() {
let value = with_logging(|| 42);
println!("got: {}", value);
} package main
import "fmt"
// Named return 'err' can be modified by a deferred function.
func divideWithRecovery(dividend, divisor int) (result int, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("recovered: %v", recovered)
result = 0
}
}()
if divisor == 0 {
panic("division by zero")
}
return dividend / divisor, nil
}
func main() {
result, err := divideWithRecovery(10, 2)
fmt.Println(result, err) // 5 <nil>
result, err = divideWithRecovery(10, 0)
fmt.Println(result, err) // 0 recovered: division by zero
} Go's named return values can be modified by a deferred function — the deferred closure captures them by reference. This is commonly used to add context to errors or recover from panics while still returning a meaningful error value. There is no Rust equivalent; Rust achieves similar patterns with wrapper functions or RAII guards. Named return values are idiomatic only in short functions where they improve readability; in longer functions they can be confusing.
Standard Library Highlights
Sorting slices
fn main() {
let mut numbers = vec![3, 1, 4, 1, 5, 9, 2, 6];
numbers.sort();
println!("{:?}", numbers);
let mut words = vec!["banana", "apple", "cherry"];
words.sort_by(|first, second| first.len().cmp(&second.len()));
println!("{:?}", words);
} package main
import (
"fmt"
"slices"
)
func main() {
numbers := []int{3, 1, 4, 1, 5, 9, 2, 6}
slices.Sort(numbers)
fmt.Println(numbers)
words := []string{"banana", "apple", "cherry"}
slices.SortFunc(words, func(first, second string) int {
return len(first) - len(second)
})
fmt.Println(words)
} Go 1.21 introduced the
slices package with generic slices.Sort and slices.SortFunc. Before 1.21, the sort package was used with interface-based sorting. Rust's slice.sort() sorts in-place with a stable algorithm; Go's slices.Sort uses an unstable sort (faster). For stable sorting, use slices.SortStableFunc. The comparison function in SortFunc returns an int (negative, zero, positive) rather than Rust's std::cmp::Ordering enum.String parsing — strconv vs str::parse
fn main() {
let count: i32 = "42".parse().expect("not an int");
let ratio: f64 = "3.14".parse().expect("not a float");
let enabled: bool = "true".parse().expect("not a bool");
println!("{} {} {}", count, ratio, enabled);
// Convert back to string:
println!("{}", count.to_string());
println!("{}", format!("{:.2}", ratio));
} package main
import (
"fmt"
"strconv"
)
func main() {
count, err := strconv.Atoi("42")
if err != nil {
panic(err)
}
ratio, err := strconv.ParseFloat("3.14", 64)
if err != nil {
panic(err)
}
enabled, err := strconv.ParseBool("true")
if err != nil {
panic(err)
}
fmt.Println(count, ratio, enabled)
// Convert back:
fmt.Println(strconv.Itoa(count))
fmt.Printf("%.2f\n", ratio)
} Go's
strconv package handles all numeric string conversions. strconv.Atoi is the common integer parser (short for ASCII to int); the more general strconv.ParseInt accepts a base and bit size. Rust's str::parse::<T>() is a single method with type inference. Go always returns an error from strconv functions rather than panicking on bad input — always check it. strconv.Itoa converts an integer to string; fmt.Sprintf or fmt.Fprintf handle more complex formatting.io.Reader / io.Writer interfaces
use std::io::{self, Write, BufWriter};
fn write_greeting(writer: &mut dyn Write) -> io::Result<()> {
write!(writer, "Hello, World!\n")
}
fn main() {
let stdout = io::stdout();
let mut buffered = BufWriter::new(stdout.lock());
write_greeting(&mut buffered).expect("write failed");
} package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func writeGreeting(writer *bufio.Writer) error {
_, err := fmt.Fprintln(writer, "Hello, World!")
return err
}
func main() {
buffered := bufio.NewWriter(os.Stdout)
if err := writeGreeting(buffered); err != nil {
fmt.Println("error:", err)
return
}
buffered.Flush()
// io.Reader: anything with Read([]byte) (int, error)
reader := strings.NewReader("stream of bytes")
buffer := make([]byte, 5)
n, _ := reader.Read(buffer)
fmt.Printf("read %d bytes: %s\n", n, buffer[:n])
} Go's
io.Reader and io.Writer are the foundational interfaces for streaming I/O — any type with Read([]byte) (int, error) or Write([]byte) (int, error) satisfies them. Rust uses the Read and Write traits from std::io. The implicit interface satisfaction in Go makes it easy to compose: bufio.NewWriter wraps any io.Writer, strings.NewReader wraps a string as an io.Reader — no adapter types or explicit impl declarations needed.Time — time.Duration vs std::time
use std::time::{Duration, Instant};
fn main() {
let start = Instant::now();
let delay = Duration::from_millis(10);
let _ = delay; // (skip actual sleep in test)
let elapsed = start.elapsed();
println!("elapsed: {}ms", elapsed.as_millis());
let timeout = Duration::from_secs(30);
println!("timeout: {}s", timeout.as_secs());
} package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
timeout := 30 * time.Second // time.Duration arithmetic
delay := 10 * time.Millisecond
fmt.Println("timeout:", timeout)
fmt.Println("delay:", delay)
elapsed := time.Since(start)
fmt.Printf("elapsed: %v\n", elapsed)
// Parse and format times:
timestamp := time.Date(2026, time.June, 1, 12, 0, 0, 0, time.UTC)
fmt.Println(timestamp.Format("2006-01-02")) // Go's reference time: Mon Jan 2 15:04:05 2006
} Go's
time.Duration is an int64 of nanoseconds. Duration literals are created by multiplying a number by a named constant: 30 * time.Second. Rust's std::time::Duration has separate from_secs/from_millis constructors. Go's most distinctive design choice: time formatting uses the specific reference time Mon Jan 2 15:04:05 MST 2006 (the first second of Go's design) as a template instead of POSIX %Y-%m-%d strftime codes — "2006-01-02" means year-month-day.