Go to Rust Series: ← Day 1 Impressions | Series Overview | Hello World Comparison →


First Impression: Cargo is Actually Great

After struggling with Rust’s ownership system on Day 1, I expected the tooling to be equally complex. I was wrong. Cargo is fantastic.

Coming from Go modules (which are already pretty good), Cargo feels like a spiritual successor with better UX.

Project Initialization

Go:

mkdir myproject
cd myproject
go mod init github.com/username/myproject

Creates go.mod:

module github.com/username/myproject

go 1.22

Rust:

cargo new myproject
cd myproject

Creates Cargo.toml AND a complete project structure:

myproject/
├── Cargo.toml
├── src/
│   └── main.rs
└── .gitignore

Winner: Cargo - It sets up everything, including version control ignore files.

Configuration Files

Go: go.mod

go.mod:

module github.com/username/myproject

go 1.22

require (
    github.com/gorilla/mux v1.8.1
    github.com/lib/pq v1.10.9
)

replace github.com/old/pkg => github.com/new/pkg v1.0.0

Simple, minimalist. Dependencies are listed with exact versions. The go.sum file stores checksums.

Rust: Cargo.toml

Cargo.toml:

[package]
name = "myproject"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.11"

[dev-dependencies]
mockito = "1.0"

[profile.release]
opt-level = 3
lto = true

More verbose, but more powerful. You can:

  • Enable specific features (tokio with “full” features)
  • Separate dev dependencies
  • Configure build profiles
  • Specify build scripts and more

Winner: Tie - Go’s is simpler; Rust’s is more flexible.

Adding Dependencies

Go: go get

Adding a dependency:

go get github.com/gorilla/[email protected]

Or edit go.mod directly:

require github.com/gorilla/mux v1.8.1

Then run:

go mod tidy

Rust: cargo add

Adding a dependency:

cargo add serde --features derive

Automatically updates Cargo.toml. Or edit manually:

[dependencies]
serde = { version = "1.0", features = ["derive"] }

Then:

cargo build

Winner: Cargo - cargo add with feature flags is cleaner than Go’s CLI.

Dependency Resolution

Go: Minimal Version Selection

Go uses Minimal Version Selection (MVS):

  • Always picks the oldest version that satisfies all constraints
  • Predictable, reproducible builds
  • No complex SAT solver needed

Example:

Your project requires: [email protected]+
Dependency A requires: [email protected]+
Dependency B requires: [email protected]+

Go picks: [email protected] (minimum version satisfying all)

Rust: SemVer-based Resolution

Cargo uses Semantic Versioning with ranges:

serde = "1.0"        # Means "^1.0" = >=1.0.0, <2.0.0
reqwest = "0.11.*"   # Exactly 0.11.x
tokio = "~1.2"       # >=1.2.0, <1.3.0
actix = "=4.0.0"     # Exact version

Cargo.lock ensures reproducible builds (like go.sum).

Winner: Personal preference - Go’s MVS is simpler; Cargo’s ranges are more flexible.

Building Projects

Go: go build

Build:

go build                          # Build current package
go build -o myapp                 # Specify output name
go build -ldflags="-s -w"        # Strip debug info

Fast, straightforward.

Build for different platforms:

GOOS=linux GOARCH=amd64 go build
GOOS=windows GOARCH=amd64 go build

Cross-compilation works out of the box for most targets.

Rust: cargo build

Build:

cargo build                       # Debug build
cargo build --release             # Optimized build
cargo build --target x86_64-unknown-linux-musl  # Specific target

Debug builds are MUCH slower than Go (Rust does more checks). Release builds are fast and highly optimized.

Cross-compilation requires installing targets:

rustup target add x86_64-pc-windows-gnu
cargo build --target x86_64-pc-windows-gnu

Winner: Go for speed, Rust for optimization levels.

Running Code

Go:

go run main.go                    # Run directly
go run .                          # Run package

