mirror of
https://github.com/sweetwisdom/everything-claude-code-zh.git
synced 2026-03-22 06:20:10 +00:00
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:
12
README.md
12
README.md
@@ -4,6 +4,7 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||

|

|
||||||
|
|
||||||
**The complete collection of Claude Code configs from an Anthropic hackathon winner.**
|
**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
|
| |-- e2e-runner.md # Playwright E2E testing
|
||||||
| |-- refactor-cleaner.md # Dead code cleanup
|
| |-- refactor-cleaner.md # Dead code cleanup
|
||||||
| |-- doc-updater.md # Documentation sync
|
| |-- 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
|
|-- skills/ # Workflow definitions and domain knowledge
|
||||||
| |-- coding-standards/ # Language best practices
|
| |-- coding-standards/ # Language best practices
|
||||||
@@ -114,6 +117,8 @@ everything-claude-code/
|
|||||||
| |-- security-review/ # Security checklist
|
| |-- security-review/ # Security checklist
|
||||||
| |-- eval-harness/ # Verification loop evaluation (Longform Guide)
|
| |-- eval-harness/ # Verification loop evaluation (Longform Guide)
|
||||||
| |-- verification-loop/ # Continuous verification (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
|
|-- commands/ # Slash commands for quick execution
|
||||||
| |-- tdd.md # /tdd - Test-driven development
|
| |-- tdd.md # /tdd - Test-driven development
|
||||||
@@ -125,7 +130,10 @@ everything-claude-code/
|
|||||||
| |-- learn.md # /learn - Extract patterns mid-session (Longform Guide)
|
| |-- learn.md # /learn - Extract patterns mid-session (Longform Guide)
|
||||||
| |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide)
|
| |-- checkpoint.md # /checkpoint - Save verification state (Longform Guide)
|
||||||
| |-- verify.md # /verify - Run verification loop (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/)
|
|-- rules/ # Always-follow guidelines (copy to ~/.claude/rules/)
|
||||||
| |-- security.md # Mandatory security checks
|
| |-- security.md # Mandatory security checks
|
||||||
@@ -353,7 +361,7 @@ Please contribute! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|||||||
|
|
||||||
### Ideas for Contributions
|
### 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)
|
- Framework-specific configs (Django, Rails, Laravel)
|
||||||
- DevOps agents (Kubernetes, Terraform, AWS)
|
- DevOps agents (Kubernetes, Terraform, AWS)
|
||||||
- Testing strategies (different frameworks)
|
- Testing strategies (different frameworks)
|
||||||
|
|||||||
368
agents/go-build-resolver.md
Normal file
368
agents/go-build-resolver.md
Normal 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
267
agents/go-reviewer.md
Normal 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
183
commands/go-build.md
Normal 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
148
commands/go-review.md
Normal 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
268
commands/go-test.md
Normal 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/`
|
||||||
673
skills/golang-patterns/SKILL.md
Normal file
673
skills/golang-patterns/SKILL.md
Normal 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.
|
||||||
719
skills/golang-testing/SKILL.md
Normal file
719
skills/golang-testing/SKILL.md
Normal 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.
|
||||||
Reference in New Issue
Block a user