10 Prompts for Go: Microservices, APIs, and CLI Tools
Introduction
Go (or Golang) has become a powerhouse for building scalable microservices, robust APIs, and lightning-fast command-line utilities. Its simplicity, built-in concurrency, and excellent standard library make it the go-to choice for developers at companies like Uber, Twitch, and Docker. But even with the best tools, the blank page problem is real: you know what to build, but not always how to start. That's where prompts come in — structured, battle-tested instructions you can feed to an AI assistant (like ChatGPT, Claude, or Copilot) to generate production-ready Go code in seconds.
This article is a curated collection of 10 prompts I actually use in my daily workflow. Each prompt includes a real-world scenario, a concrete example, and the resulting Go code. Whether you're building a REST API, a gRPC service, or a CLI tool with Cobra, these prompts will save you hours of boilerplate and debugging. Let's dive in.
1. Scaffold a Minimal REST API with Echo
Prompt: "Write a Go REST API using the Echo framework. Include endpoints GET /users, POST /users, and GET /users/:id. Use an in-memory slice as storage. Add request validation for the POST endpoint (name and email are required). Return proper HTTP status codes (201 for create, 404 for not found, 400 for bad request). Include a main.go file and a separate handler file. Use Go 1.22."
Why it works: Echo is one of the most popular Go web frameworks, known for its performance and middleware support. This prompt forces the AI to generate a clean separation of concerns (main vs handlers) and includes error handling — a must for production APIs.
Example output (handler.go):
package main
import (
"net/http"
"strconv"
"sync"
"github.com/labstack/echo/v4"
)
type User struct {
ID int `json:"id"`
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
var (
users = []User{}
mu sync.Mutex
nextID = 1
)
func getUsers(c echo.Context) error {
mu.Lock()
defer mu.Unlock()
return c.JSON(http.StatusOK, users)
}
func createUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if u.Name == "" || u.Email == "" {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "name and email are required"})
}
mu.Lock()
u.ID = nextID
nextID++
users = append(users, *u)
mu.Unlock()
return c.JSON(http.StatusCreated, u)
}
func getUserByID(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
}
mu.Lock()
defer mu.Unlock()
for _, u := range users {
if u.ID == id {
return c.JSON(http.StatusOK, u)
}
}
return c.JSON(http.StatusNotFound, map[string]string{"error": "user not found"})
}
2. Generate a gRPC Service with Protocol Buffers
Prompt: "Create a gRPC service in Go for a task management system. Define a .proto file with a service TaskService containing RPCs: CreateTask, GetTask, ListTasks. The Task message should have id, title, description, status (enum: PENDING, IN_PROGRESS, DONE), and created_at (timestamp). Generate the server-side Go code using protoc. Implement the server with an in-memory store. Use Go 1.22."
Why it works: gRPC is the backbone of many microservices architectures. This prompt covers the entire workflow: protobuf definition, code generation, and server implementation. It forces the AI to include enums and timestamps, which are common in real projects.
Example output (task.proto):
syntax = "proto3";
package task;
import "google/protobuf/timestamp.proto";
service TaskService {
rpc CreateTask (CreateTaskRequest) returns (Task);
rpc GetTask (GetTaskRequest) returns (Task);
rpc ListTasks (ListTasksRequest) returns (ListTasksResponse);
}
enum TaskStatus {
PENDING = 0;
IN_PROGRESS = 1;
DONE = 2;
}
message Task {
int32 id = 1;
string title = 2;
string description = 3;
TaskStatus status = 4;
google.protobuf.Timestamp created_at = 5;
}
message CreateTaskRequest {
string title = 1;
string description = 2;
}
message GetTaskRequest {
int32 id = 1;
}
message ListTasksRequest {
TaskStatus status = 1;
}
message ListTasksResponse {
repeated Task tasks = 1;
}
3. Build a CLI Tool with Cobra
Prompt: "Build a command-line tool in Go using Cobra. The tool is called 'todo' and supports commands: add (add a new task with a description), list (show all tasks), done (mark task as complete by ID). Tasks are stored in a JSON file in the user's home directory. Use Viper for configuration. Output should be colorized using the fatih/color package. Include a --verbose flag for debug logging."
Why it works: Cobra is the de facto standard for Go CLI tools (used by Kubernetes, Hugo, etc.). This prompt adds real-world complexity: file persistence, color output, and config management. It's perfect for internal DevOps tools.
Example output (cmd/add.go):
package cmd
import (
"fmt"
"os"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var addCmd = &cobra.Command{
Use: "add [description]",
Short: "Add a new task",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
verbose, _ := cmd.Flags().GetBool("verbose")
if verbose {
fmt.Println("Adding task:", args[0])
}
err := addTask(args[0])
if err != nil {
color.Red("Error: %v", err)
os.Exit(1)
}
color.Green("Task added successfully!")
},
}
func init() {
rootCmd.AddCommand(addCmd)
}
func addTask(description string) error {
// Implementation reads tasks.json, appends new task, writes back
return nil
}
4. Implement a Concurrent Worker Pool
Prompt: "Write a Go program that implements a worker pool pattern. Create 5 workers that process jobs from a channel. Each job is a URL to fetch. Workers should make HTTP GET requests and log the response status code. Use context.WithTimeout (5 seconds per request). Handle errors gracefully (log them, don't crash). The main function should send 20 URLs to the pool and wait for all workers to finish."
Why it works: Concurrency is Go's superpower. This prompt demonstrates a classic production pattern: bounded parallelism with error handling and timeouts. It's useful for web scrapers, health checkers, and batch processors.
Example output:
package main
import (
"context"
"fmt"
"log"
"net/http"
"sync"
"time"
)
func worker(ctx context.Context, id int, jobs <-chan string, wg *sync.WaitGroup) {
defer wg.Done()
for url := range jobs {
reqCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
if err != nil {
log.Printf("Worker %d: error creating request for %s: %v", id, url, err)
cancel()
continue
}
resp, err := http.DefaultClient.Do(req)
cancel()
if err != nil {
log.Printf("Worker %d: error fetching %s: %v", id, url, err)
continue
}
resp.Body.Close()
fmt.Printf("Worker %d: %s -> %d\n", id, url, resp.StatusCode)
}
}
func main() {
const numWorkers = 5
jobs := make(chan string, 20)
var wg sync.WaitGroup
ctx := context.Background()
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(ctx, i, jobs, &wg)
}
urls := []string{
"https://example.com",
"https://google.com",
// ... 18 more
}
for _, url := range urls {
jobs <- url
}
close(jobs)
wg.Wait()
}
5. Create a Simple HTTP Middleware for Logging and Metrics
Prompt: "Write Go middleware for the standard net/http library that: (1) logs each request with method, path, duration, and status code, (2) exposes a /metrics endpoint in Prometheus format that counts total requests by method and status. Use the prometheus/client_golang library. The middleware should wrap http.Handler. Provide a main.go that uses it."
Why it works: Observability is critical for microservices. This prompt covers both structured logging and Prometheus metrics — two pillars of production monitoring. The AI generates a reusable middleware that can be dropped into any HTTP server.
Example output:
package main
import (
"log"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
totalRequests = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests.",
},
[]string{"method", "status"},
)
)
func init() {
prometheus.MustRegister(totalRequests)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(ww, r)
duration := time.Since(start)
totalRequests.With(prometheus.Labels{
"method": r.Method,
"status": http.StatusText(ww.statusCode),
}).Inc()
log.Printf("%s %s %d %s", r.Method, r.URL.Path, ww.statusCode, duration)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
mux.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", loggingMiddleware(mux))
}
6. Write a Database Migration Tool in Go
Prompt: "Create a Go package for database migrations. It should support PostgreSQL only. The tool reads SQL files from a 'migrations' directory (named like 001_create_users.sql). It maintains a schema_migrations table to track which migrations have been applied. Implement methods: Up (apply all pending), Down (rollback last migration), and Status (show current state). Use database/sql with lib/pq driver."
Why it works: Schema changes are inevitable. This prompt produces a lightweight, embeddable migration tool — perfect for microservices that don't want to depend on external tools like Flyway or golang-migrate.
Example output:
package migration
import (
"database/sql"
"fmt"
"io/ioutil"
"path/filepath"
"sort"
"strings"
)
type Migrator struct {
db *sql.DB
}
func New(db *sql.DB) *Migrator {
return &Migrator{db: db}
}
func (m *Migrator) ensureTable() error {
_, err := m.db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY,
applied_at TIMESTAMP DEFAULT NOW()
)`)
return err
}
func (m *Migrator) Up() error {
if err := m.ensureTable(); err != nil {
return err
}
files, err := ioutil.ReadDir("migrations")
if err != nil {
return err
}
var versions []int
for _, f := range files {
if strings.HasSuffix(f.Name(), ".sql") {
var v int
fmt.Sscanf(f.Name(), "%d", &v)
versions = append(versions, v)
}
}
sort.Ints(versions)
for _, v := range versions {
var count int
m.db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = $1", v).Scan(&count)
if count > 0 {
continue
}
content, err := ioutil.ReadFile(filepath.Join("migrations", fmt.Sprintf("%03d_*.sql", v)))
if err != nil {
return err
}
_, err = m.db.Exec(string(content))
if err != nil {
return fmt.Errorf("migration %d failed: %w", v, err)
}
_, err = m.db.Exec("INSERT INTO schema_migrations (version) VALUES ($1)", v)
if err != nil {
return err
}
fmt.Printf("Applied migration %d\n", v)
}
return nil
}
7. Generate a Unit Test Suite with Table-Driven Tests
Prompt: "Write table-driven unit tests in Go for a function that calculates the Fibonacci sequence. The function signature is Fib(n int) (int, error). It should return an error if n < 0. Use the testing package. Include test cases for n=0, n=1, n=10, n=-1 (error case), and n=20. Use subtests with t.Run. Also add a benchmark."
Why it works: Table-driven tests are idiomatic in Go. This prompt teaches good testing practices: edge cases, error handling, and benchmarks. It's a template you can reuse for any function.
Example output:
package fib
import (
"testing"
)
func Fib(n int) (int, error) {
if n < 0 {
return 0, fmt.Errorf("n must be non-negative")
}
if n <= 1 {
return n, nil
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b, nil
}
func TestFib(t *testing.T) {
tests := []struct {
name string
n int
want int
err bool
}{
{"zero", 0, 0, false},
{"one", 1, 1, false},
{"ten", 10, 55, false},
{"negative", -1, 0, true},
{"twenty", 20, 6765, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Fib(tt.n)
if (err != nil) != tt.err {
t.Errorf("Fib(%d) error = %v, wantErr %v", tt.n, err, tt.err)
}
if got != tt.want {
t.Errorf("Fib(%d) = %d, want %d", tt.n, got, tt.want)
}
})
}
}
func BenchmarkFib10(b *testing.B) {
for i := 0; i < b.N; i++ {
Fib(10)
}
}
8. Implement a Simple In-Memory Cache with TTL
Prompt: "Write a Go package for an in-memory cache with TTL (time-to-live) support. The cache should be safe for concurrent use (sync.RWMutex). Implement Set(key string, value interface{}, ttl time.Duration), Get(key string) (interface{}, bool), and Delete(key string). Automatically evict expired items in a background goroutine every minute. Provide a NewCache() constructor."
Why it works: Caching is everywhere in microservices. This prompt produces a production-ready cache that handles concurrency, expiration, and cleanup — all without external dependencies.
Example output:
package cache
import (
"sync"
"time"
)
type item struct {
value interface{}
expiration time.Time
}
type Cache struct {
items map[string]item
mu sync.RWMutex
}
func NewCache() *Cache {
c := &Cache{items: make(map[string]item)}
go c.cleanup()
return c
}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = item{
value: value,
expiration: time.Now().Add(ttl),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.items[key]
if !ok {
return nil, false
}
if time.Now().After(item.expiration) {
return nil, false
}
return item.value, true
}
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func (c *Cache) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
c.mu.Lock()
for k, v := range c.items {
if time.Now().After(v.expiration) {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}
9. Build a Health Check Endpoint for a Service
Prompt: "Create a Go HTTP handler for a health check endpoint (/healthz) that checks: (1) database connectivity (ping a PostgreSQL database), (2) free disk space (use syscall.Statfs on Linux), (3) last successful background job timestamp. Return JSON with status 'ok' or 'degraded', and details for each check. Use a configurable timeout (default 2 seconds)."
Why it works: Health checks are mandatory for container orchestration (Kubernetes liveness/readiness probes). This prompt produces a composite health check that covers common failure modes: database down, disk full, or stale jobs.
Example output:
package health
import (
"database/sql"
"encoding/json"
"net/http"
"syscall"
"time"
)
type CheckResult struct {
Name string `json:"name"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
}
type HealthResponse struct {
Status string `json:"status"`
Checks []CheckResult `json:"checks"`
}
func HealthHandler(db *sql.DB, lastJobTime time.Time) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
results := []CheckResult{}
overall := "ok"
// Database check
if err := db.PingContext(r.Context()); err != nil {
results = append(results, CheckResult{Name: "database", Status: "degraded", Error: err.Error()})
overall = "degraded"
} else {
results = append(results, CheckResult{Name: "database", Status: "ok"})
}
// Disk space check
var stat syscall.Statfs_t
err := syscall.Statfs("/", &stat)
freeBytes := stat.Bavail * uint64(stat.Bsize)
freeGB := float64(freeBytes) / 1e9
if freeGB < 1.0 {
results = append(results, CheckResult{Name: "disk", Status: "degraded", Error: "less than 1GB free"})
overall = "degraded"
} else {
results = append(results, CheckResult{Name: "disk", Status: "ok"})
}
// Last job check
if time.Since(lastJobTime) > 30*time.Minute {
results = append(results, CheckResult{Name: "last_job", Status: "degraded", Error: "no recent job"})
overall = "degraded"
} else {
results = append(results, CheckResult{Name: "last_job", Status: "ok"})
}
resp := HealthResponse{Status: overall, Checks: results}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
}
10. Write a Config Loader with Environment Variables and JSON
Prompt: "Write a Go package that loads configuration from a JSON file and overrides values with environment variables. The config struct should have fields: Port (int), DatabaseURL (string), LogLevel (string), and AllowedOrigins ([]string). Use the envconfig or caarlos0/env library for env var binding. Support default values. The JSON file path should come from a CONFIG_PATH env var (default './config.json')."
Why it works: Twelve-factor app config management is essential for cloud-native services. This prompt produces a reusable config loader that works both locally (JSON file) and in production (environment variables).
Example output:
package config
import (
"encoding/json"
"os"
"github.com/caarlos0/env/v11"
)
type Config struct {
Port int `json:"port" env:"PORT" envDefault:"8080"`
DatabaseURL string `json:"database_url" env:"DATABASE_URL" envDefault:"postgres://localhost:5432/mydb"`
LogLevel string `json:"log_level" env:"LOG_LEVEL" envDefault:"info"`
AllowedOrigins []string `json:"allowed_origins" env:"ALLOWED_ORIGINS" envSeparator:","`
}
func Load() (*Config, error) {
cfg := &Config{}
// Load from JSON file
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "./config.json"
}
if data, err := os.ReadFile(configPath); err == nil {
if err := json.Unmarshal(data, cfg); err != nil {
return nil, err
}
}
// Override with env vars
if err := env.Parse(cfg); err != nil {
return nil, err
}
return cfg, nil
}
Conclusion
These 10 prompts cover the most common tasks in Go development: building APIs, CLI tools, concurrent workers, middleware, database migrations, testing, caching, health checks, and configuration management. Each prompt is designed to produce production-ready code that follows Go best practices. The key is to be specific — include framework names, error handling requirements, and real-world constraints like timeouts and concurrency safety.
Start by copying one of these prompts into your favorite AI assistant, tweak the parameters (like port numbers or package names), and watch your boilerplate disappear. The more you use these prompts, the better you'll get at crafting them. And remember: always review AI-generated code for security and correctness — it's a co-pilot, not an autopilot.
Now go build something awesome with Go!
Comments