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 (
tokiowith “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
- Cargo is more than a build tool—it’s a complete workflow
- Features are powerful—Go has nothing comparable
- Clippy is amazing—it teaches you idiomatic Rust
- Documentation tests are brilliant—examples stay up to date
- 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.