Skip to content

Commit 35edc4f

Browse files
Merge pull request #1366 from blublinsky/additional-skills
add skills support for go code/test quality and PR reviews
2 parents 8074089 + e27c652 commit 35edc4f

12 files changed

Lines changed: 1824 additions & 1 deletion

File tree

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
name: go-code-review
3+
description: Reviews Go code for idiomatic patterns, error handling, concurrency safety, and common mistakes. Use when reviewing .go files, checking error handling, goroutine usage, or interface design.
4+
---
5+
6+
# Go Code Review
7+
8+
## Quick Reference
9+
10+
| Issue Type | Reference |
11+
|------------|-----------|
12+
| Missing error checks, wrapped errors | [references/error-handling.md](references/error-handling.md) |
13+
| Race conditions, channel misuse | [references/concurrency.md](references/concurrency.md) |
14+
| Interface pollution, naming | [references/interfaces.md](references/interfaces.md) |
15+
| Resource leaks, defer misuse | [references/common-mistakes.md](references/common-mistakes.md) |
16+
17+
## Review Checklist
18+
19+
- [ ] All errors are checked (no `_ = err`)
20+
- [ ] Errors wrapped with context (`fmt.Errorf("...: %w", err)`)
21+
- [ ] Resources closed with `defer` immediately after creation
22+
- [ ] No goroutine leaks (channels closed, contexts canceled)
23+
- [ ] Interfaces defined by consumers, not producers
24+
- [ ] Interface names end in `-er` (Reader, Writer, Handler)
25+
- [ ] Exported names have doc comments
26+
- [ ] No naked returns in functions > 5 lines
27+
- [ ] Context passed as first parameter
28+
- [ ] Mutexes protect shared state, not methods
29+
30+
### Kubernetes Operator Specific
31+
32+
- [ ] Owner references set with `controllerutil.SetControllerReference()`
33+
- [ ] Finalizers added/removed safely (check for DeletionTimestamp)
34+
- [ ] Context propagated through reconcile loops
35+
- [ ] Client errors handled (distinguish NotFound vs other errors)
36+
- [ ] Status updates separate from spec changes
37+
- [ ] Resource watching pattern: Owned resources tracked via ResourceVersion, external resources use explicit watchers (see `internal/controller/watchers/`)
38+
- [ ] Reconcile functions are idempotent (safe to call multiple times)
39+
- [ ] Resource updates check semantic equality first (`apiequality.Semantic.DeepEqual`)
40+
- [ ] Return `ctrl.Result{Requeue: true}` for transient issues, errors for permanent failures
41+
- [ ] RBAC markers (`//+kubebuilder:rbac`) present for all resource access in controllers
42+
43+
## When to Load References
44+
45+
- Reviewing error return patterns → error-handling.md
46+
- Reviewing goroutines/channels → concurrency.md
47+
- Reviewing type definitions → interfaces.md
48+
- General Go review → common-mistakes.md
49+
50+
## Review Questions
51+
52+
1. Are all error returns checked and wrapped?
53+
2. Are goroutines properly managed with context cancellation?
54+
3. Are resources (files, connections) closed with defer?
55+
4. Are interfaces minimal and defined where used?
56+
57+
## Valid Patterns (Do NOT Flag)
58+
59+
These patterns are acceptable and should NOT be flagged as issues:
60+
61+
- **`_ = err` with reason comment** - Intentionally ignored errors with explanation
62+
```go
63+
_ = conn.Close() // Best effort cleanup, already handling primary error
64+
```
65+
- **Empty interface `interface{}`** - For truly generic code (pre-generics codebases)
66+
- **Naked returns in short functions** - Acceptable in functions < 5 lines with named returns
67+
- **Channel without close** - When consumer stops via context cancellation, not channel close
68+
- **Mutex protecting struct fields** - Even if accessed only via methods, this is correct encapsulation
69+
- **`//nolint` directives with reason** - Acceptable when accompanied by explanation
70+
```go
71+
//nolint:errcheck // Error logged but not returned per API contract
72+
```
73+
- **Defer in loop** - When function scope cleanup is intentional (e.g., processing files in batches)
74+
75+
## Context-Sensitive Rules
76+
77+
Only flag these issues when the specific conditions apply:
78+
79+
| Issue | Flag ONLY IF |
80+
|-------|--------------|
81+
| Missing error check | Error return is actionable (can retry, log, or propagate) |
82+
| Goroutine leak | No context cancellation path exists for the goroutine |
83+
| Missing defer | Resource isn't explicitly closed before next acquisition or return |
84+
| Interface pollution | Interface has > 1 method AND only one consumer exists |
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
# Common Mistakes
2+
3+
## Resource Leaks
4+
5+
### 1. Missing defer for Close
6+
7+
**Problem**: Resources leaked on early return.
8+
9+
```go
10+
// BAD
11+
func readFile(path string) ([]byte, error) {
12+
f, err := os.Open(path)
13+
if err != nil {
14+
return nil, err
15+
}
16+
data, err := io.ReadAll(f)
17+
if err != nil {
18+
return nil, err // file never closed!
19+
}
20+
f.Close()
21+
return data, nil
22+
}
23+
24+
// GOOD - defer immediately
25+
func readFile(path string) ([]byte, error) {
26+
f, err := os.Open(path)
27+
if err != nil {
28+
return nil, err
29+
}
30+
defer f.Close()
31+
return io.ReadAll(f)
32+
}
33+
```
34+
35+
### 2. Defer in Loop
36+
37+
**Problem**: Resources accumulate until function returns.
38+
39+
```go
40+
// BAD - files stay open until loop ends
41+
for _, path := range paths {
42+
f, _ := os.Open(path)
43+
defer f.Close() // deferred until function returns
44+
process(f)
45+
}
46+
47+
// GOOD - close in each iteration or use closure
48+
for _, path := range paths {
49+
func() {
50+
f, _ := os.Open(path)
51+
defer f.Close()
52+
process(f)
53+
}()
54+
}
55+
```
56+
57+
### 3. HTTP Response Body Not Closed
58+
59+
**Problem**: Connection pool exhaustion.
60+
61+
```go
62+
// BAD
63+
resp, err := http.Get(url)
64+
if err != nil {
65+
return err
66+
}
67+
// body never closed!
68+
data, _ := io.ReadAll(resp.Body)
69+
70+
// GOOD
71+
resp, err := http.Get(url)
72+
if err != nil {
73+
return err
74+
}
75+
defer resp.Body.Close()
76+
data, _ := io.ReadAll(resp.Body)
77+
```
78+
79+
## Naming and Style
80+
81+
### 4. Stuttering Names
82+
83+
**Problem**: Redundant when used with package name.
84+
85+
```go
86+
// BAD
87+
package user
88+
type UserService struct { ... } // user.UserService
89+
90+
// GOOD
91+
package user
92+
type Service struct { ... } // user.Service
93+
```
94+
95+
### 5. Missing Doc Comments on Exports
96+
97+
**Problem**: godoc can't generate documentation.
98+
99+
```go
100+
// BAD
101+
func NewServer(addr string) *Server { ... }
102+
103+
// GOOD
104+
// NewServer creates a new HTTP server listening on addr.
105+
func NewServer(addr string) *Server { ... }
106+
```
107+
108+
### 6. Naked Returns in Long Functions
109+
110+
**Problem**: Hard to track what's being returned.
111+
112+
```go
113+
// BAD
114+
func process(data []byte) (result string, err error) {
115+
// 50 lines of code...
116+
117+
return // what's being returned?
118+
}
119+
120+
// GOOD - explicit returns
121+
func process(data []byte) (string, error) {
122+
// 50 lines of code...
123+
124+
return processedString, nil
125+
}
126+
```
127+
128+
## Initialization
129+
130+
### 7. Init Function Overuse
131+
132+
**Problem**: Hidden side effects, hard to test.
133+
134+
```go
135+
// BAD - global state via init
136+
var db *sql.DB
137+
138+
func init() {
139+
var err error
140+
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
141+
if err != nil {
142+
log.Fatal(err)
143+
}
144+
}
145+
146+
// GOOD - explicit initialization
147+
type App struct {
148+
db *sql.DB
149+
}
150+
151+
func NewApp(dbURL string) (*App, error) {
152+
db, err := sql.Open("postgres", dbURL)
153+
if err != nil {
154+
return nil, fmt.Errorf("opening db: %w", err)
155+
}
156+
return &App{db: db}, nil
157+
}
158+
```
159+
160+
### 8. Global Mutable State
161+
162+
**Problem**: Race conditions, hard to test.
163+
164+
```go
165+
// BAD
166+
var config Config
167+
168+
func GetConfig() Config {
169+
return config
170+
}
171+
172+
// GOOD - dependency injection
173+
type Server struct {
174+
config Config
175+
}
176+
177+
func NewServer(cfg Config) *Server {
178+
return &Server{config: cfg}
179+
}
180+
```
181+
182+
## Performance
183+
184+
### 9. String Concatenation in Loop
185+
186+
**Problem**: O(n²) allocation overhead.
187+
188+
```go
189+
// BAD
190+
var result string
191+
for _, s := range items {
192+
result += s + ", "
193+
}
194+
195+
// GOOD
196+
var b strings.Builder
197+
for _, s := range items {
198+
b.WriteString(s)
199+
b.WriteString(", ")
200+
}
201+
result := b.String()
202+
```
203+
204+
### 10. Slice Preallocation
205+
206+
**Problem**: Repeated reallocations.
207+
208+
```go
209+
// BAD - grows dynamically
210+
var results []Result
211+
for _, item := range items {
212+
results = append(results, process(item))
213+
}
214+
215+
// GOOD - preallocate known size
216+
results := make([]Result, 0, len(items))
217+
for _, item := range items {
218+
results = append(results, process(item))
219+
}
220+
```
221+
222+
## Testing
223+
224+
### 11. Table-Driven Tests Missing
225+
226+
**Problem**: Verbose, repetitive test code.
227+
228+
```go
229+
// BAD
230+
func TestAdd(t *testing.T) {
231+
if Add(1, 2) != 3 {
232+
t.Error("1+2 should be 3")
233+
}
234+
if Add(0, 0) != 0 {
235+
t.Error("0+0 should be 0")
236+
}
237+
}
238+
239+
// GOOD
240+
func TestAdd(t *testing.T) {
241+
tests := []struct {
242+
a, b, want int
243+
}{
244+
{1, 2, 3},
245+
{0, 0, 0},
246+
{-1, 1, 0},
247+
}
248+
for _, tt := range tests {
249+
got := Add(tt.a, tt.b)
250+
if got != tt.want {
251+
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
252+
}
253+
}
254+
}
255+
```
256+
257+
## Review Questions
258+
259+
1. Is `defer Close()` called immediately after opening resources?
260+
2. Are HTTP response bodies always closed?
261+
3. Are package-level names not stuttering with package name?
262+
4. Do exported symbols have doc comments?
263+
5. Is mutable global state avoided?
264+
6. Are slices preallocated when size is known?

0 commit comments

Comments
 (0)