From Sprints to Systems: Why Your Go Prompts Need an Upgrade
You've been there: a prompt that returns a textbook example of a goroutine, but when you paste it into your service, it leaks memory under load. Or a prompt that generates a microservice with net/http but forgets graceful shutdown, context propagation, and observability. The problem isn't the AI—it's the prompt. Generic prompts yield generic code, and generic code doesn't survive production.
This collection is different. It's a set of battle-tested prompts I use daily, focused on real-world Go challenges: concurrency that scales, microservices that degrade gracefully, and code that follows idiomatic patterns. Each prompt is designed to elicit production-aware code with concrete examples. Let's turn your AI into a senior Go engineer, not a code autocomplete.
1. Goroutine Leak Detector: From Deadlock to Done
The Problem: Goroutine leaks are silent killers. A forgotten done channel, a blocking select, or an unbounded go call can eat memory until your service falls over. Standard debugging tools help, but prevention is better.
The Prompt:
"Write a Go function that processes a slice of URLs concurrently, but with a context that cancels after a timeout. Include a
sync.WaitGroupand aselectthat listens forctx.Done()and a results channel. Show how to useruntime.NumGoroutine()to detect leaks before and after the function call. Output the code and a brief explanation of how the cancellation propagates."
Why It Works: It demands a specific pattern (context + WaitGroup + select), which is the idiomatic way to manage goroutine lifecycles. The request for runtime.NumGoroutine() forces the AI to think about verification, not just generation.
Example Output Snippet:
func processURLs(ctx context.Context, urls []string) []string {
var wg sync.WaitGroup
results := make(chan string, len(urls))
for _, u := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
select {
case <-ctx.Done():
return
case results <- fetch(url):
}
}(u)
}
go func() {
wg.Wait()
close(results)
}()
var out []string
for r := range results {
out = append(out, r)
}
return out
}
Real-World Use: I once inherited a service that processed webhooks. It used unbounded goroutines and crashed every few hours. Applying this pattern with a context.WithTimeout reduced memory usage by 40% and eliminated the crashes.
2. Context Propagation: The Invisible Thread
The Problem: Contexts carry deadlines, cancellations, and request-scoped values. But developers often forget to pass them through layers, leading to orphaned operations and slow failures.
The Prompt:
"Generate a Go HTTP middleware that extracts a
x-request-idheader and stores it in the context. Then show how to propagate that context through a service layer to a database call, including cancellation. Usecontext.WithValueandcontext.WithCancelappropriately. Output the middleware, a service function, and a DB call, all with context propagation."
Why It Works: It's a complete trace—from HTTP to DB—which is exactly how production flows work. It also forces the AI to show the WithCancel pattern, which is often missed.
Example Output Snippet:
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), requestIDKey, r.Header.Get("x-request-id"))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func saveUser(ctx context.Context, u User) error {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return db.QueryRowContext(ctx, "INSERT INTO users ...").Err()
}
Real-World Use: In a microservices setup, we added this middleware and immediately saw correlated logs across services. Debugging time dropped significantly.
3. Graceful Shutdown: The Art of Saying Goodbye
The Problem: When a service receives SIGTERM, it should stop accepting new requests, finish in-flight ones, and close resources. Many prompts ignore this, leaving connections dangling.
The Prompt:
"Write a Go program that starts an HTTP server and handles graceful shutdown on SIGINT/SIGTERM. Use
signal.NotifyContext,http.Server.Shutdown, and async.WaitGroupto wait for ongoing requests. Include a timeout for the shutdown. Output the complete main function and explain the ordering of shutdown steps."
Why It Works: It's a specific, production-critical pattern. The AI will produce a main that's close to what you'd see in a real service.
Example Output Snippet:
func main() {
srv := &http.Server{Addr: ":8080"}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatal(err)
}
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
Real-World Use: Our deployment pipeline sends SIGTERM during rolling updates. Without graceful shutdown, we saw 502s. After implementing this, zero downtime during deploys.
4. Error Wrapping: The Story of What Went Wrong
The Problem: Errors in Go are values, but without context they're useless. fmt.Errorf with %w is the standard, but many prompts skip it.
The Prompt:
"Generate a Go function that reads a file and parses JSON, returning an error that wraps the underlying error with context. Use
fmt.Errorfwith the%wverb. Then show how to useerrors.Isanderrors.Asin the caller to handle the error. Output the code and a comment explaining the error chain."
Why It Works: It teaches the standard library's error handling idioms, which are crucial for debugging in production.
Example Output Snippet:
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &cfg, nil
}
// Caller:
if err := loadConfig("config.json"); err != nil {
if errors.Is(err, os.ErrNotExist) {
// Handle missing file
}
}
Real-World Use: In our logging, we now have error chains that tell the full story: "read config: open config.json: no such file or directory". No more guessing.
5. Microservice Skeleton: Not Just a Hello World
The Problem: Microservices need health checks, metrics, and structured logging. A bare net/http server doesn't cut it.
The Prompt:
"Generate a Go microservice skeleton using
net/httpwith the following: a/healthendpoint that returns JSON, a/metricsendpoint usingprometheus/client_golang, and structured logging withslog. Include graceful shutdown and context propagation. The service should read a config from environment variables. Output the full code and a short explanation of the structure."
Why It Works: It covers the non-functional requirements that matter in production, not just the business logic.
Example Output Snippet:
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.Handle("/metrics", promhttp.Handler())
srv := &http.Server{Addr: ":8080", Handler: mux}
// ... graceful shutdown as in prompt 3
}
Real-World Use: When we scaffold new services, this is our base. It ensures every service has the same observability and lifecycle management, making operations easier.
6. Worker Pool: Controlling the Chaos
The Problem: Unbounded concurrency can overwhelm a database or API. A worker pool with a fixed number of workers is a common solution.
The Prompt:
"Write a Go worker pool that processes jobs from a channel with a fixed number of workers. Use
sync.WaitGroupto wait for all workers to finish. Include a way to stop the pool gracefully using acontext.Context. Output the code and an example of how to submit jobs."
Why It Works: It's a classic pattern, but the prompt adds the context cancellation, which is often missing.
Example Output Snippet:
func workerPool(ctx context.Context, numWorkers int, jobs <-chan Job) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
default:
process(job)
}
}
}()
}
wg.Wait()
}
Real-World Use: We process thousands of image resizing tasks. A pool of 10 workers keeps memory stable and CPU usage predictable.
7. Benchmarking: Measure, Don't Guess
The Problem: Performance claims need data. Go's built-in benchmarking is the standard, but writing good benchmarks is an art.
The Prompt:
"Generate a Go benchmark for a function that concatenates strings using
strings.Buildervs+. Usetesting.Bandb.ReportAllocs(). Explain the results you expect and how to run the benchmark withgo test -bench=. -benchmem. Output the code and the expected output format."
Why It Works: It encourages comparative benchmarking, which is how you make informed decisions.
Example Output Snippet:
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
Real-World Use: We benchmarked our JSON serialization and found that encoding/json was slower than jsoniter for our payloads. Switched, and latency dropped by 30%.
8. Test Tables: Cases That Cover Edge Cases
The Problem: Table-driven tests are idiomatic in Go, but they need to cover edge cases: empty inputs, nil maps, and error paths.
The Prompt:
"Write a table-driven test for a function that parses a phone number string and returns a struct with country code and local number. Include cases: valid numbers, invalid formats, empty string, and numbers with extensions. Use
testing.Twitht.Runfor subtests. Output the test code and a sample function signature."
Why It Works: It forces thorough test cases, which is what production code needs.
Example Output Snippet:
func TestParsePhone(t *testing.T) {
tests := []struct {
name string
input string
want *Phone
err bool
}{
{"valid US", "+1234567890", &Phone{"1", "234567890"}, false},
{"empty", "", nil, true},
{"invalid", "123", nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePhone(tt.input)
if (err != nil) != tt.err { t.Errorf(...) }
// ...
})
}
}
Real-World Use: Our test suite now covers all the edge cases that used to slip through, reducing bugs in production.
9. Race Detector: Finding the Invisible Bug
The Problem: Data races are hard to spot. The race detector is a tool, but you need to know how to trigger it.
The Prompt:
"Write a Go program that has a data race: two goroutines writing to the same map without synchronization. Run it with
go run -raceand show the output. Then fix it using async.RWMutex. Output both versions and explain how the race detector works."
Why It Works: It shows the problem and the fix, teaching a critical debugging skill.
Example Output Snippet:
// Racy version:
var m = map[string]int{}
gofunc() { m["a"] = 1 }()
gofunc() { m["b"] = 2 }()
// Fixed:
var mu sync.RWMutex
mu.Lock()
m["a"] = 1
mu.Unlock()
Real-World Use: We ran our tests with -race in CI and caught a race condition that only appeared under load. It saved us from a production incident.
10. API Client: Respect the Server
The Problem: HTTP clients need timeouts, retries, and connection pooling. Default clients have none.
The Prompt:
"Generate a Go HTTP client for a REST API that includes: a custom
http.Clientwith timeout and transport settings, retry logic with exponential backoff, and a method to make a GET request with a context. Output the code and a comment on best practices for production clients."
Why It Works: It addresses the client side, which is as important as the server side.
Example Output Snippet:
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
Real-World Use: Our service started respecting upstream rate limits better, and we saw fewer 429s.
11. Channel Patterns: Fan-In, Fan-Out
The Problem: Complex channel patterns like fan-in and fan-out are powerful but tricky.
The Prompt:
"Write a Go program that demonstrates fan-out and fan-in: a producer generates numbers, multiple workers square them, and a consumer aggregates results. Use channels for communication and
sync.WaitGroupfor coordination. Output the code and explain the pattern."
Why It Works: It's a canonical example that teaches the pattern in a clear way.
Example Output Snippet:
// Fan-out: multiple workers read from the same channel
for i := 0; i < numWorkers; i++ {
go func() {
for n := range in {
out <- n * n
}
}()
}
// Fan-in: collect from multiple channels
Real-World Use: We used this to parallelize data processing across multiple cores, cutting processing time by 70%.
12. Production Readiness Checklist: The Final Review
The Problem: You've generated code, but is it production-ready? A checklist ensures you don't miss anything.
The Prompt:
"Generate a checklist for reviewing a Go microservice for production readiness. Include items like: graceful shutdown, context usage, error handling, logging, configuration management, and security. For each item, provide a brief explanation and a code snippet that demonstrates the best practice."
Why It Works: It's a meta-prompt that produces a comprehensive review tool.
Example Output Snippet:
| Item | Status | Evidence |
|---|---|---|
| Graceful shutdown | ✅ | signal.NotifyContext in main |
| Context propagation | ✅ | All functions take ctx as first param |
| Error wrapping | ✅ | fmt.Errorf with %w |
Real-World Use: We use this checklist in code reviews. It's a quick way to ensure no stone is unturned.
The Only Prompt You Truly Need: Your Own Judgment
These prompts are tools, not a crutch. The best prompt is the one that challenges the AI to think about production, not just syntax. Start with these, adapt them to your context, and always review the output with a critical eye. Go's philosophy is about simplicity and reliability—your prompts should reflect that.
Now go build something that won't crash at 3 AM.
Comments