Rust:

cargo run                         # Build and run
cargo run --release               # Run optimized build
cargo run --example my_example    # Run an example

Winner: Tie - Both are simple.

Testing

Go: go test

Write tests:

// math_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
    result := add(2, 3)
    if result != 5 {
        t.Errorf("Expected 5, got %d", result)
    }
}

Run tests:

go test                           # Run all tests
go test -v                        # Verbose
go test -cover                    # With coverage
go test ./...                     # All packages recursively

Rust: cargo test

Write tests:

// lib.rs or main.rs
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

Run tests:

cargo test                        # Run all tests
cargo test --lib                  # Only library tests
cargo test test_add               # Run specific test
cargo test -- --nocapture         # Show println! output

Winner: Cargo - Integrated test framework with modules, benchmarks, and doc tests.

Workspaces (Monorepos)

Go: Workspaces

go.work:

go 1.22

use (
    ./service-a
    ./service-b
    ./shared
)

Then go build from root works across all modules.

Rust: Cargo Workspaces

Cargo.toml (root):

[workspace]
members = [
    "service-a",
    "service-b",
    "shared",
]

Sharing dependencies:

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

Then in service-a/Cargo.toml:

[dependencies]
serde = { workspace = true }
shared = { path = "../shared" }

Winner: Cargo - Better dependency sharing across workspace members.

Features (Conditional Compilation)

This is where Cargo shines over Go.

Go: Build Tags

Conditional compilation:

//go:build linux
// +build linux

package main

func platformSpecific() {
    // Linux-specific code
}

Limited to platforms and custom tags.

Rust: Features

Cargo.toml:

[features]
default = ["json"]
json = ["serde_json"]
xml = ["quick-xml"]
full = ["json", "xml", "compression"]
compression = ["flate2"]

[dependencies]
serde_json = { version = "1.0", optional = true }
quick-xml = { version = "0.30", optional = true }
flate2 = { version = "1.0", optional = true }

Code:

#[cfg(feature = "json")]
use serde_json;

#[cfg(feature = "xml")]
use quick_xml;

pub fn serialize_data(data: &str) {
    #[cfg(feature = "json")]
    {
        let json = serde_json::json!({ "data": data });
        println!("JSON: {}", json);
    }

    #[cfg(feature = "xml")]
    {
        println!("XML: <data>{}</data>", data);
    }
}

Build with features:

cargo build --features json
cargo build --features "json,xml"
cargo build --all-features
cargo build --no-default-features

Winner: Cargo by a mile - Features enable fine-grained control over compilation.

Documentation

Go: go doc

Generate docs from comments:

// Add returns the sum of a and b.
//
// Example:
//     result := Add(2, 3)  // returns 5
func Add(a, b int) int {
    return a + b
}

View docs:

go doc Add
godoc -http=:6060  # Start local doc server

Rust: cargo doc

Document with doc comments:

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Generate docs:

cargo doc --open  # Build and open in browser

The example code in doc comments is executed as tests!

cargo test --doc  # Run documentation tests

Winner: Cargo - Executable documentation examples are brilliant.

Linting and Formatting

Go

Formatting:

go fmt ./...      # Format all code (opinionated, one style)

Linting:

go vet ./...      # Basic linting
golangci-lint run # Advanced linting (separate tool)

Rust

Formatting:

cargo fmt         # Format code (opinionated, based on rustfmt)

Linting:

cargo clippy      # Amazing linter with helpful suggestions
cargo clippy -- -D warnings  # Treat warnings as errors

Clippy is fantastic. It catches potential bugs and suggests idiomatic patterns.

Example:

let mut v = Vec::new();
v.push(1);
v.push(2);

Clippy suggests:

help: consider using the `vec!` macro: `let v = vec![1, 2];`

Winner: Cargo - Clippy is on another level.

Real-World Workflow Comparison

Go Workflow

