Goroutines, Channels, and the Borrow Checker: 12 Prompts Go and Rust Devs Actually Keep in Their Clipboard

Goroutines, Channels, and the Borrow Checker: 12 Prompts Go and Rust Devs Actually Keep in Their Clipboard

Concurrency bugs don't show up in code review. They show up at 3 AM, in production, as a goroutine dump nobody wants to read. Rust's borrow checker, meanwhile, has a habit of blocking you exactly when you're most convinced the code is correct. These are the two failure modes that eat the most hours in modern backend work — and both are surprisingly well-suited to AI assistance, because both have deterministic, verifiable answers: a race detector output, a compiler error, a pprof profile.

Below are 12 prompts I've used across Go and Rust services — from gRPC streaming to zero-copy parsing to deadlock forensics. Each one includes the actual prompt, a real usage scenario, and the reasoning behind why it works. No filler: every prompt here assumes you'll paste real code, real errors, or real profiles into it.

A note on tooling: Go 1.22+ changed loop variable semantics (for loop variables are now per-iteration), which invalidates a lot of older AI advice about v := v capture. Rust's async ecosystem (Tokio 1.x, async fn in traits stabilized in Rust 1.75) has also shifted. Always state your toolchain version in the prompt — it's the single highest-leverage line you can add.


1. The Deadlock Autopsy: turning a goroutine dump into a root cause

The problem: a service hangs under load. You have a SIGQUIT dump (or pprof goroutine profile) with 4000+ goroutines, most of them blocked on chan receive.

The prompt:

You are a Go concurrency debugger. Here is a goroutine dump from a hung service (Go 1.22, GOMAXPROCS=8). Identify: (1) the minimal set of goroutines forming a deadlock cycle, (2) which channel/lock each is waiting on, (3) the most likely code shape that produced it. Output a Mermaid diagram of the wait-for graph, then propose the smallest fix. Do not suggest context.WithTimeout unless the cycle is genuinely unavoidable.

Why it works: dumps are large but structurally regular. AI is good at pattern-matching goroutine N [chan receive]: stacks and reconstructing the wait-for graph. The instruction to not default to timeouts is critical — a timeout is a band-aid that hides the cycle.

Real case: a worker pool where a producer sent to an unbuffered channel inside a select that also had a ctx.Done() case — but the consumer had already exited on an unrelated error path. Fix was a defer close(jobs) in the producer plus a sync.WaitGroup on consumers. The prompt found it in one pass by tracing which goroutine was the last to block.


2. Race Detector → Idiomatic Fix

The prompt:

Here is output from go test -race and the offending function. Explain the exact interleaving that triggers the race, then rewrite the function using the most idiomatic Go primitive for this shape (mutex, atomic, channel, or sync.Once). Justify your choice against the alternatives. Preserve the public API.

Context you must include: Go version, whether the shared state is read-mostly or write-heavy, and whether ordering between goroutines matters. These three facts determine whether sync.RWMutex, atomic.Pointer[T] (Go 1.19+), or a channel is correct. Without them, you'll get a generic mutex answer.

Use case: a config hot-reload path. The naive fix was a mutex around the whole struct; the better fix was atomic.Pointer[Config] with copy-on-write, because readers outnumbered writers by orders of magnitude.


3. Context Propagation Linter

The prompt:

Review this package for context misuse. Flag: (a) context.Background() created outside main/init/tests, (b) contexts stored in struct fields, (c) context.WithCancel without a matching cancel() call, (d) contexts passed as anything other than the first parameter. For each, show the line and the corrected version.

This mirrors the checks in go vet's lostcancel analyzer and the contextcheck linter, but AI catches the semantic cases those miss — like a context created in a request handler but never cancelled on the early-return path.


4. gRPC Streaming Design: unary vs. server-stream vs. bidi

The prompt:

I'm designing a gRPC API (protobuf3, grpc-go 1.6x) for [describe workload: e.g., real-time position updates for 50k clients]. Compare unary, server-streaming, client-streaming, and bidirectional streaming for this case. For the recommended option, write the .proto service definition and a Go server handler using grpc.ServerStreamingServer[T] generics (grpc-go 1.64+). Include flow-control considerations (HTTP/2 window, grpc.MaxConcurrentStreams) and backpressure strategy.

Key detail: gRPC streaming is built on HTTP/2, so flow control is per-stream and per-connection. Mentioning the window explicitly forces the model to address backpressure instead of hand-waving it. A common real failure: server-streaming a firehose to a slow client without checking Send() errors leads to unbounded memory growth.


5. Protobuf Schema Evolution Review

The prompt:

Given these two versions of a .proto file, list all wire-compatibility breaks per the protobuf language guide (field number reuse, type changes, required→optional, enum value removal). Rate each as breaking / safe / risky. Then rewrite v2 to be backward-compatible with v1 while adding [new fields].

Why this matters: field numbers are forever. Renaming a field is safe; reusing its number for a different type silently corrupts data across versions. The authoritative reference is the official Protobuf "Language Guide (proto3)" and "Proto Best Practices" docs — worth citing in the prompt so the model anchors on the real rules.


6. pprof → Allocation Hotspot Fix

The prompt:

Here is go tool pprof -top -alloc_space output and the top 3 functions. Identify the biggest allocation source and rewrite it to reduce heap pressure. Consider: preallocating slices with make([]T, 0, n), sync.Pool for frequently reused buffers, strings.Builder over +=, and avoiding interface boxing in hot loops. Show a before/after benchmark using testing.B with -benchmem.

