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 &mut in interactive version)

By the time you’ve written interactive Hello World in Rust, you’ve learned:

  1. Macros (println!)
  2. Mutability (let mut)
  3. Borrowing (&mut)
  4. Error handling (Result, .expect(), .unwrap())
  5. 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.