# Start project
go mod init myproject

# Add dependency
go get github.com/gorilla/mux

# Write code in main.go

# Format
go fmt .

# Lint
go vet .

# Test
go test -v

# Build
go build -o myapp

# Run
./myapp

Fast, simple, minimal ceremony.

Rust Workflow

# Start project
cargo new myproject
cd myproject

# Add dependency
cargo add tokio --features full

# Write code in src/main.rs

# Format
cargo fmt

# Lint
cargo clippy

# Test (including doc tests)
cargo test

# Build (debug)
cargo build

# Build (release)
cargo build --release

# Run
cargo run

More features, slightly slower, richer output.

The Verdict

Feature Go Rust (Cargo) Winner
Initialization Manual Automatic with structure Cargo
Dependency management Simple Feature-rich Cargo
Build speed (debug) Very fast Slow Go
Build speed (release) Fast Fast Tie
Cross-compilation Built-in Requires setup Go
Testing Built-in, simple Built-in, advanced Cargo
Workspaces Good Better Cargo
Features/conditional Limited Excellent Cargo
Documentation Good Excellent (with tests) Cargo
Linting Basic (vet) Amazing (clippy) Cargo
Overall simplicity Simpler More powerful Depends

Surprising Discovery: Cargo is Better Than Expected

I came into Rust expecting pain. Cargo was a pleasant surprise:

What Cargo does better:

  • Integrated workflow (build + test + doc + lint)
  • Feature flags for conditional compilation
  • Workspace dependency sharing
  • Documentation tests that actually run
  • Clippy’s incredible suggestions

What Go does better:

  • Faster iteration (compile times)
  • Simpler mental model
  • Better out-of-the-box cross-compilation

Practical Example: Building a CLI Tool

Let’s compare building a simple CLI tool:

Go Version

main.go:

package main

import (
    "flag"
    "fmt"
)

var name = flag.String("name", "World", "name to greet")

func main() {
    flag.Parse()
    fmt.Printf("Hello, %s!\n", *name)
}

Build:

go build -o greet
./greet --name Gopher
# Output: Hello, Gopher!

Build time: ~1 second

Rust Version

Cargo.toml:

[package]
name = "greet"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.0", features = ["derive"] }

src/main.rs:

use clap::Parser;

#[derive(Parser)]
#[command(name = "greet")]
#[command(about = "A simple greeting tool")]
struct Args {
    /// Name to greet
    #[arg(short, long, default_value = "World")]
    name: String,
}

fn main() {
    let args = Args::parse();
    println!("Hello, {}!", args.name);
}

Build:

cargo build --release
./target/release/greet --name Gopher
# Output: Hello, Gopher!

Build time (first): ~30 seconds (Rust compiles dependencies) Build time (incremental): ~2 seconds

Rust’s version has auto-generated help:

./greet --help
A simple greeting tool

Usage: greet [OPTIONS]

Options:
  -n, --name <NAME>  Name to greet [default: World]
  -h, --help         Print help

Winner: Go for speed, Rust for features.

Conclusion

For Go developers learning Rust, Cargo will be one of your favorite parts.

It’s not perfect (slower builds, more configuration), but the integrated workflow is excellent. The ability to use features for conditional compilation alone makes complex projects much cleaner.

Key Takeaways

  1. Cargo is more than a build tool—it’s a complete workflow
  2. Features are powerful—Go has nothing comparable
  3. Clippy is amazing—it teaches you idiomatic Rust
  4. Documentation tests are brilliant—examples stay up to date
  5. Go is still faster to iterate—but Cargo’s release builds are highly optimized

Tomorrow, I’ll tackle why Rust’s Hello World is more complex than Go’s—and why that’s actually educational.


Go to Rust Series: ← Day 1 Impressions | Series Overview | Hello World Comparison →


Verdict: Cargo is the best part of Rust so far. It makes the strict compiler feel worth it.