Real case: a JSON log formatter allocating a map[string]interface{} per line. Switching to a struct with encoding/json (or easyjson/sonic) cut allocations dramatically. The prompt is excellent at spotting []byte(string) round-trips, which allocate twice.


7. Rust: Borrow Checker Triage

The prompt:

Here is a rustc error (E0502 / E0499 / E0505) and the function. Explain why the borrow checker rejects this in terms of lifetimes, then offer two fixes: (a) the minimal change, (b) the idiomatic refactor. Prefer restructuring over clone(). If Rc/RefCell or Arc<Mutex<T>> is genuinely required, say so explicitly and explain the runtime cost.

Why structured this way: the most common AI failure mode is suggesting .clone() as a reflex. Forcing "prefer restructuring" produces better answers — usually splitting a struct, reordering borrows, or using a method that takes &mut self and returns an owned value. Rust's borrow checker rules are documented in the official Rust Book, Chapter 4 ("Understanding Ownership") — a solid anchor.


8. Rust: Zero-Copy Parsing with bytes and lifetimes

The prompt:

Write a Rust parser (Rust 1.75+, no unsafe) that reads a length-prefixed framing protocol from a bytes::BytesMut buffer and yields borrowed slices (&[u8]) without copying the payload. Use Buf/BufMut from the bytes crate. The parser must return Result<Option<Frame<'_>>, ParseError> where Frame borrows from the buffer. Explain the lifetime relationships in comments.

The payoff: this is where Rust's ownership model genuinely shines — you get a parser that provably never copies payload bytes and never reads past the buffer. The lifetime annotation Frame<'_> tied to the input buffer is the crux, and AI handles the mechanical part well if you specify "no unsafe" up front.


9. Rust: Trait Objects vs. Generics Decision

The prompt:

I need a plugin-style architecture (Rust 1.75+) where handlers are registered at runtime. Compare Box<dyn Handler> vs. a generic Handler parameter with static dispatch. Cover: object safety, vtable overhead, Send + Sync bounds for use across Tokio tasks, and whether async fn in traits (stabilized in 1.75) changes the calculus. Recommend one and write the trait + registry.

Nuance: async fn in traits is stable but has caveats — it's not directly object-safe without #[async_trait] or the newer trait-variant crate. The model should surface this; if it doesn't, that's your signal to push back.


10. Tokio: Task Cancellation and Graceful Shutdown

The prompt:

Here is a Tokio (1.x) service with spawned tasks. Write a graceful shutdown using tokio::signal and a CancellationToken (from tokio-util). Ensure: in-flight requests complete, no task is leaked, and shutdown is bounded by a timeout. Show what happens if a task ignores cancellation.

Why: Tokio's cancellation is cooperative — dropping a JoinHandle does not stop the task. The prompt forces the model to address this honestly rather than pretending abort() is clean. Real incident: a service that "shut down" in logs while background tasks kept writing to a database for minutes.


11. Memory Safety Review: unsafe Block Justification

The prompt:

Review this unsafe block. For each unsafe operation, state the exact safety invariant it relies on and whether it's upheld by surrounding code. Check: pointer provenance, aliasing (&mut exclusivity), alignment, initialization, and Send/Sync auto-trait implications. If the invariant is unprovable, propose a safe alternative or a debug_assert! that would catch violations in tests.

Context: Rust's unsafe is a contract, not an escape hatch. The reference is the Rustonomicon ("The Dark Arts of Unsafe Rust") — the canonical guide for what invariants unsafe code must uphold. This prompt is essentially a code-review checklist for the parts of Rust where the compiler stops helping.


12. Cross-Language: Go Service ↔ Rust Service over gRPC

The prompt:

I have a Go service and a Rust service communicating over gRPC (protobuf3). Write the shared .proto, a tonic (Rust) server implementation, and a grpc-go client. Cover: error mapping between tonic::Status and Go's status.Error, deadline propagation, and how to stream large payloads without buffering everything in memory (chunking + bytes::Bytes on the Rust side).

The interesting part: error semantics differ subtly. Rust's tonic::Status maps to gRPC status codes the same way Go does, but the idioms differ (Result<T, Status> vs. (T, error)). Deadlines propagate via the grpc-timeout header — mention it explicitly and the model will wire it correctly.


What these prompts have in common

Principle Why it matters
State the toolchain version Go 1.22 loop semantics, Rust 1.75 async-in-traits — advice changes across versions
Paste real artifacts Dumps, compiler errors, pprof output — not paraphrases
Forbid the lazy fix "No clone()", "no timeout band-aid" — forces real solutions
Ask for the invariant Concurrency and unsafe code are about contracts, not syntax
Demand a verification step A benchmark, a debug_assert!, a Mermaid graph — something checkable

None of these prompts replace understanding. They replace the first 30 minutes of every debugging session — the part where you're staring at a stack trace, knowing the answer is in there somewhere. That's the part AI is genuinely good at: it doesn't get tired of reading 4000 goroutine stacks, and it doesn't have an ego about being wrong on the first try.

Keep them in a snippet manager. Version them alongside your toolchain. And when a prompt gives you a fix that compiles but feels wrong — trust that instinct and push back. The compiler will tell you if you're right; the AI usually will too, if you ask it to justify itself. That back-and-forth is the workflow now.

← All posts

Comments