Initial commit
This commit is contained in:
322
skills/go-reviewer/SKILL.md
Normal file
322
skills/go-reviewer/SKILL.md
Normal file
@@ -0,0 +1,322 @@
|
||||
---
|
||||
name: go-reviewer
|
||||
description: |
|
||||
WHEN: Go project review, error handling, goroutines, interfaces, testing
|
||||
WHAT: Error handling patterns + Concurrency safety + Interface design + Testing + Idiomatic Go
|
||||
WHEN NOT: Go API frameworks → go-api-reviewer, Rust → rust-reviewer
|
||||
---
|
||||
|
||||
# Go Reviewer Skill
|
||||
|
||||
## Purpose
|
||||
Reviews Go code for idiomatic patterns, error handling, concurrency, and best practices.
|
||||
|
||||
## When to Use
|
||||
- Go project code review
|
||||
- Error handling review
|
||||
- Goroutine/channel review
|
||||
- Interface design review
|
||||
- Go testing patterns
|
||||
|
||||
## Project Detection
|
||||
- `go.mod` in project root
|
||||
- `.go` files
|
||||
- `cmd/`, `internal/`, `pkg/` structure
|
||||
- `*_test.go` test files
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Analyze Project
|
||||
```
|
||||
**Go Version**: 1.21+
|
||||
**Module**: github.com/org/project
|
||||
**Structure**: Standard Go layout
|
||||
**Testing**: go test / testify
|
||||
**Linter**: golangci-lint
|
||||
```
|
||||
|
||||
### Step 2: Select Review Areas
|
||||
**AskUserQuestion:**
|
||||
```
|
||||
"Which areas to review?"
|
||||
Options:
|
||||
- Full Go review (recommended)
|
||||
- Error handling patterns
|
||||
- Concurrency and goroutines
|
||||
- Interface design
|
||||
- Testing and benchmarks
|
||||
multiSelect: true
|
||||
```
|
||||
|
||||
## Detection Rules
|
||||
|
||||
### Error Handling
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| Ignored error | Always handle errors | CRITICAL |
|
||||
| err != nil only | Add context with fmt.Errorf | MEDIUM |
|
||||
| Panic for errors | Return error instead | HIGH |
|
||||
| No error wrapping | Use %w for wrapping | MEDIUM |
|
||||
|
||||
```go
|
||||
// BAD: Ignored error
|
||||
data, _ := ioutil.ReadFile("config.json")
|
||||
|
||||
// GOOD: Handle error
|
||||
data, err := os.ReadFile("config.json")
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
|
||||
// BAD: No context
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// GOOD: Add context
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to process user %d: %w", userID, err)
|
||||
}
|
||||
|
||||
// BAD: Panic for recoverable error
|
||||
func GetUser(id int) *User {
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
panic(err) // Don't panic!
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// GOOD: Return error
|
||||
func GetUser(id int) (*User, error) {
|
||||
user, err := db.FindUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting user %d: %w", id, err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Concurrency
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| Data race potential | Use mutex or channels | CRITICAL |
|
||||
| Goroutine leak | Ensure goroutines exit | HIGH |
|
||||
| Unbuffered channel deadlock | Use buffered or select | HIGH |
|
||||
| No context cancellation | Pass context.Context | MEDIUM |
|
||||
|
||||
```go
|
||||
// BAD: Data race
|
||||
type Counter struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *Counter) Increment() {
|
||||
c.count++ // Race condition!
|
||||
}
|
||||
|
||||
// GOOD: Mutex protection
|
||||
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 process(items []Item) {
|
||||
for _, item := range items {
|
||||
go processItem(item) // No way to wait or cancel!
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: WaitGroup and context
|
||||
func process(ctx context.Context, items []Item) error {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, item := range items {
|
||||
item := item // Capture loop variable
|
||||
g.Go(func() error {
|
||||
return processItem(ctx, item)
|
||||
})
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// BAD: No timeout
|
||||
resp, err := client.Do(req)
|
||||
|
||||
// GOOD: With context timeout
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
resp, err := client.Do(req)
|
||||
```
|
||||
|
||||
### Interface Design
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| Large interface | Keep interfaces small | MEDIUM |
|
||||
| Interface in implementation | Define at consumer | MEDIUM |
|
||||
| Concrete type in signature | Accept interfaces | MEDIUM |
|
||||
| No interface for testing | Add interface for mocking | MEDIUM |
|
||||
|
||||
```go
|
||||
// BAD: Large interface
|
||||
type UserService interface {
|
||||
GetUser(id int) (*User, error)
|
||||
CreateUser(u *User) error
|
||||
UpdateUser(u *User) error
|
||||
DeleteUser(id int) error
|
||||
ListUsers() ([]*User, error)
|
||||
SearchUsers(query string) ([]*User, error)
|
||||
// ... 20 more methods
|
||||
}
|
||||
|
||||
// GOOD: Small, focused interfaces
|
||||
type UserGetter interface {
|
||||
GetUser(ctx context.Context, id int) (*User, error)
|
||||
}
|
||||
|
||||
type UserCreator interface {
|
||||
CreateUser(ctx context.Context, u *User) error
|
||||
}
|
||||
|
||||
// Consumer defines the interface it needs
|
||||
type UserHandler struct {
|
||||
getter UserGetter // Only what it needs
|
||||
}
|
||||
|
||||
// BAD: Concrete type dependency
|
||||
func ProcessFile(f *os.File) error {
|
||||
// Hard to test
|
||||
}
|
||||
|
||||
// GOOD: Interface dependency
|
||||
func ProcessFile(r io.Reader) error {
|
||||
// Easy to test with strings.Reader, bytes.Buffer, etc.
|
||||
}
|
||||
```
|
||||
|
||||
### Code Organization
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| No package structure | Use cmd/internal/pkg | MEDIUM |
|
||||
| Exported unnecessary | Keep internal private | LOW |
|
||||
| Package name mismatch | Match directory name | LOW |
|
||||
| Circular import | Restructure packages | HIGH |
|
||||
|
||||
```
|
||||
// GOOD: Standard Go project layout
|
||||
project/
|
||||
├── cmd/
|
||||
│ └── server/
|
||||
│ └── main.go
|
||||
├── internal/ # Private packages
|
||||
│ ├── service/
|
||||
│ │ └── user.go
|
||||
│ └── repository/
|
||||
│ └── user.go
|
||||
├── pkg/ # Public packages
|
||||
│ └── client/
|
||||
│ └── client.go
|
||||
├── go.mod
|
||||
└── go.sum
|
||||
```
|
||||
|
||||
### Testing
|
||||
| Check | Recommendation | Severity |
|
||||
|-------|----------------|----------|
|
||||
| No table-driven tests | Use test tables | MEDIUM |
|
||||
| No test helpers | Extract common setup | LOW |
|
||||
| No benchmarks | Add for hot paths | LOW |
|
||||
| Mocking concrete types | Use interfaces | MEDIUM |
|
||||
|
||||
```go
|
||||
// GOOD: Table-driven test
|
||||
func TestParseSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int64
|
||||
wantErr bool
|
||||
}{
|
||||
{"bytes", "100", 100, false},
|
||||
{"kilobytes", "1KB", 1024, false},
|
||||
{"megabytes", "1MB", 1048576, false},
|
||||
{"invalid", "abc", 0, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ParseSize(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseSize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("ParseSize() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GOOD: Benchmark
|
||||
func BenchmarkParseSize(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseSize("1MB")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response Template
|
||||
```
|
||||
## Go Code Review Results
|
||||
|
||||
**Project**: [name]
|
||||
**Go**: 1.22 | **Linter**: golangci-lint
|
||||
|
||||
### Error Handling
|
||||
| Status | File | Issue |
|
||||
|--------|------|-------|
|
||||
| CRITICAL | service.go:45 | Ignored error from db.Query |
|
||||
|
||||
### Concurrency
|
||||
| Status | File | Issue |
|
||||
|--------|------|-------|
|
||||
| HIGH | worker.go:23 | Potential goroutine leak |
|
||||
|
||||
### Interface Design
|
||||
| Status | File | Issue |
|
||||
|--------|------|-------|
|
||||
| MEDIUM | handler.go:12 | Concrete type in function signature |
|
||||
|
||||
### Testing
|
||||
| Status | File | Issue |
|
||||
|--------|------|-------|
|
||||
| MEDIUM | service_test.go | No table-driven tests |
|
||||
|
||||
### Recommended Actions
|
||||
1. [ ] Handle all returned errors
|
||||
2. [ ] Add context cancellation to goroutines
|
||||
3. [ ] Define interfaces at consumer side
|
||||
4. [ ] Convert tests to table-driven format
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
1. **Errors**: Always handle, wrap with context
|
||||
2. **Concurrency**: Use context, errgroup, proper sync
|
||||
3. **Interfaces**: Small, defined at consumer
|
||||
4. **Testing**: Table-driven, interfaces for mocking
|
||||
5. **Linting**: Use golangci-lint
|
||||
|
||||
## Integration
|
||||
- `go-api-reviewer`: API framework patterns
|
||||
- `security-scanner`: Go security audit
|
||||
- `perf-analyzer`: Go performance profiling
|
||||
Reference in New Issue
Block a user