Go and Rust Prompt Engineering: 12 Battle-Tested Templates for Faster Backends, Safe Concurrency, and Python Migration

Go and Rust dominate modern backend conversations for good reason: Go compiles in seconds, ships static binaries, and its goroutine scheduler makes concurrency approachable. Rust eliminates data races at compile time and delivers predictable performance without a garbage collector. But LLMs often produce mediocre code in both languages unless you write precise prompts. A vague "write a Go server" yields toy code with no error handling, no context cancellation, and no benchmarks. A well-structured prompt produces production-ready code with tests and profiling hooks.

This guide provides 12 concrete, copy-paste-ready prompts for Go and Rust code generation, refactoring, and optimization. Each includes a usage example and a note on the task it solves. Use them with any capable LLM (GPT-4, Claude, Gemini, or open-weight models like DeepSeek-Coder and Qwen2.5-Coder). The prompts assume you know your domain; they just make the model work at your level.

1. Goroutines and Channels: Worker Pool with Graceful Shutdown

Task: Generate a bounded worker pool that processes jobs concurrently, respects context cancellation, and shuts down without leaking goroutines.

Prompt:

You are a senior Go engineer. Write a worker pool with these requirements:
- N workers read from a jobs channel (chan Job)
- Results go to a results channel (chan Result)
- Respect context.Context cancellation: when ctx is done, workers stop accepting new jobs
- Use sync.WaitGroup to wait for all workers
- The pool exposes Submit(job Job) bool and Close()
- No goroutine leaks: verify with a test using runtime.NumGoroutine()
Provide the full file, a main.go demo, and a test file. Explain why we close the jobs channel before waiting.

Example: For an image-resizing service processing 10k uploads/hour, this prompt yields a pool with backpressure via a buffered channel and safe shutdown on SIGTERM. The generated test typically asserts goroutine count returns to baseline after Close().

Why it works: It names the exact primitives (WaitGroup, context) and demands a leak test. LLMs default to naive go func() loops; this forces discipline.

2. Refactoring a Python Script to Idiomatic Go

Task: Migrate a small Python service (e.g., a Flask endpoint doing DB queries and JSON serialization) to Go.

Prompt:

Convert this Python Flask service to idiomatic Go using net/http (no frameworks).
Requirements:
- Keep the same routes and JSON response shapes
- Use database/sql with prepared statements
- Add context timeouts (5s) on DB calls
- Return proper HTTP status codes (400 for bad input, 500 for internal errors)
- Include a Makefile with build, test, and run targets
Python code:
<paste code>

Example: A 200-line Flask app with three endpoints becomes a ~300-line Go service with http.Server{ReadTimeout, WriteTimeout} configured. The prompt prevents the model from importing Gin or Echo unless you want it.

Why it works: Specifying "no frameworks" and exact status codes avoids the common LLM habit of adding dependencies. Requesting a Makefile makes the output immediately runnable.

3. Borrow Checker Errors: Explain and Fix

Task: Diagnose a Rust compile error involving lifetimes or mutable aliasing.

Prompt:

You are a Rust expert. The compiler reports:
<paste full error including E-code>
Here is the relevant code:
<paste code>
1. Explain the error in plain English.
2. Show the minimal fix.
3. Show an alternative using Rc<RefCell<T>> or Arc<Mutex<T>> and explain when each is appropriate.
4. Add a unit test proving the fix compiles and runs.

Example: A common error is cannot borrow 'x' as mutable more than once at a time when iterating and mutating a Vec. The model will suggest retain or index-based iteration, plus a test.

Why it works: Asking for both minimal and alternative fixes teaches the underlying ownership model rather than just patching.

4. Async/Await in Rust with Tokio

Task: Build a concurrent HTTP client that fetches multiple URLs with a timeout and returns results in order.

Prompt:

Write a Rust async program using tokio and reqwest.
Requirements:
- Fetch a list of URLs concurrently with a max of 10 in flight (use futures::stream::iter + buffer_unordered)
- Per-request timeout of 3 seconds
- Collect status codes and body lengths
- Return results in the original order
- Include Cargo.toml with pinned versions and a main that reads URLs from args
Explain the difference between join_all and buffer_unordered.

Example: This pattern is standard for web scrapers and health-check tools. The prompt yields code using tokio::time::timeout and FuturesUnordered, with a clear explanation of backpressure.

Why it works: Naming buffer_unordered and the order requirement pushes the model past the naive join_all approach that ignores rate limits.

5. Benchmarking Go Code with testing.B

Task: Write benchmarks for a hot function and interpret the results.

Prompt:

Write Go benchmarks for this function using testing.B:
<paste function>
Requirements:
- Benchmark with b.ReportAllocs()
- Include a sub-benchmark for input sizes 100, 1000, 10000
- Use b.RunParallel for a concurrent variant
- Show how to run with -benchmem and interpret ns/op and B/op
- Suggest one optimization based on the likely allocation pattern

Example: For a JSON parsing function, the benchmark reveals allocations from []byte conversions. The model suggests json.Decoder with a reused buffer, often cutting B/op significantly.

Why it works: b.ReportAllocs() and -benchmem are concrete flags; the model produces runnable commands like go test -bench=. -benchmem.

6. Profiling Rust with perf and Flamegraph

Task: Identify CPU hotspots in a Rust binary and generate a flamegraph.

Prompt:

My Rust binary is CPU-bound. Give me a step-by-step workflow to profile it on Linux:
- Build with debug symbols in release mode (Cargo.toml settings)
- Run perf record with the right flags
- Generate a flamegraph using cargo-flamegraph
- Interpret the output and identify the top 3 functions
Include exact commands and a sample Cargo.toml [profile.release] section.

