Initial commit
This commit is contained in:
443
commands/review.md
Normal file
443
commands/review.md
Normal file
@@ -0,0 +1,443 @@
|
||||
---
|
||||
name: golang-development:review
|
||||
description: Review Go code for idiomatic patterns, performance issues, security vulnerabilities, and common pitfalls with actionable suggestions
|
||||
---
|
||||
|
||||
# Golang Development Review Command
|
||||
|
||||
Comprehensive Go code review focusing on idiomatic patterns, performance, security, and best practices.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/golang-development:review [file-path-or-directory] [focus-area]
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1` - File path or directory to review (optional, defaults to current directory)
|
||||
- `$2` - Focus area: `all`, `idioms`, `performance`, `security`, `concurrency`, `errors` (optional, defaults to `all`)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Review all files in current directory
|
||||
/golang-development:review
|
||||
|
||||
# Review specific file
|
||||
/golang-development:review internal/service/user.go
|
||||
|
||||
# Focus on performance issues
|
||||
/golang-development:review . performance
|
||||
|
||||
# Focus on security
|
||||
/golang-development:review ./handlers security
|
||||
|
||||
# Review concurrency patterns
|
||||
/golang-development:review . concurrency
|
||||
```
|
||||
|
||||
## Review Categories
|
||||
|
||||
### 1. Idiomatic Go (`idioms`)
|
||||
|
||||
**Checks:**
|
||||
- Naming conventions (camelCase, capitalization)
|
||||
- Error handling patterns
|
||||
- Interface usage and design
|
||||
- Struct composition over inheritance
|
||||
- Receiver naming and types
|
||||
- Exported vs. unexported identifiers
|
||||
- Go proverbs adherence
|
||||
|
||||
**Example Issues:**
|
||||
```go
|
||||
// ❌ BAD: Non-idiomatic error handling
|
||||
func getUser(id string) (*User, string) {
|
||||
if id == "" {
|
||||
return nil, "invalid ID"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ GOOD: Idiomatic error handling
|
||||
func GetUser(id string) (*User, error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("invalid ID: %s", id)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ BAD: Getter naming
|
||||
func (u *User) GetName() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
// ✅ GOOD: Idiomatic getter
|
||||
func (u *User) Name() string {
|
||||
return u.name
|
||||
}
|
||||
|
||||
// ❌ BAD: Setter without validation
|
||||
func (u *User) SetAge(age int) {
|
||||
u.age = age
|
||||
}
|
||||
|
||||
// ✅ GOOD: Validated setter with error
|
||||
func (u *User) SetAge(age int) error {
|
||||
if age < 0 || age > 150 {
|
||||
return fmt.Errorf("invalid age: %d", age)
|
||||
}
|
||||
u.age = age
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Performance (`performance`)
|
||||
|
||||
**Checks:**
|
||||
- Unnecessary allocations
|
||||
- String concatenation in loops
|
||||
- Slice pre-allocation
|
||||
- Map pre-allocation
|
||||
- Defer in loops
|
||||
- Inefficient algorithms
|
||||
- Memory leaks
|
||||
- Goroutine leaks
|
||||
|
||||
**Example Issues:**
|
||||
```go
|
||||
// ❌ BAD: String concatenation in loop
|
||||
func concat(strs []string) string {
|
||||
result := ""
|
||||
for _, s := range strs {
|
||||
result += s // Allocates new string each time
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ✅ GOOD: Use strings.Builder
|
||||
func concat(strs []string) string {
|
||||
var sb strings.Builder
|
||||
for _, s := range strs {
|
||||
sb.WriteString(s)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ❌ BAD: Growing slice
|
||||
func process(n int) []int {
|
||||
var result []int
|
||||
for i := 0; i < n; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ✅ GOOD: Pre-allocate
|
||||
func process(n int) []int {
|
||||
result := make([]int, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
result = append(result, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ❌ BAD: Defer in tight loop
|
||||
for i := 0; i < 10000; i++ {
|
||||
mu.Lock()
|
||||
defer mu.Unlock() // Defers accumulate
|
||||
// ...
|
||||
}
|
||||
|
||||
// ✅ GOOD: Explicit unlock
|
||||
for i := 0; i < 10000; i++ {
|
||||
mu.Lock()
|
||||
// ...
|
||||
mu.Unlock()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Security (`security`)
|
||||
|
||||
**Checks:**
|
||||
- SQL injection vulnerabilities
|
||||
- Command injection
|
||||
- Path traversal
|
||||
- Hardcoded credentials
|
||||
- Weak cryptography
|
||||
- Unsafe operations
|
||||
- Input validation
|
||||
- XSS vulnerabilities
|
||||
|
||||
**Example Issues:**
|
||||
```go
|
||||
// ❌ BAD: SQL injection
|
||||
func getUser(db *sql.DB, username string) (*User, error) {
|
||||
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
|
||||
return db.Query(query)
|
||||
}
|
||||
|
||||
// ✅ GOOD: Parameterized query
|
||||
func getUser(db *sql.DB, username string) (*User, error) {
|
||||
query := "SELECT * FROM users WHERE username = $1"
|
||||
return db.Query(query, username)
|
||||
}
|
||||
|
||||
// ❌ BAD: Hardcoded credentials
|
||||
const apiKey = "sk_live_1234567890"
|
||||
|
||||
// ✅ GOOD: Environment variables
|
||||
apiKey := os.Getenv("API_KEY")
|
||||
|
||||
// ❌ BAD: Weak random
|
||||
func generateToken() string {
|
||||
return fmt.Sprintf("%d", rand.Int())
|
||||
}
|
||||
|
||||
// ✅ GOOD: Cryptographically secure random
|
||||
func generateToken() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Concurrency (`concurrency`)
|
||||
|
||||
**Checks:**
|
||||
- Race conditions
|
||||
- Deadlock potential
|
||||
- Missing mutex protection
|
||||
- Channel misuse
|
||||
- Context propagation
|
||||
- Goroutine leaks
|
||||
- Improper synchronization
|
||||
- Lock contention
|
||||
|
||||
**Example Issues:**
|
||||
```go
|
||||
// ❌ BAD: Race condition
|
||||
type Counter struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.count++ // Not thread-safe
|
||||
}
|
||||
|
||||
// ✅ GOOD: Protected with mutex
|
||||
type Counter struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.count++
|
||||
}
|
||||
|
||||
// ❌ BAD: Goroutine leak
|
||||
func fetchData(url string) <-chan Result {
|
||||
ch := make(chan Result)
|
||||
go func() {
|
||||
// If this fails, goroutine leaks
|
||||
data := fetch(url)
|
||||
ch <- data
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// ✅ GOOD: Context for cancellation
|
||||
func fetchData(ctx context.Context, url string) <-chan Result {
|
||||
ch := make(chan Result)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
data := fetch(url)
|
||||
select {
|
||||
case ch <- data:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Error Handling (`errors`)
|
||||
|
||||
**Checks:**
|
||||
- Ignored errors
|
||||
- Error wrapping
|
||||
- Sentinel errors
|
||||
- Custom error types
|
||||
- Error messages
|
||||
- Panic usage
|
||||
- Recover usage
|
||||
|
||||
**Example Issues:**
|
||||
```go
|
||||
// ❌ BAD: Ignored error
|
||||
file, _ := os.Open("file.txt")
|
||||
|
||||
// ✅ GOOD: Handle error
|
||||
file, err := os.Open("file.txt")
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
|
||||
// ❌ BAD: Lost error context
|
||||
func process() error {
|
||||
if err := doSomething(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ✅ GOOD: Wrapped error
|
||||
func process() error {
|
||||
if err := doSomething(); err != nil {
|
||||
return fmt.Errorf("process failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ❌ BAD: Panic for normal errors
|
||||
func getConfig() *Config {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
panic(err) // Don't panic
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// ✅ GOOD: Return error
|
||||
func getConfig() (*Config, error) {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Review Output Format
|
||||
|
||||
```
|
||||
📝 Code Review Results
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
📂 File: internal/service/user.go
|
||||
|
||||
⚠️ HIGH: SQL Injection Vulnerability (line 45)
|
||||
├─ Issue: Unsanitized user input in SQL query
|
||||
├─ Risk: Database compromise
|
||||
└─ Fix: Use parameterized queries
|
||||
|
||||
💡 MEDIUM: Non-Idiomatic Error Handling (line 67)
|
||||
├─ Issue: Returning string error instead of error type
|
||||
├─ Impact: Type safety, error wrapping
|
||||
└─ Suggestion: Return error type
|
||||
|
||||
⚡ LOW: Performance - Missing Pre-allocation (line 89)
|
||||
├─ Issue: Slice growing without capacity hint
|
||||
├─ Impact: Multiple allocations
|
||||
└─ Optimization: make([]Type, 0, expectedSize)
|
||||
|
||||
✅ GOOD: Proper context propagation (line 23)
|
||||
✅ GOOD: Thread-safe cache implementation (line 112)
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Summary:
|
||||
High: 1 | Medium: 1 | Low: 1 | Good: 2
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
## Automated Checks
|
||||
|
||||
The review includes automated checks using:
|
||||
- `go vet` - Official Go static analysis
|
||||
- `staticcheck` - Advanced static analysis
|
||||
- `gosec` - Security-focused linter
|
||||
- `golangci-lint` - Comprehensive linter suite
|
||||
- Custom pattern matching for Go-specific issues
|
||||
|
||||
## Manual Review Areas
|
||||
|
||||
For complex code, the command performs manual review of:
|
||||
- Architecture and design patterns
|
||||
- API design and interfaces
|
||||
- Test coverage and quality
|
||||
- Documentation completeness
|
||||
- Code complexity and maintainability
|
||||
|
||||
## Actionable Suggestions
|
||||
|
||||
Each issue includes:
|
||||
1. **Location**: Exact file and line number
|
||||
2. **Severity**: HIGH, MEDIUM, LOW
|
||||
3. **Description**: What the issue is
|
||||
4. **Impact**: Why it matters
|
||||
5. **Fix**: How to resolve it
|
||||
6. **Example**: Code snippet showing the fix
|
||||
|
||||
## Integration with Tools
|
||||
|
||||
The command can integrate with:
|
||||
- GitHub PR comments
|
||||
- GitLab merge request notes
|
||||
- Bitbucket PR feedback
|
||||
- Slack notifications
|
||||
- Email reports
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `.go-review.yml` in project root:
|
||||
|
||||
```yaml
|
||||
ignore:
|
||||
- vendor/
|
||||
- mocks/
|
||||
- ".*_test.go"
|
||||
|
||||
severity:
|
||||
min_level: MEDIUM
|
||||
|
||||
focus:
|
||||
- security
|
||||
- performance
|
||||
- concurrency
|
||||
|
||||
custom_rules:
|
||||
- pattern: "fmt.Print"
|
||||
message: "Use structured logging"
|
||||
severity: LOW
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this command:
|
||||
- Before creating pull requests
|
||||
- During code reviews
|
||||
- After major refactoring
|
||||
- When onboarding new team members
|
||||
- As part of CI/CD pipeline
|
||||
- When learning Go best practices
|
||||
- Before production deployment
|
||||
|
||||
## Best Practices
|
||||
|
||||
The review checks compliance with:
|
||||
- Effective Go guidelines
|
||||
- Go Code Review Comments
|
||||
- Go proverbs
|
||||
- Industry best practices
|
||||
- Security standards (OWASP)
|
||||
- Performance optimization patterns
|
||||
456
commands/scaffold.md
Normal file
456
commands/scaffold.md
Normal file
@@ -0,0 +1,456 @@
|
||||
---
|
||||
name: golang-development:scaffold
|
||||
description: Scaffold new Go projects with modern structure, Go modules, testing setup, CI/CD pipelines, and best practices
|
||||
---
|
||||
|
||||
# Golang Development Scaffold Command
|
||||
|
||||
Create a new Go project with a production-ready structure, modern tooling, and best practices built-in.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/golang-development:scaffold <project-name> [options]
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1` - Project name (required, will be used for module name)
|
||||
- `$2` - Project type: `service`, `cli`, `library`, or `microservice` (optional, defaults to `service`)
|
||||
- `$3` - Additional options as JSON (optional)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Create a basic HTTP service
|
||||
/golang-development:scaffold my-api service
|
||||
|
||||
# Create a CLI application
|
||||
/golang-development:scaffold my-tool cli
|
||||
|
||||
# Create a library
|
||||
/golang-development:scaffold my-lib library
|
||||
|
||||
# Create a microservice with full features
|
||||
/golang-development:scaffold user-service microservice '{"with_grpc": true, "with_db": true}'
|
||||
```
|
||||
|
||||
## Project Structures
|
||||
|
||||
### Service (HTTP API)
|
||||
```
|
||||
my-api/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── handler/
|
||||
│ │ └── health.go
|
||||
│ ├── middleware/
|
||||
│ │ └── logging.go
|
||||
│ └── service/
|
||||
│ └── user.go
|
||||
├── pkg/
|
||||
│ └── response/
|
||||
│ └── response.go
|
||||
├── api/
|
||||
│ └── openapi.yaml
|
||||
├── scripts/
|
||||
│ └── build.sh
|
||||
├── deployments/
|
||||
│ ├── Dockerfile
|
||||
│ └── k8s/
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── .gitignore
|
||||
├── .golangci.yml
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### CLI Application
|
||||
```
|
||||
my-tool/
|
||||
├── cmd/
|
||||
│ └── my-tool/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── command/
|
||||
│ │ ├── root.go
|
||||
│ │ └── serve.go
|
||||
│ └── config/
|
||||
│ └── config.go
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── .gitignore
|
||||
├── .golangci.yml
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Library
|
||||
```
|
||||
my-lib/
|
||||
├── example_test.go
|
||||
├── lib.go
|
||||
├── lib_test.go
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── .gitignore
|
||||
├── .golangci.yml
|
||||
├── LICENSE
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Microservice (Full Features)
|
||||
```
|
||||
user-service/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go
|
||||
├── internal/
|
||||
│ ├── domain/
|
||||
│ │ └── user.go
|
||||
│ ├── handler/
|
||||
│ │ ├── http/
|
||||
│ │ └── grpc/
|
||||
│ ├── repository/
|
||||
│ │ └── postgres/
|
||||
│ ├── service/
|
||||
│ │ └── user_service.go
|
||||
│ └── infrastructure/
|
||||
│ ├── database/
|
||||
│ ├── cache/
|
||||
│ └── messaging/
|
||||
├── api/
|
||||
│ ├── http/
|
||||
│ │ └── openapi.yaml
|
||||
│ └── grpc/
|
||||
│ └── user.proto
|
||||
├── pkg/
|
||||
│ ├── logger/
|
||||
│ ├── metrics/
|
||||
│ └── tracing/
|
||||
├── migrations/
|
||||
│ └── 001_create_users.sql
|
||||
├── deployments/
|
||||
│ ├── Dockerfile
|
||||
│ ├── docker-compose.yml
|
||||
│ └── k8s/
|
||||
├── scripts/
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── .gitignore
|
||||
├── .golangci.yml
|
||||
├── Makefile
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Generated Files
|
||||
|
||||
### main.go (Service)
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"{{.ModuleName}}/internal/handler"
|
||||
"{{.ModuleName}}/internal/middleware"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Setup router
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Middleware
|
||||
handler := middleware.Logging(
|
||||
middleware.Recovery(mux),
|
||||
)
|
||||
|
||||
// Routes
|
||||
mux.HandleFunc("/health", handler.Health)
|
||||
mux.HandleFunc("/ready", handler.Ready)
|
||||
|
||||
// Server configuration
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%s", port),
|
||||
Handler: handler,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// Start server
|
||||
go func() {
|
||||
log.Printf("Server starting on port %s", port)
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Graceful shutdown
|
||||
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")
|
||||
}
|
||||
```
|
||||
|
||||
### Makefile
|
||||
```makefile
|
||||
.PHONY: build test lint run clean
|
||||
|
||||
# Variables
|
||||
APP_NAME := {{.ProjectName}}
|
||||
VERSION := $(shell git describe --tags --always --dirty)
|
||||
BUILD_TIME := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
LDFLAGS := -ldflags "-X main.Version=$(VERSION) -X main.BuildTime=$(BUILD_TIME)"
|
||||
|
||||
# Build
|
||||
build:
|
||||
go build $(LDFLAGS) -o bin/$(APP_NAME) ./cmd/server
|
||||
|
||||
# Test
|
||||
test:
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
# Coverage
|
||||
coverage:
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Lint
|
||||
lint:
|
||||
golangci-lint run
|
||||
|
||||
# Run
|
||||
run:
|
||||
go run ./cmd/server
|
||||
|
||||
# Clean
|
||||
clean:
|
||||
rm -rf bin/
|
||||
rm -f coverage.out
|
||||
|
||||
# Install tools
|
||||
tools:
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
# Docker
|
||||
docker-build:
|
||||
docker build -t $(APP_NAME):$(VERSION) .
|
||||
|
||||
docker-run:
|
||||
docker run -p 8080:8080 $(APP_NAME):$(VERSION)
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
```dockerfile
|
||||
# Build stage
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
|
||||
|
||||
# Runtime stage
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
WORKDIR /root/
|
||||
|
||||
# Copy binary from builder
|
||||
COPY --from=builder /app/main .
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./main"]
|
||||
```
|
||||
|
||||
### .golangci.yml
|
||||
```yaml
|
||||
linters:
|
||||
enable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
- gofmt
|
||||
- goimports
|
||||
- misspell
|
||||
- gocritic
|
||||
- gosec
|
||||
- revive
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-blank: true
|
||||
govet:
|
||||
check-shadowing: true
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
```
|
||||
|
||||
### GitHub Actions CI (.github/workflows/ci.yml)
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.out
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: latest
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, lint]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./cmd/server
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The command accepts a JSON configuration object:
|
||||
|
||||
```json
|
||||
{
|
||||
"with_grpc": true,
|
||||
"with_db": true,
|
||||
"with_redis": true,
|
||||
"with_kafka": true,
|
||||
"with_docker": true,
|
||||
"with_k8s": true,
|
||||
"with_ci": true,
|
||||
"db_driver": "postgres",
|
||||
"module_path": "github.com/user/project"
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Parse arguments and configuration
|
||||
2. Create project directory structure
|
||||
3. Initialize Go module
|
||||
4. Generate main.go and core files
|
||||
5. Create Makefile and build scripts
|
||||
6. Add Dockerfile and Docker Compose
|
||||
7. Generate CI/CD configuration
|
||||
8. Create README with usage instructions
|
||||
9. Initialize git repository
|
||||
10. Run `go mod tidy`
|
||||
|
||||
## Features Included
|
||||
|
||||
- **Modern Project Structure**: Clean architecture with separation of concerns
|
||||
- **HTTP Server**: Production-ready with graceful shutdown
|
||||
- **Middleware**: Logging, recovery, CORS, authentication templates
|
||||
- **Health Checks**: Health and readiness endpoints
|
||||
- **Testing**: Test structure and examples
|
||||
- **Linting**: golangci-lint configuration
|
||||
- **CI/CD**: GitHub Actions workflow
|
||||
- **Docker**: Multi-stage Dockerfile
|
||||
- **Kubernetes**: Basic manifests (if requested)
|
||||
- **Documentation**: Comprehensive README
|
||||
|
||||
## Post-Scaffold Steps
|
||||
|
||||
After scaffolding, the command will suggest:
|
||||
|
||||
```bash
|
||||
cd {{.ProjectName}}
|
||||
go mod tidy
|
||||
make test
|
||||
make run
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this command to:
|
||||
- Start new Go projects quickly
|
||||
- Ensure consistent project structure
|
||||
- Set up best practices from the start
|
||||
- Include modern tooling and CI/CD
|
||||
- Scaffold microservices or APIs
|
||||
- Create CLI tools with proper structure
|
||||
577
commands/test.md
Normal file
577
commands/test.md
Normal file
@@ -0,0 +1,577 @@
|
||||
---
|
||||
name: golang-development:test
|
||||
description: Generate comprehensive tests including unit tests, table-driven tests, benchmarks, and examples with high coverage
|
||||
---
|
||||
|
||||
# Golang Development Test Command
|
||||
|
||||
Generate comprehensive, production-ready tests for Go code including unit tests, table-driven tests, benchmarks, and examples.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
/golang-development:test <file-or-function> [test-type] [options]
|
||||
```
|
||||
|
||||
## Arguments
|
||||
|
||||
- `$1` - File path or function name to test (required)
|
||||
- `$2` - Test type: `unit`, `table`, `benchmark`, `integration`, `all` (optional, defaults to `unit`)
|
||||
- `$3` - Options as JSON (optional)
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Generate unit tests for a file
|
||||
/golang-development:test internal/service/user.go
|
||||
|
||||
# Generate table-driven tests
|
||||
/golang-development:test internal/service/user.go table
|
||||
|
||||
# Generate benchmarks
|
||||
/golang-development:test internal/service/user.go benchmark
|
||||
|
||||
# Generate all test types
|
||||
/golang-development:test internal/service/user.go all
|
||||
|
||||
# Generate tests with options
|
||||
/golang-development:test internal/service/user.go unit '{"with_mocks": true, "coverage_target": 90}'
|
||||
```
|
||||
|
||||
## Test Types
|
||||
|
||||
### 1. Unit Tests
|
||||
|
||||
Basic unit tests for individual functions:
|
||||
|
||||
```go
|
||||
// Source: user.go
|
||||
package service
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Email string
|
||||
Age int
|
||||
}
|
||||
|
||||
func (u *User) IsAdult() bool {
|
||||
return u.Age >= 18
|
||||
}
|
||||
|
||||
func ValidateEmail(email string) error {
|
||||
if !strings.Contains(email, "@") {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generated: user_test.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUser_IsAdult(t *testing.T) {
|
||||
t.Run("adult user", func(t *testing.T) {
|
||||
user := &User{Age: 25}
|
||||
if !user.IsAdult() {
|
||||
t.Error("expected user to be adult")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("minor user", func(t *testing.T) {
|
||||
user := &User{Age: 15}
|
||||
if user.IsAdult() {
|
||||
t.Error("expected user to be minor")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("edge case - exactly 18", func(t *testing.T) {
|
||||
user := &User{Age: 18}
|
||||
if !user.IsAdult() {
|
||||
t.Error("18 year old should be adult")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid email",
|
||||
email: "user@example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid email - no @",
|
||||
email: "userexample.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
email: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateEmail() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Table-Driven Tests
|
||||
|
||||
Comprehensive table-driven tests:
|
||||
|
||||
```go
|
||||
// Source: calculator.go
|
||||
package calculator
|
||||
|
||||
func Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
func Divide(a, b float64) (float64, error) {
|
||||
if b == 0 {
|
||||
return 0, errors.New("division by zero")
|
||||
}
|
||||
return a / b, nil
|
||||
}
|
||||
|
||||
// Generated: calculator_test.go
|
||||
package calculator
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a int
|
||||
b int
|
||||
expected int
|
||||
}{
|
||||
{"positive numbers", 2, 3, 5},
|
||||
{"negative numbers", -2, -3, -5},
|
||||
{"mixed signs", -2, 3, 1},
|
||||
{"zeros", 0, 0, 0},
|
||||
{"large numbers", 1000000, 2000000, 3000000},
|
||||
{"overflow scenario", math.MaxInt - 1, 1, math.MaxInt},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Add(tt.a, tt.b)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||||
tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDivide(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
a float64
|
||||
b float64
|
||||
expected float64
|
||||
expectErr bool
|
||||
}{
|
||||
{"normal division", 10.0, 2.0, 5.0, false},
|
||||
{"division by zero", 10.0, 0.0, 0.0, true},
|
||||
{"negative numbers", -10.0, 2.0, -5.0, false},
|
||||
{"fractional result", 7.0, 2.0, 3.5, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Divide(tt.a, tt.b)
|
||||
|
||||
if tt.expectErr {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if math.Abs(result-tt.expected) > 0.0001 {
|
||||
t.Errorf("Divide(%f, %f) = %f; want %f",
|
||||
tt.a, tt.b, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Benchmarks
|
||||
|
||||
Performance benchmarks:
|
||||
|
||||
```go
|
||||
// Generated: user_bench_test.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkUser_IsAdult(b *testing.B) {
|
||||
user := &User{Age: 25}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = user.IsAdult()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidateEmail(b *testing.B) {
|
||||
email := "test@example.com"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ValidateEmail(email)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidateEmail_Invalid(b *testing.B) {
|
||||
email := "invalid-email"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ValidateEmail(email)
|
||||
}
|
||||
}
|
||||
|
||||
// Memory allocation benchmarks
|
||||
func BenchmarkStringConcatenation(b *testing.B) {
|
||||
strs := []string{"hello", "world", "foo", "bar"}
|
||||
|
||||
b.Run("operator", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := ""
|
||||
for _, s := range strs {
|
||||
result += s
|
||||
}
|
||||
_ = result
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("strings.Builder", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var sb strings.Builder
|
||||
for _, s := range strs {
|
||||
sb.WriteString(s)
|
||||
}
|
||||
_ = sb.String()
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Integration Tests
|
||||
|
||||
Integration tests with external dependencies:
|
||||
|
||||
```go
|
||||
// Generated: user_integration_test.go
|
||||
// +build integration
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := sql.Open("postgres", "postgres://test:test@localhost/test?sslmode=disable")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Create schema
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
age INTEGER NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create schema: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DROP TABLE users")
|
||||
db.Close()
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func TestUserRepository_Create_Integration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test in short mode")
|
||||
}
|
||||
|
||||
db := setupTestDB(t)
|
||||
repo := NewUserRepository(db)
|
||||
|
||||
ctx := context.Background()
|
||||
user := &User{
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, user)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create user: %v", err)
|
||||
}
|
||||
|
||||
if user.ID == "" {
|
||||
t.Error("expected user ID to be set")
|
||||
}
|
||||
|
||||
// Verify user was created
|
||||
retrieved, err := repo.GetByEmail(ctx, user.Email)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve user: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Email != user.Email {
|
||||
t.Errorf("email mismatch: got %s, want %s", retrieved.Email, user.Email)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Mock Generation
|
||||
|
||||
Generate mocks for interfaces:
|
||||
|
||||
```go
|
||||
// Source: repository.go
|
||||
package service
|
||||
|
||||
type UserRepository interface {
|
||||
GetByID(ctx context.Context, id string) (*User, error)
|
||||
Create(ctx context.Context, user *User) error
|
||||
Update(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// Generated: mocks/user_repository_mock.go
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"yourmodule/service"
|
||||
)
|
||||
|
||||
type MockUserRepository struct {
|
||||
mu sync.Mutex
|
||||
|
||||
GetByIDFunc func(ctx context.Context, id string) (*service.User, error)
|
||||
GetByIDCalls []GetByIDCall
|
||||
|
||||
CreateFunc func(ctx context.Context, user *service.User) error
|
||||
CreateCalls []CreateCall
|
||||
|
||||
UpdateFunc func(ctx context.Context, user *service.User) error
|
||||
UpdateCalls []UpdateCall
|
||||
|
||||
DeleteFunc func(ctx context.Context, id string) error
|
||||
DeleteCalls []DeleteCall
|
||||
}
|
||||
|
||||
type GetByIDCall struct {
|
||||
Ctx context.Context
|
||||
ID string
|
||||
}
|
||||
|
||||
type CreateCall struct {
|
||||
Ctx context.Context
|
||||
User *service.User
|
||||
}
|
||||
|
||||
// ... more types ...
|
||||
|
||||
func (m *MockUserRepository) GetByID(ctx context.Context, id string) (*service.User, error) {
|
||||
m.mu.Lock()
|
||||
m.GetByIDCalls = append(m.GetByIDCalls, GetByIDCall{Ctx: ctx, ID: id})
|
||||
m.mu.Unlock()
|
||||
|
||||
if m.GetByIDFunc != nil {
|
||||
return m.GetByIDFunc(ctx, id)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ... more methods ...
|
||||
|
||||
// Usage in tests:
|
||||
func TestUserService_GetUser(t *testing.T) {
|
||||
mockRepo := &mocks.MockUserRepository{
|
||||
GetByIDFunc: func(ctx context.Context, id string) (*service.User, error) {
|
||||
return &service.User{
|
||||
ID: id,
|
||||
Email: "test@example.com",
|
||||
Age: 25,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := service.NewUserService(mockRepo)
|
||||
user, err := svc.GetUser(context.Background(), "123")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if user.ID != "123" {
|
||||
t.Errorf("expected user ID 123, got %s", user.ID)
|
||||
}
|
||||
|
||||
if len(mockRepo.GetByIDCalls) != 1 {
|
||||
t.Errorf("expected 1 call to GetByID, got %d", len(mockRepo.GetByIDCalls))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Test Helpers
|
||||
|
||||
Generate common test helpers:
|
||||
|
||||
```go
|
||||
// Generated: testhelpers/helpers.go
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AssertEqual checks if two values are equal
|
||||
func AssertEqual(t *testing.T, got, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertError checks if an error occurred
|
||||
func AssertError(t *testing.T, err error, wantErr bool) {
|
||||
t.Helper()
|
||||
if (err != nil) != wantErr {
|
||||
t.Errorf("error = %v, wantErr %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertNil checks if value is nil
|
||||
func AssertNil(t *testing.T, got interface{}) {
|
||||
t.Helper()
|
||||
if got != nil {
|
||||
t.Errorf("expected nil, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertNotNil checks if value is not nil
|
||||
func AssertNotNil(t *testing.T, got interface{}) {
|
||||
t.Helper()
|
||||
if got == nil {
|
||||
t.Error("expected non-nil value")
|
||||
}
|
||||
}
|
||||
|
||||
// Eventually retries assertion until timeout
|
||||
func Eventually(t *testing.T, assertion func() bool, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
|
||||
for time.Now().Before(deadline) {
|
||||
if assertion() {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Error("assertion failed within timeout")
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
```json
|
||||
{
|
||||
"with_mocks": true,
|
||||
"with_benchmarks": true,
|
||||
"with_examples": true,
|
||||
"coverage_target": 80,
|
||||
"use_testify": false,
|
||||
"parallel_tests": true,
|
||||
"generate_helpers": true
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage Analysis
|
||||
|
||||
The command includes coverage analysis:
|
||||
|
||||
```bash
|
||||
# Run tests with coverage
|
||||
go test -coverprofile=coverage.out ./...
|
||||
|
||||
# View coverage report
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
# Check coverage threshold
|
||||
go test -cover ./... | grep "coverage:"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
Generated tests follow:
|
||||
- Table-driven test patterns
|
||||
- Subtests for isolation
|
||||
- Test helpers for DRY code
|
||||
- Proper cleanup with t.Cleanup()
|
||||
- Context usage in tests
|
||||
- Parallel test execution
|
||||
- Comprehensive edge cases
|
||||
- Clear test names
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this command to:
|
||||
- Generate tests for new code
|
||||
- Improve test coverage
|
||||
- Add missing test cases
|
||||
- Create benchmark tests
|
||||
- Generate integration tests
|
||||
- Mock external dependencies
|
||||
- Follow testing best practices
|
||||
Reference in New Issue
Block a user