feat: add comprehensive Golang language support

Add Go-specific agents, skills, and commands for idiomatic Go development:

Agents:
- go-reviewer: Go code review for concurrency, error handling, security
- go-build-resolver: Fix Go build errors with minimal changes

Skills:
- golang-patterns: Idiomatic Go patterns, best practices, conventions
- golang-testing: TDD, table-driven tests, benchmarks, fuzzing

Commands:
- /go-review: Invoke Go code reviewer
- /go-test: Go TDD workflow with coverage
- /go-build: Fix Go build/vet/lint errors

Also updates README.md to document the new Go support.
This commit is contained in:
Claude
2026-01-26 08:20:42 +00:00
parent 56ff5d444b
commit 75e1e46f3f
8 changed files with 2636 additions and 2 deletions

View File

@@ -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)

368
agents/go-build-resolver.md Normal file
View File

@@ -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.

267
agents/go-reviewer.md Normal file
View File

@@ -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?"

183
commands/go-build.md Normal file
View File

@@ -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/`

148
commands/go-review.md Normal file
View File

@@ -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/`

268
commands/go-test.md Normal file
View File

@@ -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/`

View File

@@ -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.

View File

@@ -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.