writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
goUse this skill when the user asks about Go build tools, module management, goroutines, channels, interfaces, error handling, testing, or production deployment. Focus on tooling, concurrency, and ecosystem — not syntax.
Independent GitHub snapshot · Automated classification · Original documentation governs
Guide for Go build tools, module management, concurrency patterns, interface design, error handling, testing, and production deployment.
Keywords: go build, go mod, goroutine, channel, interface go, error handling go, testing go, go generate, go tool, context, go concurrency.
What kind of project?
├── CLI tool → Single main.go + internal packages, cmd/ layout
├── Web API / microservice → cmd/ (entrypoints) + internal/ (private) + pkg/ (shared)
├── Library → Exported API surface minimal, no internal, flat or by feature
├── Monorepo → go.work file, multiple modules, shared tooling
└── gRPC service → protobuf codegen in separate package, buf tool
Workload type?
├── I/O bound (HTTP, DB, file) → Goroutines + channels / errgroup
├── CPU bound (compute, transform) → goroutines capped at runtime.NumCPU()
├── Fan-out / fan-in → worker pool with channel, sync.WaitGroup
├── Pipeline (producer → transform → consumer) → channels for each stage
├── Timeout / cancellation → context.Context with deadline/timeout
└── Once-only init → sync.Once for thread-safe lazy init
Error strategy?
├── Expected errors (validation, not found) → return error type, callers handle
├── Unexpected errors (network, IO) → wrap with fmt.Errorf("%w: %v", sentinel, err)
├── Retryable errors → errors.Is + retry with backoff
├── Panic → only for programmer errors (nil deref, bounds); recover in goroutine top
└── Deferred cleanup → defer mu.Unlock(), defer file.Close(), defer db.Rollback()
go mod init github.com/org/myproject
go mod tidy # Clean dependencies
go mod verify # Verify checksums
go mod download # Pre-download for Docker
go work init ./cmd ./pkg # Monorepo workspace
module github.com/org/myproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
go.uber.org/zap v1.27.0
golang.org/x/sync v0.7.0
)
// replace directive for local development
replace github.com/org/internal => ../internal
go build -o bin/server ./cmd/server
go build -ldflags="-X main.Version=v1.0.0" ./cmd/server
go test -race -count=1 ./...
go vet ./...
go generate ./...
// Define small interfaces (1-3 methods)
type Storer interface {
Store(ctx context.Context, key string, value []byte) error
Load(ctx context.Context, key string) ([]byte, error)
}
// Implement with concrete struct
type S3Storer struct {
bucket string
client *s3.Client
}
// Accept interface, return struct
func NewHandler(s Storer) *Handler {
return &Handler{store: s}
}
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func WorkerPool(ctx context.Context, jobs <-chan Job, results chan<- Result, workers int) {
var wg sync.WaitGroup
for range workers {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
select {
case <-ctx.Done():
return
case results <- process(job):
}
}
}()
}
wg.Wait()
close(results)
}
func main() {
srv := &http.Server{Addr: ":8080", Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
import "errors"
// Sentinel errors
var ErrNotFound = errors.New("not found")
// Wrap with context
func GetUser(id int) (*User, error) {
user, err := db.QueryUser(id)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("get user %d: %w", id, ErrNotFound)
}
return user, fmt.Errorf("get user %d: %w", id, err)
}
// Inspect
if errors.Is(err, ErrNotFound) {
http.NotFound(w, r)
}
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHandler(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want int
errMsg string
}{
{name: "valid input", input: "hello", want: 5, errMsg: ""},
{name: "empty string", input: "", want: 0, errMsg: "cannot be empty"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Handler(tt.input)
if tt.errMsg != "" {
assert.ErrorContains(t, err, tt.errMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func BenchmarkParse(b *testing.B) {
input := []byte(`{"name": "test", "value": 42}`)
for b.Loop() { // Go 1.24+; use b.N for older
var v Value
json.Unmarshal(input, &v)
}
}
// go test -bench=. -benchmem -count=5
func FuzzParse(f *testing.F) {
f.Add([]byte(`{"name": "test"}`))
f.Fuzz(func(t *testing.T, data []byte) {
var v Value
json.Unmarshal(data, &v)
// No panic expected
})
}
# Linting
golangci-lint run --fast # Parallel linters
staticcheck ./... # Go-specific static analysis
# Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # HTML report
# Race detection
go test -race ./... # Always in CI
# Profiling
pprof for CPU/memory
trace for goroutine scheduling
ctx context.Context in struct fieldif err != nil is a check, but nested = unreadable. Return earlyrecover() outside deferred function: Only works in deferred call. Always defer func() { recover() }()go run in production: Compile binary with go build, deploy binary. Never run from source-race in CI: Data races undetected until production. Always run with race detectorany / interface{} everywhere: Defeats type safety. Use generics (Go 1.18+) or specific interfacessync.Pool for frequently allocated temporary objectsmake([]T, 0, capacity) to avoid reallocationencoding/json vs jsoniter — stdlib is fast enough for most usespprof CPU, trace for scheduling, pprof heap for memorystrings.Builder over bytes.Buffer for string concatenationruntime.NumCPU() capped goroutines for CPU-bound workatomic operations over mutex for simple counters/flagsGo's net/http handler signature func(w http.ResponseWriter, r *http.Request) enables composable middleware via the decorator pattern. Middleware chain: recover → logging → auth → requestID → rateLimit → handler. Each middleware wraps the next and calls next.ServeHTTP(w, r). Use httpsnoop to capture response status and duration for logging. Common middleware: (a) request logging with duration and status, (b) panic recovery that logs stack trace and returns 500, (c) CORS headers for browser apps, (d) request ID injection via context, (e) authentication (JWT validation, API key check), (f) rate limiting (token bucket or sliding window), (g) gzip compression for responses. Use alice or chi for middleware chaining, or just compose manually. Always recover panics in HTTP handlers to avoid crashing the server.
Go convention is manual dependency injection in main(), not frameworks. Pattern: main() creates all dependencies (config, DB connection, logger, service), injects them into handlers, starts the server. For large projects, use Google Wire for codegen-based DI: define providers and injectors, wire generate produces the wire_gen.go file. Wire catches missing dependencies at codegen time (not runtime). Manual DI is simpler for <10 dependencies. Anti-pattern: global var registry or init() for wiring — makes testing impossible. Example: handler := NewUserHandler(logger, userService, authService).
Go databases need explicit migration management. Tools: golang-migrate/migrate (CLI + library), pressly/goose (Go-based), bytebase/bytebase (GUI). Migration files: 000001_create_users.up.sql and 000001_create_users.down.sql. Run in CI/CD as a separate step before deploying new binary. Safety: (a) write both up and down migrations, (b) always add columns (don't remove or rename in same deployment), (c) use NOT NULL with defaults to avoid null-in-new-column issues, (d) test down migrations work before production. For zero-downtime: migrate schema first, deploy new binary second. Use WithInstance for programmatic migration in main(): migrate.Up().
Use slog (Go 1.21+ standard library) or zap for structured logging with levels, fields, and sampling. Pattern: create a logger with service name and version, add request-scoped fields (requestID, userID) via context, log errors with stack traces. Log levels: Debug (development), Info (production operations), Warn (unexpected but handled), Error (failures requiring investigation). Never log PII or secrets. Sampling in high-throughput services: log first N errors per interval, then every Nth. Slog example: slog.Info("user created", "user_id", id, "duration_ms", elapsed). For zap: logger.Info("user created", zap.String("user_id", id), zap.Duration("elapsed", elapsed)).
Observability needs?
├── Simple logging → slog (stdlib, no external deps)
├── Metrics + logging → slog + prometheus client (otel/otel)
├── Full observability (traces, metrics, logs) → OpenTelemetry Go SDK
│ Exporter: OTLP to collector → Tempo (traces), Mimir (metrics), Loki (logs)
└── Error tracking only → Sentry Go SDK
Deployment target?
├── Docker compose / bare metal → Binary on host + systemd
│ Build: CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/server
├── Kubernetes → Distroless Docker image
│ Health: /healthz and /readyz endpoints
│ Graceful shutdown with signal handling
├── Serverless → AWS Lambda (aws-lambda-go) / Cloud Functions
│ Stateless handlers, no long-running goroutines
└── Edge → WASM target or tinygo for WebAssembly
Module organization for monorepo?
├── Single module → Simple, works for <100k LOC
├── Multi-module with go.work → Shared libraries, separate binaries
│ go.work: use for local development only (not committed to CI)
├── Multi-module without go.work → Each module independent
│ CI builds each module separately
└── Replace directives → For local development only
Never commit replace directives to main branch
package main
import (
"log/slog"
"net/http"
"time"
)
type wrappedWriter struct {
http.ResponseWriter
statusCode int
}
func (w *wrappedWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &wrappedWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.statusCode,
"duration_ms", time.Since(start).Milliseconds(),
)
})
}
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
slog.Error("panic recovered", "error", rec)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
handler := recoverMiddleware(loggingMiddleware(mux))
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
slog.Info("server starting", "addr", server.Addr)
if err := server.ListenAndServe(); err != nil {
slog.Error("server error", "error", err)
}
}
// Pipeline: generator -> stage1 -> stage2 -> consumer
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
c := gen(2, 3, 4)
out := sq(c)
for n := range out {
fmt.Println(n) // 4, 9, 16
}
}
func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
want time.Duration
err bool
}{
{name: "seconds", input: "30s", want: 30 * time.Second},
{name: "minutes", input: "5m", want: 5 * time.Minute},
{name: "invalid", input: "xyz", err: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseDuration(tt.input)
if tt.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
references/context-cancellation.md — Context propagation, timeout, cancellation patternsreferences/testing-benchmarking.md — Testing, benchmarking, fuzzing, coveragereferences/go-fundamentals.md — Go Fundamentalsreferences/go-advanced.md — Advanced Go Patternsreferences/go-http-server.md — Go HTTP Server Patternspackage client
import (
"context"
"fmt"
"math/rand"
"net/http"
"time"
)
type CircuitBreaker struct {
failures int
threshold int
resetAfter time.Duration
lastFailure time.Time
halfOpen bool
}
func (cb *CircuitBreaker) Call(ctx context.Context, fn func(context.Context) error) error {
if cb.failures >= cb.threshold && time.Since(cb.lastFailure) < cb.resetAfter {
return fmt.Errorf("circuit open")
}
err := fn(ctx)
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
return err
}
cb.failures = 0
return nil
}
func RetryBackoff(ctx context.Context, maxRetries int, fn func(context.Context) error) error {
for i := range maxRetries {
err := fn(ctx)
if err == nil {
return nil
}
if i == maxRetries-1 {
return err
}
delay := time.Duration(100*(1<<i)+rand.Intn(100)) * time.Millisecond
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
return nil
}
package middleware
import (
"context"
"log/slog"
"net/http"
"time"
)
type ctxKey string
const RequestIDKey ctxKey = "request_id"
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = fmt.Sprintf("%d", time.Now().UnixNano())
}
ctx := context.WithValue(r.Context(), RequestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(wrapped, r)
slog.InfoContext(r.Context(), "request",
"method", r.Method, "path", r.URL.Path,
"status", wrapped.status, "duration", time.Since(start).Milliseconds(),
)
})
}
http.Server.Shutdown() with context.WithTimeout(ctx, 30s) — wait for in-flight requests.sync.WaitGroup.Wait() → close output channel.sql.DB.Close() blocks until pool drained. Call in shutdown defer.slog.HandlerOptions{Level: slog.LevelInfo} in prod, LevelDebug in dev.promhttp.Handler() at /metrics. Histogram for request duration, counter for errors.| Anti-Pattern | Why It Hurts | Fix |
|---|---|---|
context.Background() in library code | Never cancelled. Leaks goroutines on caller shutdown. | Accept context.Context as first param. Propagate. |
init() for registration | Implicit side effect. Can't test registration. | Register in main() or use go:generate. |
Embedding sync.Mutex publicly | Exposes Lock/Unlock. Breaks encapsulation. | type safeMap struct { mu sync.Mutex; m map[string]T } |
io.ReadAll without limit | OOM on large response. | io.LimitReader(r, maxSize) |
defer db.Close() in function | DB connection stays open until function returns. | Return DB connection to pool explicitly. |
Writing to http.ResponseWriter after handler return | Panic. | Check wroteHeader flag. |
sync.Pool for temporary structs in high-throughput handlers. Reduces GC pressure by 30-50%.make([]T, 0, expectedCap) avoids multiple reallocations.strings.Builder for many concatenations. Single + for 2-3 strings.encoding/json vs jsoniter: stdlib is fine for most. Use jsoniter only under profiling evidence.runtime.GOMAXPROCS(N) set to CPU quota in containerized environments (prevents over-subscription).sync.Pool for buffer reuse in HTTP response parsing.runtime.NumCPU() * 2.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id).html/template auto-escapes. React/Next.js also handle. Raw text/template does NOT.gorilla/csrf or nosurf. Token in cookie + form header.golang-jwt/jwt/v5. Verify alg, exp, iss. Reject alg: none.golang.org/x/time/rate per-IP limiter. Per-user in authenticated routes.rs/cors with explicit origins. Never * for authenticated endpoints.http.Server{TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}}.govulncheck ./... in CI. Block on critical vulnerabilities.