Go to Rust Series: ← Cargo vs Go Modules | Series Overview | Setting Up Rust →
The “Simple” Hello World
Every programming tutorial starts with “Hello, World!” It’s supposed to be trivial. But comparing Go and Rust versions reveals fundamental philosophical differences.
Go’s Hello World
main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Compile and run:
go run main.go
Output:
Hello, World!
Done. Five lines. Zero surprises.
Rust’s Hello World
main.rs:
fn main() {
println!("Hello, World!");
}
Compile and run:
cargo run
Output:
Compiling hello v0.1.0 (/path/to/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/hello`
Hello, World!
Looks similar! But there’s more happening under the hood.
Surface-Level Differences
1. Package Declaration
Go:
package main // Required for executables
Every Go file needs a package declaration. The main package signals an executable.
Rust:
// No package declaration in the file
Package metadata lives in Cargo.toml, not the source file. Cleaner, but one more place to look.
2. Import vs Use
Go:
import "fmt" // Import the fmt package
Rust:
// No import needed for println! - it's in the prelude
Rust’s prelude automatically imports common items like println!, Option, Result, etc. But you’ll need explicit use statements for most other things:
use std::io; // For I/O operations
3. Functions
Go:
func main() {
// function body
}
Rust:
fn main() {
// function body
}
Nearly identical. The fn keyword is shorter than func.
4. Printing
This is where it gets interesting.
Go:
fmt.Println("Hello, World!") // Function call
Rust:
println!("Hello, World!"); // Macro call
See that !? That’s a macro, not a function.
The Macro Mystery
When I first saw println!(), I thought: “Why the exclamation mark? Is Rust yelling at me?”
Turns out, macros are one of Rust’s most powerful features.
Function vs Macro
Go function:
func Println(a ...interface{}) (n int, err error) {
// Implementation
}
Functions in Go take typed arguments. Variadic functions use ... but still operate at runtime.
Rust macro:
println!("Value: {}", 42);
println!("Multiple: {} and {}", 1, 2);
println!("Named: {name}", name = "Rust");
println! is a compile-time macro that generates code based on its arguments. It:
- Validates format strings at compile time
- Type-checks arguments at compile time
- Generates optimized code for each call
Example: Compile-time validation
This compiles in Go (but panics at runtime):
fmt.Printf("%d", "not a number") // Compiles fine, runtime panic!
This won’t compile in Rust:
println!("{}", some_var); // Error if some_var doesn't impl Display
The compiler catches type mismatches immediately.
Deeper Comparison: Formatted Printing
Go: fmt.Printf
Basic formatting:
name := "Gopher"
age := 5
fmt.Printf("Name: %s, Age: %d\n", name, age)
Different functions for different outputs:
fmt.Println("To stdout") // With newline
fmt.Print("To stdout") // No newline
fmt.Printf("Formatted: %d", 1) // Format string
fmt.Sprintf("Returns string") // Returns string instead of printing
Rust: println! and Friends
Basic formatting:
let name = "Rustacean";
let age = 5;
println!("Name: {}, Age: {}", name, age);
Different macros for different outputs:
println!("To stdout with newline"); // Standard output
print!("To stdout, no newline"); // Standard output, no newline
eprintln!("To stderr with newline"); // Standard error
eprint!("To stderr, no newline"); // Standard error, no newline
format!("Returns String"); // Returns String
Advanced formatting:
println!("{:?}", some_var); // Debug format
println!("{:#?}", some_var); // Pretty debug format
println!("{:x}", 255); // Hex: ff
println!("{:b}", 5); // Binary: 101
println!("{:.2}", 3.14159); // Precision: 3.14
println!("{:>10}", "right"); // Right-align: " right"
What “Hello World” Teaches You
Go’s Lesson: Simplicity
Go’s hello world shows you:
- Minimal syntax
- Explicit imports
- Standard library organization
- Fast compilation
Philosophy: Get running quickly, learn details later.
Rust’s Lesson: Fundamentals
Rust’s hello world introduces:
- Macros (the
!syntax) - Prelude (implicit imports)
- Cargo (project structure)
- Traits (why
println!needs Display or Debug trait)
Philosophy: Even simple programs teach core concepts.
Expanding Hello World
Let’s make both slightly more complex.
Go: Interactive Hello
main.go:
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your name: ")
name, _ := reader.ReadString('\n')
name = strings.TrimSpace(name)
fmt.Printf("Hello, %s!\n", name)
}
Run:
$ go run main.go
Enter your name: Gopher
Hello, Gopher!
Rust: Interactive Hello
main.rs:
use std::io::{self, Write};
fn main() {
print!("Enter your name: ");
io::stdout().flush().unwrap(); // Flush is manual!
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("Failed to read line");
let name = name.trim();
println!("Hello, {}!", name);
}
Run:
$ cargo run
Enter your name: Rustacean
Hello, Rustacean!
Key Differences in Interactive Version
1. Input Reading
Go:
reader := bufio.NewReader(os.Stdin)
name, _ := reader.ReadString('\n')
- Create a buffered reader
- Read until newline
- Ignore error with
_(not recommended for production!)
Rust:
let mut name = String::new();
io::stdin()
.read_line(&mut name)
.expect("Failed to read line");
- Create mutable String
- Read into it (notice
&mut- mutable borrow) - Handle Result with
.expect()(panics on error)
2. Flushing Output
Go:
fmt.Print("Enter your name: ")
// Automatically flushed before reading input
Rust:
print!("Enter your name: ");
io::stdout().flush().unwrap(); // Must manually flush!
Without flush(), the prompt might not appear before input.
3. Error Handling
Go - Implicit (dangerous):
name, _ := reader.ReadString('\n') // Ignore error
Go - Explicit (better):
name, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error:", err)
return
}
Rust - Must handle:
io::stdin()
.read_line(&mut name)
.expect("Failed to read line"); // Panics on error
Or explicitly:
match io::stdin().read_line(&mut name) {
Ok(_) => {},
Err(e) => {
eprintln!("Error: {}", e);
return;
}
}
Rust forces you to acknowledge the Result. You can’t ignore it.
Adding a Web Server: Not-So-Hello World
Let’s go one step further: a web server that says hello.
Go: HTTP Server
main.go:
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s!", name)
}
func main() {
http.HandleFunc("/", helloHandler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
Run:
go run main.go
Test:
curl http://localhost:8080?name=Gopher
# Output: Hello, Gopher!
Zero external dependencies. HTTP server is in the standard library.
Rust: HTTP Server (with dependencies)
Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
warp = "0.3"
main.rs:
use warp::Filter;
#[tokio::main]
async fn main() {
let hello = warp::path::end()
.and(warp::query::<std::collections::HashMap<String, String>>())
.map(|params: std::collections::HashMap<String, String>| {
let name = params.get("name")
.map(|s| s.as_str())
.unwrap_or("World");
format!("Hello, {}!", name)
});
println!("Server starting on :8080");
warp::serve(hello).run(([127, 0, 0, 1], 8080)).await;
}
Run:
cargo run
Test:
curl http://localhost:8080?name=Rustacean
# Output: Hello, Rustacean!
Observations:
- Rust needs external crates (tokio, warp)
- Requires async runtime
- More complex type annotations
- But: much faster performance and better type safety
Build Time Comparison
Go: Hello World
$ time go build -o hello main.go
real 0m0.234s
Subsecond builds even on first compile.
Rust: Hello World (no dependencies)
$ time cargo build --release
real 0m0.612s
Still fast for no dependencies.
Rust: Web Server (with dependencies)
$ time cargo build --release
real 0m45.321s
First build: 45 seconds! (compiles tokio, warp, and all dependencies)
Incremental build:
$ touch src/main.rs
$ time cargo build --release
real 0m1.823s
Much faster, but still slower than Go.
Why Rust’s “Hello World” is More Educational
Go’s Hello World:
- Gets you running immediately
- Teaches package structure
- Shows standard library usage
Rust’s Hello World:
- Introduces macros early
- Forces thinking about types
- Teaches error handling from the start
- Shows ownership (with
&mutin interactive version)
By the time you’ve written interactive Hello World in Rust, you’ve learned:
- Macros (
println!) - Mutability (
let mut) - Borrowing (
&mut) - Error handling (
Result,.expect(),.unwrap()) - Traits (implicit:
Display,Debug)
In Go, you can write hundreds of lines before encountering these concepts.
The Verdict
| Aspect | Go | Rust |
|---|---|---|
| Lines of code | 5 | 3 (simple), 10+ (realistic) |
| Time to first run | 30 seconds | 2 minutes (with cargo new) |
| Concepts introduced | Few | Many |
| Build time | <1 second | <1 second (no deps), 30s+ (with deps) |
| Dependencies needed | None (stdlib rocks) | Often external crates |
| Learning curve | Gentle | Steep |
| Educational value | Low (intentionally) | High (unavoidably) |
Conclusion
Go’s “Hello World” is simpler by design. You run it, it works, you move on.
Rust’s “Hello World” is deceptively complex. It introduces macros, type systems, and error handling from line one.
This reflects each language’s philosophy:
- Go: “Make simple things simple, complex things possible”
- Rust: “Make correct things natural, incorrect things hard”
For a Go developer, Rust’s verbosity feels like overhead. But that “overhead” prevents bugs before they happen.
Tomorrow: Setting Up Rust tooling compared to Go’s simplicity.
Go to Rust Series: ← Cargo vs Go Modules | Series Overview | Setting Up Rust →
Verdict: Go gets you started faster. Rust teaches you more from the start. Neither is wrong.