Why I Vibe in Go, Not Rust or Python

Source: lifelog.my
36 points by riclib 21 hours ago on hackernews | 26 comments

Last night I built a website from scratch. Not a landing page. A full blog with three-domain routing, animated video covers, an audio player with playlists, dark mode, RSS feeds, social cards, and a sticky sidebar with a lightbox. Seven commits. Zero test failures. One binary.

The site you’re reading this on. Built in one session. In Go.

I work with an AI that writes most of the code. The question everyone asks is which language to vibe in. Python is fast to start. Rust is correct by construction. Go is boring.

I choose boring. Here’s why.

The Case Against Python

Python has no compiler. It has type hints, which are optional, which means they’re not there.

When an AI writes four thousand lines of Python in a day, every line runs. Every line produces output. And somewhere in those four thousand lines, a dictionary key is misspelled, a None propagates through three function calls before it surfaces, a variable changes type between assignment and use because nothing prevents it.

The bug arrives in production. Not because the AI is bad. Because nothing between the AI and production said no.

When the machine writes 90% of the code, “it runs” is not a quality bar. It’s the absence of one.

Mypy exists. Mypy is optional. Optional means it’s not there. I’ve never seen a Python project where mypy covers 100% of the code with strict mode. I’ve seen hundreds where it covers the 20% someone added last quarter. The other 80% is Any, all the way down.

Python is fast to prototype. It’s also fast to production-incident. These are the same property.

The Case Against Rust

The case against Rust is more interesting because Rust is correct.

The borrow checker catches real bugs. The type system is rigorous. The compiler says no, and when it says no, it’s right. The problem is that the compiler says no too much.

When an AI writes Rust, the borrow checker fights the generated code. I — the human — spend my taste budget on lifetime annotations instead of architecture. Instead of saying “why do we keep both around?” — five words that collapse complexity — I’m saying “add a .clone() here” or “wrap this in Arc<Mutex<>>.” I’m using Layer 5, the most expensive layer, on problems the language invented.

The Async Tax

Go has goroutines. You write go func() and it works.

Rust has colored functions. You pick tokio as your async runtime. Then every crate you choose must be tokio-compatible. One crate uses async-std. Now you have two runtimes. The AI doesn’t know which one to use. You spend Layer 5 explaining concurrency runtimes instead of designing the system.

Last night my binary ran HTTP handlers, NATS consumers, filesystem watchers, git push timers, and SSE streams. All goroutines. All started with go func(). No runtime selection. No colored functions. No Pin<Box<dyn Future>>. No Arc<Mutex<>> wrappers around state that is just a struct field.

In Rust, each of those would require tokio compatibility verification. The NATS client — is it tokio-native? The filesystem watcher — does it use async-std internally? The timer — does it spawn a thread or use the tokio runtime? These are real questions that consume real human attention. In Go, they don’t exist.

The Ecosystem Roulette

Come back to a Rust project after three months. Run cargo update. Half your transitive dependencies don’t compile because a trait bound changed in a crate you’ve never heard of.

The language is correct. The ecosystem is fragile.

Go’s compatibility promise is real. go build today, go build in two years. Same binary. Same behavior. The standard library doesn’t break. The dependencies you chose in 2024 still compile in 2026 because Go takes backwards compatibility seriously at every level — language, standard library, and ecosystem culture.

Rust optimizes for correctness at the cost of velocity. When vibing, velocity is the point.

The Case For Go

Last night I added a field to a struct. SiteLinks needed a Riclib field and a FeedRiclib field. I added them. Ran go build. The compiler showed me every call site that needed updating. Not some of them. Every single one. I fixed them. Built again. Zero errors.

In Python, the struct is a dictionary. Adding a field changes nothing. The missing key surfaces at runtime, on the code path the tests didn’t cover, in production, at 2 AM.

