diff --git a/README.md b/README.md index 0a45969..7f8743b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) ![Shell](https://img.shields.io/badge/-Shell-4EAA25?logo=gnu-bash&logoColor=white) ![TypeScript](https://img.shields.io/badge/-TypeScript-3178C6?logo=typescript&logoColor=white) +![Go](https://img.shields.io/badge/-Go-00ADD8?logo=go&logoColor=white) ![Markdown](https://img.shields.io/badge/-Markdown-000000?logo=markdown&logoColor=white) **The complete collection of Claude Code configs from an Anthropic hackathon winner.** @@ -101,6 +102,8 @@ everything-claude-code/ | |-- e2e-runner.md # Playwright E2E testing | |-- refactor-cleaner.md # Dead code cleanup | |-- doc-updater.md # Documentation sync +| |-- go-reviewer.md # Go code review (NEW) +| |-- go-build-resolver.md # Go build error resolution (NEW) | |-- skills/ # Workflow definitions and domain knowledge | |-- coding-standards/ # Language best practices @@ -114,6 +117,8 @@ everything-claude-code/ | |-- security-review/ # Security checklist | |-- eval-harness/ # Verification loop evaluation (Longform Guide) | |-- verification-loop/ # Continuous verification (Longform Guide) +| |-- golang-patterns/ # Go idioms and best practices (NEW) +| |-- golang-testing/ # Go testing patterns, TDD, benchmarks (NEW) | |-- commands/ # Slash commands for quick execution | |-- tdd.md # /tdd - Test-driven development @@ -125,7 +130,10 @@ everything-claude-code/ | |-- learn.md # /learn - Extract patterns mid-session (Longform Guide) | |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide) | |-- verify.md # /verify - Run verification loop (Longform Guide) -| |-- setup-pm.md # /setup-pm - Configure package manager (NEW) +| |-- setup-pm.md # /setup-pm - Configure package manager +| |-- go-review.md # /go-review - Go code review (NEW) +| |-- go-test.md # /go-test - Go TDD workflow (NEW) +| |-- go-build.md # /go-build - Fix Go build errors (NEW) | |-- rules/ # Always-follow guidelines (copy to ~/.claude/rules/) | |-- security.md # Mandatory security checks @@ -353,7 +361,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. ### Ideas for Contributions -- Language-specific skills (Python, Go, Rust patterns) +- Language-specific skills (Python, Rust patterns) - Go now included! - Framework-specific configs (Django, Rails, Laravel) - DevOps agents (Kubernetes, Terraform, AWS) - Testing strategies (different frameworks) diff --git a/agents/go-build-resolver.md b/agents/go-build-resolver.md new file mode 100644 index 0000000..0825fbb --- /dev/null +++ b/agents/go-build-resolver.md @@ -0,0 +1,368 @@ +--- +name: go-build-resolver +description: Go build, vet, and compilation error resolution specialist. Fixes build errors, go vet issues, and linter warnings with minimal changes. Use when Go builds fail. +tools: ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +model: opus +--- + +# Go Build Error Resolver + +You are an expert Go build error resolution specialist. Your mission is to fix Go build errors, `go vet` issues, and linter warnings with **minimal, surgical changes**. + +## Core Responsibilities + +1. Diagnose Go compilation errors +2. Fix `go vet` warnings +3. Resolve `staticcheck` / `golangci-lint` issues +4. Handle module dependency problems +5. Fix type errors and interface mismatches + +## Diagnostic Commands + +Run these in order to understand the problem: + +```bash +# 1. Basic build check +go build ./... + +# 2. Vet for common mistakes +go vet ./... + +# 3. Static analysis (if available) +staticcheck ./... 2>/dev/null || echo "staticcheck not installed" +golangci-lint run 2>/dev/null || echo "golangci-lint not installed" + +# 4. Module verification +go mod verify +go mod tidy -v + +# 5. List dependencies +go list -m all +``` + +## Common Error Patterns & Fixes + +### 1. Undefined Identifier + +**Error:** `undefined: SomeFunc` + +**Causes:** +- Missing import +- Typo in function/variable name +- Unexported identifier (lowercase first letter) +- Function defined in different file with build constraints + +**Fix:** +```go +// Add missing import +import "package/that/defines/SomeFunc" + +// Or fix typo +// somefunc -> SomeFunc + +// Or export the identifier +// func someFunc() -> func SomeFunc() +``` + +### 2. Type Mismatch + +**Error:** `cannot use x (type A) as type B` + +**Causes:** +- Wrong type conversion +- Interface not satisfied +- Pointer vs value mismatch + +**Fix:** +```go +// Type conversion +var x int = 42 +var y int64 = int64(x) + +// Pointer to value +var ptr *int = &x +var val int = *ptr + +// Value to pointer +var val int = 42 +var ptr *int = &val +``` + +### 3. Interface Not Satisfied + +**Error:** `X does not implement Y (missing method Z)` + +**Diagnosis:** +```bash +# Find what methods are missing +go doc package.Interface +``` + +**Fix:** +```go +// Implement missing method with correct signature +func (x *X) Z() error { + // implementation + return nil +} + +// Check receiver type matches (pointer vs value) +// If interface expects: func (x X) Method() +// You wrote: func (x *X) Method() // Won't satisfy +``` + +### 4. Import Cycle + +**Error:** `import cycle not allowed` + +**Diagnosis:** +```bash +go list -f '{{.ImportPath}} -> {{.Imports}}' ./... +``` + +**Fix:** +- Move shared types to a separate package +- Use interfaces to break the cycle +- Restructure package dependencies + +``` +# Before (cycle) +package/a -> package/b -> package/a + +# After (fixed) +package/types <- shared types +package/a -> package/types +package/b -> package/types +``` + +### 5. Cannot Find Package + +**Error:** `cannot find package "x"` + +**Fix:** +```bash +# Add dependency +go get package/path@version + +# Or update go.mod +go mod tidy + +# Or for local packages, check go.mod module path +# Module: github.com/user/project +# Import: github.com/user/project/internal/pkg +``` + +### 6. Missing Return + +**Error:** `missing return at end of function` + +**Fix:** +```go +func Process() (int, error) { + if condition { + return 0, errors.New("error") + } + return 42, nil // Add missing return +} +``` + +### 7. Unused Variable/Import + +**Error:** `x declared but not used` or `imported and not used` + +**Fix:** +```go +// Remove unused variable +x := getValue() // Remove if x not used + +// Use blank identifier if intentionally ignoring +_ = getValue() + +// Remove unused import or use blank import for side effects +import _ "package/for/init/only" +``` + +### 8. Multiple-Value in Single-Value Context + +**Error:** `multiple-value X() in single-value context` + +**Fix:** +```go +// Wrong +result := funcReturningTwo() + +// Correct +result, err := funcReturningTwo() +if err != nil { + return err +} + +// Or ignore second value +result, _ := funcReturningTwo() +``` + +### 9. Cannot Assign to Field + +**Error:** `cannot assign to struct field x.y in map` + +**Fix:** +```go +// Cannot modify struct in map directly +m := map[string]MyStruct{} +m["key"].Field = "value" // Error! + +// Fix: Use pointer map or copy-modify-reassign +m := map[string]*MyStruct{} +m["key"] = &MyStruct{} +m["key"].Field = "value" // Works + +// Or +m := map[string]MyStruct{} +tmp := m["key"] +tmp.Field = "value" +m["key"] = tmp +``` + +### 10. Invalid Operation (Type Assertion) + +**Error:** `invalid type assertion: x.(T) (non-interface type)` + +**Fix:** +```go +// Can only assert from interface +var i interface{} = "hello" +s := i.(string) // Valid + +var s string = "hello" +// s.(int) // Invalid - s is not interface +``` + +## Module Issues + +### Replace Directive Problems + +```bash +# Check for local replaces that might be invalid +grep "replace" go.mod + +# Remove stale replaces +go mod edit -dropreplace=package/path +``` + +### Version Conflicts + +```bash +# See why a version is selected +go mod why -m package + +# Get specific version +go get package@v1.2.3 + +# Update all dependencies +go get -u ./... +``` + +### Checksum Mismatch + +```bash +# Clear module cache +go clean -modcache + +# Re-download +go mod download +``` + +## Go Vet Issues + +### Suspicious Constructs + +```go +// Vet: unreachable code +func example() int { + return 1 + fmt.Println("never runs") // Remove this +} + +// Vet: printf format mismatch +fmt.Printf("%d", "string") // Fix: %s + +// Vet: copying lock value +var mu sync.Mutex +mu2 := mu // Fix: use pointer *sync.Mutex + +// Vet: self-assignment +x = x // Remove pointless assignment +``` + +## Fix Strategy + +1. **Read the full error message** - Go errors are descriptive +2. **Identify the file and line number** - Go directly to the source +3. **Understand the context** - Read surrounding code +4. **Make minimal fix** - Don't refactor, just fix the error +5. **Verify fix** - Run `go build ./...` again +6. **Check for cascading errors** - One fix might reveal others + +## Resolution Workflow + +``` +1. go build ./... + ↓ Error? +2. Parse error message + ↓ +3. Read affected file + ↓ +4. Apply minimal fix + ↓ +5. go build ./... + ↓ Still errors? + → Back to step 2 + ↓ Success? +6. go vet ./... + ↓ Warnings? + → Fix and repeat + ↓ +7. go test ./... + ↓ +8. Done! +``` + +## Stop Conditions + +Stop and report if: +- Same error persists after 3 fix attempts +- Fix introduces more errors than it resolves +- Error requires architectural changes beyond scope +- Circular dependency that needs package restructuring +- Missing external dependency that needs manual installation + +## Output Format + +After each fix attempt: + +``` +[FIXED] internal/handler/user.go:42 +Error: undefined: UserService +Fix: Added import "project/internal/service" + +Remaining errors: 3 +``` + +Final summary: +``` +Build Status: SUCCESS/FAILED +Errors Fixed: N +Vet Warnings Fixed: N +Files Modified: list +Remaining Issues: list (if any) +``` + +## Important Notes + +- **Never** add `//nolint` comments without explicit approval +- **Never** change function signatures unless necessary for the fix +- **Always** run `go mod tidy` after adding/removing imports +- **Prefer** fixing root cause over suppressing symptoms +- **Document** any non-obvious fixes with inline comments + +Build errors should be fixed surgically. The goal is a working build, not a refactored codebase. diff --git a/agents/go-reviewer.md b/agents/go-reviewer.md new file mode 100644 index 0000000..c498d1a --- /dev/null +++ b/agents/go-reviewer.md @@ -0,0 +1,267 @@ +--- +name: go-reviewer +description: Expert Go code reviewer specializing in idiomatic Go, concurrency patterns, error handling, and performance. Use for all Go code changes. MUST BE USED for Go projects. +tools: ["Read", "Grep", "Glob", "Bash"] +model: opus +--- + +You are a senior Go code reviewer ensuring high standards of idiomatic Go and best practices. + +When invoked: +1. Run `git diff -- '*.go'` to see recent Go file changes +2. Run `go vet ./...` and `staticcheck ./...` if available +3. Focus on modified `.go` files +4. Begin review immediately + +## Security Checks (CRITICAL) + +- **SQL Injection**: String concatenation in `database/sql` queries + ```go + // Bad + db.Query("SELECT * FROM users WHERE id = " + userID) + // Good + db.Query("SELECT * FROM users WHERE id = $1", userID) + ``` + +- **Command Injection**: Unvalidated input in `os/exec` + ```go + // Bad + exec.Command("sh", "-c", "echo " + userInput) + // Good + exec.Command("echo", userInput) + ``` + +- **Path Traversal**: User-controlled file paths + ```go + // Bad + os.ReadFile(filepath.Join(baseDir, userPath)) + // Good + cleanPath := filepath.Clean(userPath) + if strings.HasPrefix(cleanPath, "..") { + return ErrInvalidPath + } + ``` + +- **Race Conditions**: Shared state without synchronization +- **Unsafe Package**: Use of `unsafe` without justification +- **Hardcoded Secrets**: API keys, passwords in source +- **Insecure TLS**: `InsecureSkipVerify: true` +- **Weak Crypto**: Use of MD5/SHA1 for security purposes + +## Error Handling (CRITICAL) + +- **Ignored Errors**: Using `_` to ignore errors + ```go + // Bad + result, _ := doSomething() + // Good + result, err := doSomething() + if err != nil { + return fmt.Errorf("do something: %w", err) + } + ``` + +- **Missing Error Wrapping**: Errors without context + ```go + // Bad + return err + // Good + return fmt.Errorf("load config %s: %w", path, err) + ``` + +- **Panic Instead of Error**: Using panic for recoverable errors +- **errors.Is/As**: Not using for error checking + ```go + // Bad + if err == sql.ErrNoRows + // Good + if errors.Is(err, sql.ErrNoRows) + ``` + +## Concurrency (HIGH) + +- **Goroutine Leaks**: Goroutines that never terminate + ```go + // Bad: No way to stop goroutine + go func() { + for { doWork() } + }() + // Good: Context for cancellation + go func() { + for { + select { + case <-ctx.Done(): + return + default: + doWork() + } + } + }() + ``` + +- **Race Conditions**: Run `go build -race ./...` +- **Unbuffered Channel Deadlock**: Sending without receiver +- **Missing sync.WaitGroup**: Goroutines without coordination +- **Context Not Propagated**: Ignoring context in nested calls +- **Mutex Misuse**: Not using `defer mu.Unlock()` + ```go + // Bad: Unlock might not be called on panic + mu.Lock() + doSomething() + mu.Unlock() + // Good + mu.Lock() + defer mu.Unlock() + doSomething() + ``` + +## Code Quality (HIGH) + +- **Large Functions**: Functions over 50 lines +- **Deep Nesting**: More than 4 levels of indentation +- **Interface Pollution**: Defining interfaces not used for abstraction +- **Package-Level Variables**: Mutable global state +- **Naked Returns**: In functions longer than a few lines + ```go + // Bad in long functions + func process() (result int, err error) { + // ... 30 lines ... + return // What's being returned? + } + ``` + +- **Non-Idiomatic Code**: + ```go + // Bad + if err != nil { + return err + } else { + doSomething() + } + // Good: Early return + if err != nil { + return err + } + doSomething() + ``` + +## Performance (MEDIUM) + +- **Inefficient String Building**: + ```go + // Bad + for _, s := range parts { result += s } + // Good + var sb strings.Builder + for _, s := range parts { sb.WriteString(s) } + ``` + +- **Slice Pre-allocation**: Not using `make([]T, 0, cap)` +- **Pointer vs Value Receivers**: Inconsistent usage +- **Unnecessary Allocations**: Creating objects in hot paths +- **N+1 Queries**: Database queries in loops +- **Missing Connection Pooling**: Creating new DB connections per request + +## Best Practices (MEDIUM) + +- **Accept Interfaces, Return Structs**: Functions should accept interface parameters +- **Context First**: Context should be first parameter + ```go + // Bad + func Process(id string, ctx context.Context) + // Good + func Process(ctx context.Context, id string) + ``` + +- **Table-Driven Tests**: Tests should use table-driven pattern +- **Godoc Comments**: Exported functions need documentation + ```go + // ProcessData transforms raw input into structured output. + // It returns an error if the input is malformed. + func ProcessData(input []byte) (*Data, error) + ``` + +- **Error Messages**: Should be lowercase, no punctuation + ```go + // Bad + return errors.New("Failed to process data.") + // Good + return errors.New("failed to process data") + ``` + +- **Package Naming**: Short, lowercase, no underscores + +## Go-Specific Anti-Patterns + +- **init() Abuse**: Complex logic in init functions +- **Empty Interface Overuse**: Using `interface{}` instead of generics +- **Type Assertions Without ok**: Can panic + ```go + // Bad + v := x.(string) + // Good + v, ok := x.(string) + if !ok { return ErrInvalidType } + ``` + +- **Deferred Call in Loop**: Resource accumulation + ```go + // Bad: Files opened until function returns + for _, path := range paths { + f, _ := os.Open(path) + defer f.Close() + } + // Good: Close in loop iteration + for _, path := range paths { + func() { + f, _ := os.Open(path) + defer f.Close() + process(f) + }() + } + ``` + +## Review Output Format + +For each issue: +``` +[CRITICAL] SQL Injection vulnerability +File: internal/repository/user.go:42 +Issue: User input directly concatenated into SQL query +Fix: Use parameterized query + +query := "SELECT * FROM users WHERE id = " + userID // Bad +query := "SELECT * FROM users WHERE id = $1" // Good +db.Query(query, userID) +``` + +## Diagnostic Commands + +Run these checks: +```bash +# Static analysis +go vet ./... +staticcheck ./... +golangci-lint run + +# Race detection +go build -race ./... +go test -race ./... + +# Security scanning +govulncheck ./... +``` + +## Approval Criteria + +- **Approve**: No CRITICAL or HIGH issues +- **Warning**: MEDIUM issues only (can merge with caution) +- **Block**: CRITICAL or HIGH issues found + +## Go Version Considerations + +- Check `go.mod` for minimum Go version +- Note if code uses features from newer Go versions (generics 1.18+, fuzzing 1.18+) +- Flag deprecated functions from standard library + +Review with the mindset: "Would this code pass review at Google or a top Go shop?" diff --git a/commands/go-build.md b/commands/go-build.md new file mode 100644 index 0000000..94be32e --- /dev/null +++ b/commands/go-build.md @@ -0,0 +1,183 @@ +--- +description: Fix Go build errors, go vet warnings, and linter issues incrementally. Invokes the go-build-resolver agent for minimal, surgical fixes. +--- + +# Go Build and Fix + +This command invokes the **go-build-resolver** agent to incrementally fix Go build errors with minimal changes. + +## What This Command Does + +1. **Run Diagnostics**: Execute `go build`, `go vet`, `staticcheck` +2. **Parse Errors**: Group by file and sort by severity +3. **Fix Incrementally**: One error at a time +4. **Verify Each Fix**: Re-run build after each change +5. **Report Summary**: Show what was fixed and what remains + +## When to Use + +Use `/go-build` when: +- `go build ./...` fails with errors +- `go vet ./...` reports issues +- `golangci-lint run` shows warnings +- Module dependencies are broken +- After pulling changes that break the build + +## Diagnostic Commands Run + +```bash +# Primary build check +go build ./... + +# Static analysis +go vet ./... + +# Extended linting (if available) +staticcheck ./... +golangci-lint run + +# Module issues +go mod verify +go mod tidy -v +``` + +## Example Session + +``` +User: /go-build + +Agent: +# Go Build Resolution + +## Initial Diagnostics + +```bash +$ go build ./... +internal/service/user.go:25:15: undefined: UserRepository +internal/handler/api.go:42:9: cannot use x (type string) as type int +internal/handler/api.go:58:2: missing return at end of function +``` + +Errors found: 3 + +## Fix 1: Undefined Identifier + +File: internal/service/user.go:25 +Error: undefined: UserRepository +Cause: Missing import + +```go +// Added import +import "project/internal/repository" + +// Changed +var repo UserRepository +// To +var repo repository.UserRepository +``` + +```bash +$ go build ./... +# 2 errors remaining +``` + +## Fix 2: Type Mismatch + +File: internal/handler/api.go:42 +Error: cannot use x (type string) as type int + +```go +// Changed +count := params.Get("count") +// To +countStr := params.Get("count") +count, _ := strconv.Atoi(countStr) +``` + +```bash +$ go build ./... +# 1 error remaining +``` + +## Fix 3: Missing Return + +File: internal/handler/api.go:58 +Error: missing return at end of function + +```go +func GetUser(id string) (*User, error) { + if id == "" { + return nil, ErrInvalidID + } + user := findUser(id) + // Added missing return + return user, nil +} +``` + +```bash +$ go build ./... +# Build successful! +``` + +## Final Verification + +```bash +$ go vet ./... +# No issues + +$ go test ./... +ok project/internal/service 0.015s +ok project/internal/handler 0.023s +``` + +## Summary + +| Metric | Count | +|--------|-------| +| Build errors fixed | 3 | +| Vet warnings fixed | 0 | +| Files modified | 2 | +| Remaining issues | 0 | + +Build Status: ✅ SUCCESS +``` + +## Common Errors Fixed + +| Error | Typical Fix | +|-------|-------------| +| `undefined: X` | Add import or fix typo | +| `cannot use X as Y` | Type conversion or fix assignment | +| `missing return` | Add return statement | +| `X does not implement Y` | Add missing method | +| `import cycle` | Restructure packages | +| `declared but not used` | Remove or use variable | +| `cannot find package` | `go get` or `go mod tidy` | + +## Fix Strategy + +1. **Build errors first** - Code must compile +2. **Vet warnings second** - Fix suspicious constructs +3. **Lint warnings third** - Style and best practices +4. **One fix at a time** - Verify each change +5. **Minimal changes** - Don't refactor, just fix + +## Stop Conditions + +The agent will stop and report if: +- Same error persists after 3 attempts +- Fix introduces more errors +- Requires architectural changes +- Missing external dependencies + +## Related Commands + +- `/go-test` - Run tests after build succeeds +- `/go-review` - Review code quality +- `/verify` - Full verification loop + +## Related + +- Agent: `agents/go-build-resolver.md` +- Skill: `skills/golang-patterns/` diff --git a/commands/go-review.md b/commands/go-review.md new file mode 100644 index 0000000..62bdce0 --- /dev/null +++ b/commands/go-review.md @@ -0,0 +1,148 @@ +--- +description: Comprehensive Go code review for idiomatic patterns, concurrency safety, error handling, and security. Invokes the go-reviewer agent. +--- + +# Go Code Review + +This command invokes the **go-reviewer** agent for comprehensive Go-specific code review. + +## What This Command Does + +1. **Identify Go Changes**: Find modified `.go` files via `git diff` +2. **Run Static Analysis**: Execute `go vet`, `staticcheck`, and `golangci-lint` +3. **Security Scan**: Check for SQL injection, command injection, race conditions +4. **Concurrency Review**: Analyze goroutine safety, channel usage, mutex patterns +5. **Idiomatic Go Check**: Verify code follows Go conventions and best practices +6. **Generate Report**: Categorize issues by severity + +## When to Use + +Use `/go-review` when: +- After writing or modifying Go code +- Before committing Go changes +- Reviewing pull requests with Go code +- Onboarding to a new Go codebase +- Learning idiomatic Go patterns + +## Review Categories + +### CRITICAL (Must Fix) +- SQL/Command injection vulnerabilities +- Race conditions without synchronization +- Goroutine leaks +- Hardcoded credentials +- Unsafe pointer usage +- Ignored errors in critical paths + +### HIGH (Should Fix) +- Missing error wrapping with context +- Panic instead of error returns +- Context not propagated +- Unbuffered channels causing deadlocks +- Interface not satisfied errors +- Missing mutex protection + +### MEDIUM (Consider) +- Non-idiomatic code patterns +- Missing godoc comments on exports +- Inefficient string concatenation +- Slice not preallocated +- Table-driven tests not used + +## Automated Checks Run + +```bash +# Static analysis +go vet ./... + +# Advanced checks (if installed) +staticcheck ./... +golangci-lint run + +# Race detection +go build -race ./... + +# Security vulnerabilities +govulncheck ./... +``` + +## Example Usage + +``` +User: /go-review + +Agent: +# Go Code Review Report + +## Files Reviewed +- internal/handler/user.go (modified) +- internal/service/auth.go (modified) + +## Static Analysis Results +✓ go vet: No issues +✓ staticcheck: No issues + +## Issues Found + +[CRITICAL] Race Condition +File: internal/service/auth.go:45 +Issue: Shared map accessed without synchronization +```go +var cache = map[string]*Session{} // Concurrent access! + +func GetSession(id string) *Session { + return cache[id] // Race condition +} +``` +Fix: Use sync.RWMutex or sync.Map +```go +var ( + cache = map[string]*Session{} + cacheMu sync.RWMutex +) + +func GetSession(id string) *Session { + cacheMu.RLock() + defer cacheMu.RUnlock() + return cache[id] +} +``` + +[HIGH] Missing Error Context +File: internal/handler/user.go:28 +Issue: Error returned without context +```go +return err // No context +``` +Fix: Wrap with context +```go +return fmt.Errorf("get user %s: %w", userID, err) +``` + +## Summary +- CRITICAL: 1 +- HIGH: 1 +- MEDIUM: 0 + +Recommendation: ❌ Block merge until CRITICAL issue is fixed +``` + +## Approval Criteria + +| Status | Condition | +|--------|-----------| +| ✅ Approve | No CRITICAL or HIGH issues | +| ⚠️ Warning | Only MEDIUM issues (merge with caution) | +| ❌ Block | CRITICAL or HIGH issues found | + +## Integration with Other Commands + +- Use `/go-test` first to ensure tests pass +- Use `/go-build` if build errors occur +- Use `/go-review` before committing +- Use `/code-review` for non-Go specific concerns + +## Related + +- Agent: `agents/go-reviewer.md` +- Skills: `skills/golang-patterns/`, `skills/golang-testing/` diff --git a/commands/go-test.md b/commands/go-test.md new file mode 100644 index 0000000..779062a --- /dev/null +++ b/commands/go-test.md @@ -0,0 +1,268 @@ +--- +description: Enforce TDD workflow for Go. Write table-driven tests first, then implement. Verify 80%+ coverage with go test -cover. +--- + +# Go TDD Command + +This command enforces test-driven development methodology for Go code using idiomatic Go testing patterns. + +## What This Command Does + +1. **Define Types/Interfaces**: Scaffold function signatures first +2. **Write Table-Driven Tests**: Create comprehensive test cases (RED) +3. **Run Tests**: Verify tests fail for the right reason +4. **Implement Code**: Write minimal code to pass (GREEN) +5. **Refactor**: Improve while keeping tests green +6. **Check Coverage**: Ensure 80%+ coverage + +## When to Use + +Use `/go-test` when: +- Implementing new Go functions +- Adding test coverage to existing code +- Fixing bugs (write failing test first) +- Building critical business logic +- Learning TDD workflow in Go + +## TDD Cycle + +``` +RED → Write failing table-driven test +GREEN → Implement minimal code to pass +REFACTOR → Improve code, tests stay green +REPEAT → Next test case +``` + +## Example Session + +``` +User: /go-test I need a function to validate email addresses + +Agent: +# TDD Session: Email Validator + +## Step 1: Define Interface + +```go +// validator/email.go +package validator + +// ValidateEmail checks if the given string is a valid email address. +// Returns nil if valid, error describing the issue otherwise. +func ValidateEmail(email string) error { + panic("not implemented") +} +``` + +## Step 2: Write Table-Driven Tests (RED) + +```go +// validator/email_test.go +package validator + +import ( + "testing" +) + +func TestValidateEmail(t *testing.T) { + tests := []struct { + name string + email string + wantErr bool + }{ + // Valid emails + {"simple email", "user@example.com", false}, + {"with subdomain", "user@mail.example.com", false}, + {"with plus", "user+tag@example.com", false}, + {"with dots", "first.last@example.com", false}, + + // Invalid emails + {"empty string", "", true}, + {"no at sign", "userexample.com", true}, + {"no domain", "user@", true}, + {"no local part", "@example.com", true}, + {"double at", "user@@example.com", true}, + {"spaces", "user @example.com", true}, + {"no tld", "user@example", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateEmail(tt.email) + if tt.wantErr && err == nil { + t.Errorf("ValidateEmail(%q) = nil; want error", tt.email) + } + if !tt.wantErr && err != nil { + t.Errorf("ValidateEmail(%q) = %v; want nil", tt.email, err) + } + }) + } +} +``` + +## Step 3: Run Tests - Verify FAIL + +```bash +$ go test ./validator/... + +--- FAIL: TestValidateEmail (0.00s) + --- FAIL: TestValidateEmail/simple_email (0.00s) + panic: not implemented + +FAIL +``` + +✓ Tests fail as expected (panic). + +## Step 4: Implement Minimal Code (GREEN) + +```go +// validator/email.go +package validator + +import ( + "errors" + "regexp" +) + +var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + +var ( + ErrEmailEmpty = errors.New("email cannot be empty") + ErrEmailInvalid = errors.New("email format is invalid") +) + +func ValidateEmail(email string) error { + if email == "" { + return ErrEmailEmpty + } + if !emailRegex.MatchString(email) { + return ErrEmailInvalid + } + return nil +} +``` + +## Step 5: Run Tests - Verify PASS + +```bash +$ go test ./validator/... + +PASS +ok project/validator 0.003s +``` + +✓ All tests passing! + +## Step 6: Check Coverage + +```bash +$ go test -cover ./validator/... + +PASS +coverage: 100.0% of statements +ok project/validator 0.003s +``` + +✓ Coverage: 100% + +## TDD Complete! +``` + +## Test Patterns + +### Table-Driven Tests +```go +tests := []struct { + name string + input InputType + want OutputType + wantErr bool +}{ + {"case 1", input1, want1, false}, + {"case 2", input2, want2, true}, +} + +for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Function(tt.input) + // assertions + }) +} +``` + +### Parallel Tests +```go +for _, tt := range tests { + tt := tt // Capture + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + // test body + }) +} +``` + +### Test Helpers +```go +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() + db := createDB() + t.Cleanup(func() { db.Close() }) + return db +} +``` + +## Coverage Commands + +```bash +# Basic coverage +go test -cover ./... + +# Coverage profile +go test -coverprofile=coverage.out ./... + +# View in browser +go tool cover -html=coverage.out + +# Coverage by function +go tool cover -func=coverage.out + +# With race detection +go test -race -cover ./... +``` + +## Coverage Targets + +| Code Type | Target | +|-----------|--------| +| Critical business logic | 100% | +| Public APIs | 90%+ | +| General code | 80%+ | +| Generated code | Exclude | + +## TDD Best Practices + +**DO:** +- Write test FIRST, before any implementation +- Run tests after each change +- Use table-driven tests for comprehensive coverage +- Test behavior, not implementation details +- Include edge cases (empty, nil, max values) + +**DON'T:** +- Write implementation before tests +- Skip the RED phase +- Test private functions directly +- Use `time.Sleep` in tests +- Ignore flaky tests + +## Related Commands + +- `/go-build` - Fix build errors +- `/go-review` - Review code after implementation +- `/verify` - Run full verification loop + +## Related + +- Skill: `skills/golang-testing/` +- Skill: `skills/tdd-workflow/` diff --git a/skills/golang-patterns/SKILL.md b/skills/golang-patterns/SKILL.md new file mode 100644 index 0000000..713c8eb --- /dev/null +++ b/skills/golang-patterns/SKILL.md @@ -0,0 +1,673 @@ +--- +name: golang-patterns +description: Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. +--- + +# Go Development Patterns + +Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications. + +## When to Activate + +- Writing new Go code +- Reviewing Go code +- Refactoring existing Go code +- Designing Go packages/modules + +## Core Principles + +### 1. Simplicity and Clarity + +Go favors simplicity over cleverness. Code should be obvious and easy to read. + +```go +// Good: Clear and direct +func GetUser(id string) (*User, error) { + user, err := db.FindUser(id) + if err != nil { + return nil, fmt.Errorf("get user %s: %w", id, err) + } + return user, nil +} + +// Bad: Overly clever +func GetUser(id string) (*User, error) { + return func() (*User, error) { + if u, e := db.FindUser(id); e == nil { + return u, nil + } else { + return nil, e + } + }() +} +``` + +### 2. Make the Zero Value Useful + +Design types so their zero value is immediately usable without initialization. + +```go +// Good: Zero value is useful +type Counter struct { + mu sync.Mutex + count int // zero value is 0, ready to use +} + +func (c *Counter) Inc() { + c.mu.Lock() + c.count++ + c.mu.Unlock() +} + +// Good: bytes.Buffer works with zero value +var buf bytes.Buffer +buf.WriteString("hello") + +// Bad: Requires initialization +type BadCounter struct { + counts map[string]int // nil map will panic +} +``` + +### 3. Accept Interfaces, Return Structs + +Functions should accept interface parameters and return concrete types. + +```go +// Good: Accepts interface, returns concrete type +func ProcessData(r io.Reader) (*Result, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return &Result{Data: data}, nil +} + +// Bad: Returns interface (hides implementation details unnecessarily) +func ProcessData(r io.Reader) (io.Reader, error) { + // ... +} +``` + +## Error Handling Patterns + +### Error Wrapping with Context + +```go +// Good: Wrap errors with context +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("load config %s: %w", path, err) + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config %s: %w", path, err) + } + + return &cfg, nil +} +``` + +### Custom Error Types + +```go +// Define domain-specific errors +type ValidationError struct { + Field string + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) +} + +// Sentinel errors for common cases +var ( + ErrNotFound = errors.New("resource not found") + ErrUnauthorized = errors.New("unauthorized") + ErrInvalidInput = errors.New("invalid input") +) +``` + +### Error Checking with errors.Is and errors.As + +```go +func HandleError(err error) { + // Check for specific error + if errors.Is(err, sql.ErrNoRows) { + log.Println("No records found") + return + } + + // Check for error type + var validationErr *ValidationError + if errors.As(err, &validationErr) { + log.Printf("Validation error on field %s: %s", + validationErr.Field, validationErr.Message) + return + } + + // Unknown error + log.Printf("Unexpected error: %v", err) +} +``` + +### Never Ignore Errors + +```go +// Bad: Ignoring error with blank identifier +result, _ := doSomething() + +// Good: Handle or explicitly document why it's safe to ignore +result, err := doSomething() +if err != nil { + return err +} + +// Acceptable: When error truly doesn't matter (rare) +_ = writer.Close() // Best-effort cleanup, error logged elsewhere +``` + +## Concurrency Patterns + +### Worker Pool + +```go +func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { + var wg sync.WaitGroup + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobs { + results <- process(job) + } + }() + } + + wg.Wait() + close(results) +} +``` + +### Context for Cancellation and Timeouts + +```go +func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + 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) +} +``` + +### Graceful Shutdown + +```go +func GracefulShutdown(server *http.Server) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} +``` + +### errgroup for Coordinated Goroutines + +```go +import "golang.org/x/sync/errgroup" + +func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { + g, ctx := errgroup.WithContext(ctx) + results := make([][]byte, len(urls)) + + for i, url := range urls { + i, url := i, url // Capture loop variables + g.Go(func() error { + data, err := FetchWithTimeout(ctx, url) + if err != nil { + return err + } + results[i] = data + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + return results, nil +} +``` + +### Avoiding Goroutine Leaks + +```go +// Bad: Goroutine leak if context is cancelled +func leakyFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte) + go func() { + data, _ := fetch(url) + ch <- data // Blocks forever if no receiver + }() + return ch +} + +// Good: Properly handles cancellation +func safeFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte, 1) // Buffered channel + go func() { + data, err := fetch(url) + if err != nil { + return + } + select { + case ch <- data: + case <-ctx.Done(): + } + }() + return ch +} +``` + +## Interface Design + +### Small, Focused Interfaces + +```go +// Good: Single-method interfaces +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} + +type Closer interface { + Close() error +} + +// Compose interfaces as needed +type ReadWriteCloser interface { + Reader + Writer + Closer +} +``` + +### Define Interfaces Where They're Used + +```go +// In the consumer package, not the provider +package service + +// UserStore defines what this service needs +type UserStore interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +type Service struct { + store UserStore +} + +// Concrete implementation can be in another package +// It doesn't need to know about this interface +``` + +### Optional Behavior with Type Assertions + +```go +type Flusher interface { + Flush() error +} + +func WriteAndFlush(w io.Writer, data []byte) error { + if _, err := w.Write(data); err != nil { + return err + } + + // Flush if supported + if f, ok := w.(Flusher); ok { + return f.Flush() + } + return nil +} +``` + +## Package Organization + +### Standard Project Layout + +``` +myproject/ +├── cmd/ +│ └── myapp/ +│ └── main.go # Entry point +├── internal/ +│ ├── handler/ # HTTP handlers +│ ├── service/ # Business logic +│ ├── repository/ # Data access +│ └── config/ # Configuration +├── pkg/ +│ └── client/ # Public API client +├── api/ +│ └── v1/ # API definitions (proto, OpenAPI) +├── testdata/ # Test fixtures +├── go.mod +├── go.sum +└── Makefile +``` + +### Package Naming + +```go +// Good: Short, lowercase, no underscores +package http +package json +package user + +// Bad: Verbose, mixed case, or redundant +package httpHandler +package json_parser +package userService // Redundant 'Service' suffix +``` + +### Avoid Package-Level State + +```go +// Bad: Global mutable state +var db *sql.DB + +func init() { + db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) +} + +// Good: Dependency injection +type Server struct { + db *sql.DB +} + +func NewServer(db *sql.DB) *Server { + return &Server{db: db} +} +``` + +## Struct Design + +### Functional Options Pattern + +```go +type Server struct { + addr string + timeout time.Duration + logger *log.Logger +} + +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { + s.timeout = d + } +} + +func WithLogger(l *log.Logger) Option { + return func(s *Server) { + s.logger = l + } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{ + addr: addr, + timeout: 30 * time.Second, // default + logger: log.Default(), // default + } + for _, opt := range opts { + opt(s) + } + return s +} + +// Usage +server := NewServer(":8080", + WithTimeout(60*time.Second), + WithLogger(customLogger), +) +``` + +### Embedding for Composition + +```go +type Logger struct { + prefix string +} + +func (l *Logger) Log(msg string) { + fmt.Printf("[%s] %s\n", l.prefix, msg) +} + +type Server struct { + *Logger // Embedding - Server gets Log method + addr string +} + +func NewServer(addr string) *Server { + return &Server{ + Logger: &Logger{prefix: "SERVER"}, + addr: addr, + } +} + +// Usage +s := NewServer(":8080") +s.Log("Starting...") // Calls embedded Logger.Log +``` + +## Memory and Performance + +### Preallocate Slices When Size is Known + +```go +// Bad: Grows slice multiple times +func processItems(items []Item) []Result { + var results []Result + for _, item := range items { + results = append(results, process(item)) + } + return results +} + +// Good: Single allocation +func processItems(items []Item) []Result { + results := make([]Result, 0, len(items)) + for _, item := range items { + results = append(results, process(item)) + } + return results +} +``` + +### Use sync.Pool for Frequent Allocations + +```go +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func ProcessRequest(data []byte) []byte { + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + + buf.Write(data) + // Process... + return buf.Bytes() +} +``` + +### Avoid String Concatenation in Loops + +```go +// Bad: Creates many string allocations +func join(parts []string) string { + var result string + for _, p := range parts { + result += p + "," + } + return result +} + +// Good: Single allocation with strings.Builder +func join(parts []string) string { + var sb strings.Builder + for i, p := range parts { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(p) + } + return sb.String() +} + +// Best: Use standard library +func join(parts []string) string { + return strings.Join(parts, ",") +} +``` + +## Go Tooling Integration + +### Essential Commands + +```bash +# Build and run +go build ./... +go run ./cmd/myapp + +# Testing +go test ./... +go test -race ./... +go test -cover ./... + +# Static analysis +go vet ./... +staticcheck ./... +golangci-lint run + +# Module management +go mod tidy +go mod verify + +# Formatting +gofmt -w . +goimports -w . +``` + +### Recommended Linter Configuration (.golangci.yml) + +```yaml +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - goimports + - misspell + - unconvert + - unparam + +linters-settings: + errcheck: + check-type-assertions: true + govet: + check-shadowing: true + +issues: + exclude-use-default: false +``` + +## Quick Reference: Go Idioms + +| Idiom | Description | +|-------|-------------| +| Accept interfaces, return structs | Functions accept interface params, return concrete types | +| Errors are values | Treat errors as first-class values, not exceptions | +| Don't communicate by sharing memory | Use channels for coordination between goroutines | +| Make the zero value useful | Types should work without explicit initialization | +| A little copying is better than a little dependency | Avoid unnecessary external dependencies | +| Clear is better than clever | Prioritize readability over cleverness | +| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports | +| Return early | Handle errors first, keep happy path unindented | + +## Anti-Patterns to Avoid + +```go +// Bad: Naked returns in long functions +func process() (result int, err error) { + // ... 50 lines ... + return // What is being returned? +} + +// Bad: Using panic for control flow +func GetUser(id string) *User { + user, err := db.Find(id) + if err != nil { + panic(err) // Don't do this + } + return user +} + +// Bad: Passing context in struct +type Request struct { + ctx context.Context // Context should be first param + ID string +} + +// Good: Context as first parameter +func ProcessRequest(ctx context.Context, id string) error { + // ... +} + +// Bad: Mixing value and pointer receivers +type Counter struct{ n int } +func (c Counter) Value() int { return c.n } // Value receiver +func (c *Counter) Increment() { c.n++ } // Pointer receiver +// Pick one style and be consistent +``` + +**Remember**: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple. diff --git a/skills/golang-testing/SKILL.md b/skills/golang-testing/SKILL.md new file mode 100644 index 0000000..f7d546e --- /dev/null +++ b/skills/golang-testing/SKILL.md @@ -0,0 +1,719 @@ +--- +name: golang-testing +description: Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. +--- + +# Go Testing Patterns + +Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology. + +## When to Activate + +- Writing new Go functions or methods +- Adding test coverage to existing code +- Creating benchmarks for performance-critical code +- Implementing fuzz tests for input validation +- Following TDD workflow in Go projects + +## TDD Workflow for Go + +### The RED-GREEN-REFACTOR Cycle + +``` +RED → Write a failing test first +GREEN → Write minimal code to pass the test +REFACTOR → Improve code while keeping tests green +REPEAT → Continue with next requirement +``` + +### Step-by-Step TDD in Go + +```go +// Step 1: Define the interface/signature +// calculator.go +package calculator + +func Add(a, b int) int { + panic("not implemented") // Placeholder +} + +// Step 2: Write failing test (RED) +// calculator_test.go +package calculator + +import "testing" + +func TestAdd(t *testing.T) { + got := Add(2, 3) + want := 5 + if got != want { + t.Errorf("Add(2, 3) = %d; want %d", got, want) + } +} + +// Step 3: Run test - verify FAIL +// $ go test +// --- FAIL: TestAdd (0.00s) +// panic: not implemented + +// Step 4: Implement minimal code (GREEN) +func Add(a, b int) int { + return a + b +} + +// Step 5: Run test - verify PASS +// $ go test +// PASS + +// Step 6: Refactor if needed, verify tests still pass +``` + +## Table-Driven Tests + +The standard pattern for Go tests. Enables comprehensive coverage with minimal code. + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -1, -2, -3}, + {"zero values", 0, 0, 0}, + {"mixed signs", -1, 1, 0}, + {"large numbers", 1000000, 2000000, 3000000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.expected { + t.Errorf("Add(%d, %d) = %d; want %d", + tt.a, tt.b, got, tt.expected) + } + }) + } +} +``` + +### Table-Driven Tests with Error Cases + +```go +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + input string + want *Config + wantErr bool + }{ + { + name: "valid config", + input: `{"host": "localhost", "port": 8080}`, + want: &Config{Host: "localhost", Port: 8080}, + }, + { + name: "invalid JSON", + input: `{invalid}`, + wantErr: true, + }, + { + name: "empty input", + input: "", + wantErr: true, + }, + { + name: "minimal config", + input: `{}`, + want: &Config{}, // Zero value config + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseConfig(tt.input) + + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got %+v; want %+v", got, tt.want) + } + }) + } +} +``` + +## Subtests and Sub-benchmarks + +### Organizing Related Tests + +```go +func TestUser(t *testing.T) { + // Setup shared by all subtests + db := setupTestDB(t) + + t.Run("Create", func(t *testing.T) { + user := &User{Name: "Alice"} + err := db.CreateUser(user) + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + if user.ID == "" { + t.Error("expected user ID to be set") + } + }) + + t.Run("Get", func(t *testing.T) { + user, err := db.GetUser("alice-id") + if err != nil { + t.Fatalf("GetUser failed: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } + }) + + t.Run("Update", func(t *testing.T) { + // ... + }) + + t.Run("Delete", func(t *testing.T) { + // ... + }) +} +``` + +### Parallel Subtests + +```go +func TestParallel(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"case1", "input1"}, + {"case2", "input2"}, + {"case3", "input3"}, + } + + for _, tt := range tests { + tt := tt // Capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Run subtests in parallel + result := Process(tt.input) + // assertions... + _ = result + }) + } +} +``` + +## Test Helpers + +### Helper Functions + +```go +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() // Marks this as a helper function + + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + + // Cleanup when test finishes + t.Cleanup(func() { + db.Close() + }) + + // Run migrations + if _, err := db.Exec(schema); err != nil { + t.Fatalf("failed to create schema: %v", err) + } + + return db +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func assertEqual[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("got %v; want %v", got, want) + } +} +``` + +### Temporary Files and Directories + +```go +func TestFileProcessing(t *testing.T) { + // Create temp directory - automatically cleaned up + tmpDir := t.TempDir() + + // Create test file + testFile := filepath.Join(tmpDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + // Run test + result, err := ProcessFile(testFile) + if err != nil { + t.Fatalf("ProcessFile failed: %v", err) + } + + // Assert... + _ = result +} +``` + +## Golden Files + +Testing against expected output files stored in `testdata/`. + +```go +var update = flag.Bool("update", false, "update golden files") + +func TestRender(t *testing.T) { + tests := []struct { + name string + input Template + }{ + {"simple", Template{Name: "test"}}, + {"complex", Template{Name: "test", Items: []string{"a", "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Render(tt.input) + + golden := filepath.Join("testdata", tt.name+".golden") + + if *update { + // Update golden file: go test -update + err := os.WriteFile(golden, got, 0644) + if err != nil { + t.Fatalf("failed to update golden file: %v", err) + } + } + + want, err := os.ReadFile(golden) + if err != nil { + t.Fatalf("failed to read golden file: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want) + } + }) + } +} +``` + +## Mocking with Interfaces + +### Interface-Based Mocking + +```go +// Define interface for dependencies +type UserRepository interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +// Production implementation +type PostgresUserRepository struct { + db *sql.DB +} + +func (r *PostgresUserRepository) GetUser(id string) (*User, error) { + // Real database query +} + +// Mock implementation for tests +type MockUserRepository struct { + GetUserFunc func(id string) (*User, error) + SaveUserFunc func(user *User) error +} + +func (m *MockUserRepository) GetUser(id string) (*User, error) { + return m.GetUserFunc(id) +} + +func (m *MockUserRepository) SaveUser(user *User) error { + return m.SaveUserFunc(user) +} + +// Test using mock +func TestUserService(t *testing.T) { + mock := &MockUserRepository{ + GetUserFunc: func(id string) (*User, error) { + if id == "123" { + return &User{ID: "123", Name: "Alice"}, nil + } + return nil, ErrNotFound + }, + } + + service := NewUserService(mock) + + user, err := service.GetUserProfile("123") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } +} +``` + +## Benchmarks + +### Basic Benchmarks + +```go +func BenchmarkProcess(b *testing.B) { + data := generateTestData(1000) + b.ResetTimer() // Don't count setup time + + for i := 0; i < b.N; i++ { + Process(data) + } +} + +// Run: go test -bench=BenchmarkProcess -benchmem +// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op +``` + +### Benchmark with Different Sizes + +```go +func BenchmarkSort(b *testing.B) { + sizes := []int{100, 1000, 10000, 100000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + data := generateRandomSlice(size) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Make a copy to avoid sorting already sorted data + tmp := make([]int, len(data)) + copy(tmp, data) + sort.Ints(tmp) + } + }) + } +} +``` + +### Memory Allocation Benchmarks + +```go +func BenchmarkStringConcat(b *testing.B) { + parts := []string{"hello", "world", "foo", "bar", "baz"} + + b.Run("plus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var s string + for _, p := range parts { + s += p + } + _ = s + } + }) + + b.Run("builder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var sb strings.Builder + for _, p := range parts { + sb.WriteString(p) + } + _ = sb.String() + } + }) + + b.Run("join", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = strings.Join(parts, "") + } + }) +} +``` + +## Fuzzing (Go 1.18+) + +### Basic Fuzz Test + +```go +func FuzzParseJSON(f *testing.F) { + // Add seed corpus + f.Add(`{"name": "test"}`) + f.Add(`{"count": 123}`) + f.Add(`[]`) + f.Add(`""`) + + f.Fuzz(func(t *testing.T, input string) { + var result map[string]interface{} + err := json.Unmarshal([]byte(input), &result) + + if err != nil { + // Invalid JSON is expected for random input + return + } + + // If parsing succeeded, re-encoding should work + _, err = json.Marshal(result) + if err != nil { + t.Errorf("Marshal failed after successful Unmarshal: %v", err) + } + }) +} + +// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s +``` + +### Fuzz Test with Multiple Inputs + +```go +func FuzzCompare(f *testing.F) { + f.Add("hello", "world") + f.Add("", "") + f.Add("abc", "abc") + + f.Fuzz(func(t *testing.T, a, b string) { + result := Compare(a, b) + + // Property: Compare(a, a) should always equal 0 + if a == b && result != 0 { + t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result) + } + + // Property: Compare(a, b) and Compare(b, a) should have opposite signs + reverse := Compare(b, a) + if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) { + if result != 0 || reverse != 0 { + t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent", + a, b, result, b, a, reverse) + } + } + }) +} +``` + +## Test Coverage + +### Running Coverage + +```bash +# Basic coverage +go test -cover ./... + +# Generate coverage profile +go test -coverprofile=coverage.out ./... + +# View coverage in browser +go tool cover -html=coverage.out + +# View coverage by function +go tool cover -func=coverage.out + +# Coverage with race detection +go test -race -coverprofile=coverage.out ./... +``` + +### Coverage Targets + +| Code Type | Target | +|-----------|--------| +| Critical business logic | 100% | +| Public APIs | 90%+ | +| General code | 80%+ | +| Generated code | Exclude | + +### Excluding Generated Code from Coverage + +```go +//go:generate mockgen -source=interface.go -destination=mock_interface.go + +// In coverage profile, exclude with build tags: +// go test -cover -tags=!generate ./... +``` + +## HTTP Handler Testing + +```go +func TestHealthHandler(t *testing.T) { + // Create request + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + // Call handler + HealthHandler(w, req) + + // Check response + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK) + } + + body, _ := io.ReadAll(resp.Body) + if string(body) != "OK" { + t.Errorf("got body %q; want %q", body, "OK") + } +} + +func TestAPIHandler(t *testing.T) { + tests := []struct { + name string + method string + path string + body string + wantStatus int + wantBody string + }{ + { + name: "get user", + method: http.MethodGet, + path: "/users/123", + wantStatus: http.StatusOK, + wantBody: `{"id":"123","name":"Alice"}`, + }, + { + name: "not found", + method: http.MethodGet, + path: "/users/999", + wantStatus: http.StatusNotFound, + }, + { + name: "create user", + method: http.MethodPost, + path: "/users", + body: `{"name":"Bob"}`, + wantStatus: http.StatusCreated, + }, + } + + handler := NewAPIHandler() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body io.Reader + if tt.body != "" { + body = strings.NewReader(tt.body) + } + + req := httptest.NewRequest(tt.method, tt.path, body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + if w.Code != tt.wantStatus { + t.Errorf("got status %d; want %d", w.Code, tt.wantStatus) + } + + if tt.wantBody != "" && w.Body.String() != tt.wantBody { + t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody) + } + }) + } +} +``` + +## Testing Commands + +```bash +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run specific test +go test -run TestAdd ./... + +# Run tests matching pattern +go test -run "TestUser/Create" ./... + +# Run tests with race detector +go test -race ./... + +# Run tests with coverage +go test -cover -coverprofile=coverage.out ./... + +# Run short tests only +go test -short ./... + +# Run tests with timeout +go test -timeout 30s ./... + +# Run benchmarks +go test -bench=. -benchmem ./... + +# Run fuzzing +go test -fuzz=FuzzParse -fuzztime=30s ./... + +# Count test runs (for flaky test detection) +go test -count=10 ./... +``` + +## Best Practices + +**DO:** +- Write tests FIRST (TDD) +- Use table-driven tests for comprehensive coverage +- Test behavior, not implementation +- Use `t.Helper()` in helper functions +- Use `t.Parallel()` for independent tests +- Clean up resources with `t.Cleanup()` +- Use meaningful test names that describe the scenario + +**DON'T:** +- Test private functions directly (test through public API) +- Use `time.Sleep()` in tests (use channels or conditions) +- Ignore flaky tests (fix or remove them) +- Mock everything (prefer integration tests when possible) +- Skip error path testing + +## Integration with CI/CD + +```yaml +# GitHub Actions example +test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run tests + run: go test -race -coverprofile=coverage.out ./... + + - name: Check coverage + run: | + go tool cover -func=coverage.out | grep total | awk '{print $3}' | \ + awk -F'%' '{if ($1 < 80) exit 1}' +``` + +**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.