Example: The output typically includes [profile.release] debug = true and perf record -g --call-graph dwarf ./target/release/app. This is the standard workflow documented by the Rust Performance Book.

Why it works: Asking for exact commands and profile settings avoids vague advice. The model references real tools (perf, cargo-flamegraph).

7. Migrating a Python Microservice to Go: Step-by-Step Plan

Task: Plan and execute a migration of a Python service to Go without downtime.

Prompt:

Create a migration plan from Python (FastAPI) to Go for a service with 5 endpoints and a PostgreSQL database.
Include:
- Strangler Fig pattern: run both services behind a reverse proxy, shift traffic gradually
- Shared schema: use the same migrations, no DB changes
- Contract tests to compare responses between Python and Go
- Rollback strategy
- A checklist for the first week in production
Output as a Markdown table with phases, tasks, and success criteria.

Example: Teams often use Nginx or Envoy to split traffic by percentage. The prompt yields a phased plan with measurable criteria like "p99 latency within 10% of Python baseline."

Why it works: Naming the Strangler Fig pattern and contract tests forces a realistic plan instead of a big-bang rewrite.

8. Error Handling in Go: Wrapping and Sentinels

Task: Refactor error handling to use errors.Is, errors.As, and %w.

Prompt:

Refactor this Go code to use idiomatic error handling:
<paste code>
Requirements:
- Wrap errors with fmt.Errorf("...: %w", err)
- Define sentinel errors with errors.New for known cases
- Use errors.Is for comparisons and errors.As for typed errors
- Add a custom error type with Unwrap()
- Show how to log with slog while preserving the error chain

Example: A DB layer returns ErrNotFound; the handler uses errors.Is(err, ErrNotFound) to return 404. This is the pattern recommended in the Go blog post "Working with Errors in Go 1.13."

Why it works: It references real APIs and a specific Go version, so the model produces correct, modern code.

9. Rust Ownership: Converting a Tree Structure

Task: Implement a tree with parent pointers without fighting the borrow checker.

Prompt:

Implement a tree in Rust where nodes need parent references.
Compare three approaches:
1. Arena with indices (Vec<Node> + usize)
2. Rc<RefCell<Node>> with Weak for parent
3. Indices with a generational arena crate (e.g., slotmap)
Provide code for each, and explain trade-offs in terms of performance, safety, and ergonomics. Recommend one for a read-heavy workload.

Example: The arena approach is common in parsers and compilers. The model will show struct Node { parent: Option<usize>, children: Vec<usize> } and explain why it avoids RefCell runtime checks.

Why it works: Comparing approaches with explicit trade-offs produces a decision framework, not just code.

10. Concurrency Safety: Detecting Data Races

Task: Find and fix data races in Go code using the race detector.

Prompt:

Analyze this Go code for data races:
<paste code>
1. Identify all shared mutable state.
2. Show how to run the race detector: go test -race ./...
3. Propose fixes using sync.Mutex, atomic, or channels.
4. Write a test that fails with -race before the fix and passes after.

Example: A common bug is incrementing a counter from multiple goroutines. The model suggests atomic.AddInt64 or a mutex, and provides a test that reliably triggers the race detector.

Why it works: The -race flag is a real, powerful tool; requiring a failing-then-passing test makes the fix verifiable.

11. Cross-Compilation and Static Binaries

Task: Build Go binaries for Linux, macOS, and Windows with CGO disabled.

Prompt:

Write a Makefile that cross-compiles a Go program for linux/amd64, linux/arm64, darwin/arm64, and windows/amd64.
Requirements:
- CGO_ENABLED=0 for static binaries
- Output to dist/ with clear naming
- Include a target for Docker multi-stage build using scratch or distroless
- Show how to verify the binary is static with file and ldd

Example: The resulting Dockerfile uses FROM gcr.io/distroless/static for a tiny image. This is standard practice for Go services.

Why it works: It specifies real base images and verification commands, producing deployable artifacts.

12. Testing Rust with Property-Based Tests

Task: Add property-based tests to a Rust function using proptest.

Prompt:

Write property-based tests for this Rust function using proptest:
<paste function>
Requirements:
- Define invariants (e.g., round-trip encode/decode, idempotence)
- Use proptest! macro with strategies for strings, integers, and vectors
- Include a regression test for a known edge case
- Explain how proptest shrinks failing inputs

Example: For a custom serialization format, the round-trip property catches edge cases like empty strings and Unicode. Proptest then minimizes the failing case automatically.

Why it works: Property-based testing is underused; naming proptest and invariants yields high-value tests beyond example-based cases.

Prompt # Focus Language Key Tool/API
1 Worker pool Go context, WaitGroup
2 Python to Go Go net/http, database/sql
3 Borrow checker Rust lifetimes, Rc/RefCell
4 Async/await Rust tokio, reqwest
5 Benchmarking Go testing.B, -benchmem
6 Profiling Rust perf, cargo-flamegraph
7 Migration plan Go Strangler Fig
8 Error handling Go errors.Is/As, %w
9 Ownership patterns Rust arena, RefCell
10 Data races Go -race detector
11 Cross-compilation Go CGO_ENABLED=0
12 Property tests Rust proptest

These prompts are starting points. The more context you provide—your actual code, error messages, and performance targets—the better the output. For deeper structured learning on Go, Rust, and AI-assisted development, explore courses at asibiont.com. If you want to go further, try combining prompts: ask the model to benchmark the code it just generated, then profile it. That loop—generate, measure, optimize—is how you turn LLM output into production systems.

← All posts

Comments