In Rust, the struct has the field. The compiler catches the call sites. But it also catches the lifetime of the string reference in the field, the borrow of the struct across a thread boundary, and the async Send bound that the new field violates. Four problems. One was mine. Three were the language’s.

In Go, the compiler caught my problem and only my problem. Then it got out of the way.

The Five Layers

Five layers between the machine and production:

Layer 1: The Compiler. Catches the obvious. Types don’t match. Imports unused. Variables shadowed. The machine’s first draft fails here. This is cheap. This is instant. This is the floor.

Layer 2: The Type System. Catches the structural. A function expects WikiLinkResolver, not func(string) string. A handler returns ([]byte, error), not just []byte. The machine’s second draft fails here, sometimes.

Layer 3: Explicit Errors. Catches the ignored. Every error must be handled. Not caught, not swallowed — handled. if err != nil. Four hundred times per file. The machine cannot write except: pass in Go because Go does not have except. The machine must handle the error. The human can see the handling.

Layer 4: Enforced Simplicity. Catches nothing. Prevents everything. One way to loop. One way to format. One way to organize. The machine generates uniform code because Go does not permit non-uniform code. The human can read it at a glance. The human can steer.

Layer 5: The Human. Catches the subtle. “Make it dark mode aware.” “The cover is cut at the top.” “The stickiness didn’t work.” Five-word corrections that no compiler can generate because five words require taste, and taste requires understanding what you’re building and why.

The human is the most expensive layer. Go makes sure the human only handles what four cheaper layers already missed. Python sends everything to the human. Rust sends the human’s problems plus its own.

The Binary Argument

When the session was done, the result was one file. I ran scp to copy it to the server. The server ran it. Three domains — lifelog.my, yagnipedia.com, riclib.com — served from one binary. No runtime. No dependency installation. No container.

The deploy script is 30 lines of bash:

GOOS=linux GOARCH=amd64 go build -o lg-linux .
scp lg-linux server:/home/lifelog/bin/lg
ssh server "systemctl restart lifelog"

That’s the deployment.

Python needs a virtualenv, or a Docker container, or both. The Dockerfile installs system dependencies, copies requirements.txt, runs pip install, copies the code, sets the entrypoint. The Go equivalent is COPY binary /usr/local/bin/. One line. One layer. One file.

Rust produces a binary too. But cargo build downloads 400 crates first. The build takes four minutes. The CI pipeline has a crate cache that breaks every time a dependency updates. The binary is correct. Getting to the binary is the tax.

The site you are reading this essay on was built last night. In one session.

The AI wrote the templates, the HTTP handlers, the CSS, the audio player, the RSS feed, the social card meta tags, the lightbox, the sticky sidebar. I wrote five-word corrections. The compiler caught the structural errors. The type system caught the interface mismatches. The explicit error handling ensured nothing failed silently. The enforced simplicity meant I could read the AI’s code at a glance, because Go code only looks one way.

The feedback loop: templ generate && go build. If it compiles, it works. If it doesn’t compile, the error message is four lines, not four paragraphs. The AI reads the error, fixes the code, builds again. Seconds, not minutes.

Seven commits. Zero test failures. The binary shipped at 2 AM. It’s still running.

The Filter

I don’t vibe in Go because Go is the best language. I vibe in Go because Go is the best filter.

When the machine writes 90% of the code, the language is not a tool for writing. It’s a tool for catching. Python catches nothing. Rust catches everything, including things that aren’t problems. Go catches the things that matter and gets out of the way.

The compiler is the floor. The human is the taste. The binary is the proof.

See Also

  • Vibe In Go — The original Yagnipedia entry: five layers, 559,872 paths, the compiler is the bouncer
  • Vibe Engineering — Steering the machine with five-word corrections across half a million design paths
  • Vibe Coding — What happens when the machine writes code in a language with no compiler
  • AI in Go — The ecosystem argument: net/http is the ecosystem
  • Boring Technology — One way to do things is boring. Boring is navigable.