Ask an LLM to "write Go code" and you often get a Java-flavored mutant: a panic where an error should be, a sync.Mutex guarding everything, and a context that exists only in the README. The code compiles. It may even pass a happy-path test. Then it meets a network timeout, a cancelled request, or two goroutines touching the same map — and the failure is silent, intermittent, and expensive.
The difference between that output and production-grade Go is rarely the model. It is the prompt. A prompt that names Go's idioms — explicit error returns, channels over shared memory, context.Context threaded through every blocking call — consistently produces code that a Go reviewer would not immediately reject.
This roundup is built around a single criterion: if the task involves concurrency or network calls, you need prompts that specify concurrency and context requirements; if it is simple CRUD, a basic prompt with an explicit style instruction is enough. Below are 12 prompts, each with the failure it prevents and a usage example. Copy them, adapt the bracketed parts, and keep them in a snippet library.
The two modes of working with an LLM in Go
Before the prompts, a mental model. Most bad Go from LLMs falls into two buckets:
- Style drift: the model writes Go syntax with another language's habits — exceptions instead of errors, classes instead of structs, inheritance-like embedding abuse.
- Concurrency drift: the model adds goroutines because they look impressive, then protects them with a mutex and forgets cancellation, backpressure, and
defercleanup.
A prompt can fix style in one sentence. Concurrency needs structure: you must state the synchronization primitive, the cancellation path, and the shutdown condition. The prompts below are ordered roughly from "always useful" to "only for concurrent and networked code."
Prompt 1: The idiomatic baseline — explicit errors, no panics
For: any task, from a CLI tool to a service. This is the prompt you paste before anything else.
Why it works: Go's error handling is not a limitation to be worked around; it is the contract. Telling the model that panic is reserved for programmer errors and unrecoverable states removes its favorite shortcut.
Prompt (copy-paste):
Write Go code for [task]. Requirements:
- Return errors explicitly; wrap with fmt.Errorf("doing X: %w", err).
- Never use panic for expected failures (I/O, network, validation).
- Define sentinel errors with errors.New for cases callers must check.
- No global mutable state.
- Use structs with unexported fields and constructor functions.
- Include a short example of how the caller handles each returned error.
Usage example: ask for a config loader. A weak answer returns panic("config not found"). With this prompt you get return fmt.Errorf("loading config %q: %w", path, err) and a caller that checks errors.Is(err, ErrConfigMissing). That difference is the entire debuggability of your service at 3 a.m.
Prompt 2: Channels over mutexes — and when the model should push back
For: producer–consumer pipelines, fan-out workers, event buses.
Why it works: LLMs default to mutexes because they are the lowest-common-denominator synchronization primitive across languages. Forcing a channel-based design makes ownership explicit — and ownership is what prevents races.
Prompt:
Design [pipeline] using goroutines and channels. Rules:
- Prefer channels for handoff of data between goroutines.
- Use sync.Mutex only for guarding a small struct's fields; justify each use in a comment.
- State clearly which goroutine owns each channel and who closes it.
- Show the shutdown path: how does a producer stop when the consumer is gone?
- No goroutine may outlive the function that started it without a documented reason.
Usage example: a log-processing fan-out. The model will typically produce a jobs := make(chan Job), N workers, and a sync.WaitGroup. The prompt forces it to add close(jobs) in the producer and a done channel for early exit — two lines that prevent a goroutine leak.
go test and then memory climbs for days. Always ask the model to show the shutdown path explicitly, and verify with go test -race and a goroutine-count check.
Prompt 3: Context threading — every blocking call gets a deadline
For: HTTP handlers, database calls, gRPC clients, anything that can hang.
Why it works: context.Context is Go's cancellation contract, but models often accept it as a parameter and never check it. The prompt makes propagation testable.
Prompt:
Implement [function] with context.Context as the first parameter.
- Propagate ctx to every blocking call (HTTP, DB, channel receive via select).
- Never store ctx in a struct.
- Add a select on ctx.Done() around every channel receive and long loop.
- Return ctx.Err() unwrapped when cancellation is the cause.
- Show a caller that sets a 2-second timeout.
Usage example: an HTTP client wrapper. Without the prompt, the model writes http.Get(url) — no timeout, no cancellation. With it, you get http.NewRequestWithContext(ctx, ...) and a select that returns promptly when the caller disconnects.
Prompt 4: The race-detector contract
For: any code with shared state, before you merge.
Why it works: you cannot ask a model to "be careful." You can ask it to enumerate shared state and prove each access is synchronized.
Prompt:
Review this Go code for data races:
[code]
For each variable shared between goroutines, list:
1. Which goroutines read and write it.
2. The synchronization that protects each access (mutex, channel, atomic).
3. Any access path that is unsynchronized.
Then rewrite the code so that `go test -race` would pass, and explain the change.
Usage example: a counter incremented inside go func(). The model will identify count++ as unsynchronized and propose either atomic.Int64 or a channel-based aggregator — and, crucially, tell you which one fits the access pattern. This is also a natural companion to the bug-hunting workflow in Your Second Reviewer Never Sleeps.
Prompt 5: Error wrapping and sentinel errors for a real API
For: library or service boundaries where callers must distinguish failure modes.
Why it works: wrapping with %w preserves the chain for errors.Is and errors.As, but models often wrap with %v, silently breaking it. The prompt makes the contract explicit.
Prompt:
Define the error contract for [package]:
- Sentinel errors for conditions callers must branch on.
- Wrap with %w, never %v, when adding context.
- A custom error type with errors.As support for [validation failure].
- Show three caller examples: errors.Is, errors.As, and a generic log.
Usage example: a payment client. You get ErrInsufficientFunds, ErrCardDeclined, and a ValidationError type — so the caller does not string-match error messages, which is the classic silent breakage when wording changes.
Prompt 6: The table-driven test with a cancellation case
For: proving concurrency code actually behaves under cancellation.
Why it works: the model writes happy-path tests unless told otherwise. Requiring a cancellation case forces it to exercise ctx.Done().
Prompt:
Write table-driven tests for [function] using testing package only.
Cases must include: success, error return, context cancellation mid-flight, and timeout.
Use t.Run for subtests. For the cancellation case, cancel the context after 10ms and assert the function returns ctx.Err().
Usage example: a worker pool. The cancellation case catches the classic bug where the worker ignores ctx and blocks on a channel send forever. This prompt pairs well with the debugging prompts in The LLM Detective.
Prompt 7: Bounded concurrency — no unbounded goroutine spawn
For: fan-out over a list of URLs, rows, or messages.
Why it works: for _, x := range xs { go f(x) } is the most common LLM concurrency pattern and the fastest way to exhaust file descriptors. The prompt caps it.
Prompt:
Process [N items] concurrently with a bounded worker pool.
- Maximum K goroutines (K configurable, default 8).
- Use a semaphore (buffered channel) or errgroup with SetLimit.
- Collect the first error and cancel remaining work via context.
- Return all results in input order.
Usage example: fetching 10,000 URLs. Unbounded, this opens 10,000 connections; bounded at 8, it finishes predictably and respects rate limits. Note the trade-off: bounded concurrency is slower on a fast local network but stable in production — choose based on the downstream service's limits, not your benchmark.
Prompt 8: Graceful shutdown with signal handling
For: services, workers, CLIs that hold resources.
Why it works: shutdown is where context, channels, and defer meet. Models routinely forget signal.NotifyContext and leak connections on SIGTERM.
Prompt:
Write the main function for [service] with graceful shutdown:
- signal.NotifyContext for SIGINT and SIGTERM.
- Pass ctx to the server; on cancel, stop accepting and wait for in-flight work with a timeout.
- Close all resources with defer in reverse order.
- Log shutdown steps.
Usage example: an HTTP server. You get srv.Shutdown(ctx) with a 5-second deadline and a log line per phase — the difference between a clean deploy and dropped requests during rollout.
Prompt 9: Select with default — non-blocking and backpressure
For: queues, metrics collectors, anything that must not block the caller.
Why it works: select with default is a subtle idiom; models often omit it and create hidden blocking.
Prompt:
Implement [enqueue function] that never blocks the caller.
- Use select with default to drop or buffer when full.
- Expose a metric or counter for drops.
- Document the backpressure policy: drop, block, or grow.
Usage example: a metrics channel. The model writes select { case ch <- m: default: atomic.AddInt64(&dropped, 1) } — and you can alert on drops instead of discovering latency spikes.
Prompt 10: Refactor a mutex-heavy file to channels (or the reverse)
For: legacy code that "works" but is unmaintainable.
Why it works: conversion prompts force the model to reason about ownership, not just syntax.
Prompt:
Refactor this file from mutex-based sharing to channel-based ownership:
[code]
Preserve the public API. Explain which invariants the mutex protected and how the channel design preserves them. If the mutex is a better fit, say so and explain why — do not refactor for its own sake.
Usage example: a cache struct with a mutex and a Get/Set. The model may correctly conclude that a sync.RWMutex is the right tool and a channel would add latency — a useful, non-obvious answer that a naive prompt would never produce.
Prompt 11: The style linter prompt — enforce Go conventions in review
For: reviewing LLM output before commit.
Why it works: it turns the model into a reviewer with a checklist, catching the style drift from the introduction.
Prompt:
Review this Go code against Go conventions:
[code]
Check: error strings lowercase without punctuation; no stuttering names (user.UserID → user.ID); interfaces defined at the consumer, not the producer; no init() with side effects; defer before the resource is used; short variable names in short scopes.
Return a numbered list of violations with fixes.
Usage example: a package that returns errors.New("Failed to load."). The reviewer flags the capital and the period, and suggests errFailedToLoad. Small, but it is what makes a codebase feel like Go.
Prompt 12: The decision prompt — should this even be concurrent?
For: the beginning of any task where concurrency is tempting.
Why it works: it inverts the default. Instead of "make it concurrent," it asks the model to justify concurrency against a sequential baseline.
Prompt:
For [task], compare a sequential implementation and a concurrent one.
- Estimate whether the work is I/O-bound or CPU-bound.
- State the expected speedup and its ceiling (Amdahl's law qualitatively).
- List the new failure modes: races, leaks, deadlocks, partial failure.
- Recommend one and justify it in three sentences.
Usage example: parsing 200 small JSON files. The model may conclude that sequential parsing is fast enough and concurrency adds leak risk — saving you a week of debugging a race in code that did not need to exist.
When a basic prompt is enough
Not every task needs the full battery. For a CRUD handler, a repository layer, or a CLI flag parser, Prompts 1, 5, and 11 cover 90% of the value. Adding concurrency requirements to sequential code produces over-engineered output — mutexes guarding a single-threaded struct, contexts in functions that never block. Match the prompt to the task: concurrency and network calls justify the heavy prompts; pure data transformation does not.
A compact decision table
| Task type | Prompts to use | Main risk if skipped |
|---|---|---|
| Simple CRUD, CLI, parsing | 1, 5, 11 | Style drift, %v instead of %w |
| HTTP handlers, DB calls | 1, 3, 6, 8 | Missing timeouts, leaked connections |
| Fan-out over many items | 2, 7, 9 | Unbounded goroutines, FD exhaustion |
| Shared state, caches | 4, 10 | Data races, silent corruption |
| Before committing LLM output | 4, 11 | Races and style debt reaching main |
What this looks like in practice
Imagine a service that fetches prices from three suppliers and merges the results. A naive prompt yields three goroutines, a sync.WaitGroup, a shared map written without a lock, and no timeout — a race that appears under load and a hang when one supplier is slow.
Applying Prompts 3, 4, and 7 in sequence yields: errgroup.WithContext with a limit of 3, results collected through a channel, the first error cancelling the rest, and a 2-second deadline on each HTTP call. The code is longer by about 15 lines and shorter by an entire incident.
Run go test -race on both versions. The first fails intermittently; the second passes. That is the measurable difference a prompt makes.
If you want to go deeper into the language-level trade-offs behind these patterns — including where Go's concurrency model meets systems programming constraints — see Go and Rust — Systems Programming: A Real-World Task and Pitfalls. For the broader question of turning LLM output into a reliable engineering process rather than a demo, LLM Engineering with LangChain: RAG and Agents is a useful companion.
The takeaway is simple: an LLM writes the Go you describe. Describe errors, ownership, cancellation, and shutdown — and you get code that survives production. Describe only the feature, and you get a demo that compiles. Keep these 12 prompts in your snippet library, start every Go task with Prompt 1, and add the concurrency prompts the moment a go keyword appears in your requirements.