[autoresearch] baseline: system prompt quality tests (5/5 passing)
This commit is contained in:
1 parent
05cc036f6d
commit
60b0101717
142 files changed
+18589
-9947
No files matched your search
@@ -0,0 +1,315 @@
|
||||
---
|
||||
trigger: model_decision
|
||||
description: When writing Go code, reviewing Go idioms, or working on a Go backend project
|
||||
---
|
||||
|
||||
## Go Idioms and Patterns
|
||||
|
||||
### Core Philosophy
|
||||
|
||||
Go favors simplicity, explicitness, and readability. The language is intentionally small — resist the urge to import patterns from other languages. If it looks boring and obvious, it's probably idiomatic Go.
|
||||
|
||||
> **Scope:** This file covers Go-specific *coding idioms*. For file layout, see `project-structure-go-backend.md`. For test naming conventions, see `testing-strategy.md`. For logging library choice, see `logging-and-observability-principles.md`.
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **Always return errors — never panic in library or business code**
|
||||
- `panic` is reserved for truly unrecoverable states (programmer errors, nil dereference)
|
||||
- Use `recover` only at top-level goroutine boundaries (middleware, server startup)
|
||||
|
||||
2. **Wrap errors with context using `%w`**
|
||||
```go
|
||||
// ✅ Preserves the error chain for errors.Is / errors.As
|
||||
return fmt.Errorf("creating task for user %s: %w", userID, err)
|
||||
|
||||
// ❌ Loses the error chain
|
||||
return fmt.Errorf("creating task: %v", err)
|
||||
```
|
||||
|
||||
3. **Use sentinel errors for expected branch conditions**
|
||||
```go
|
||||
// Define in errors.go
|
||||
var ErrNotFound = errors.New("not found")
|
||||
var ErrUnauthorized = errors.New("unauthorized")
|
||||
|
||||
// Caller checks with errors.Is
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
// handle
|
||||
}
|
||||
```
|
||||
|
||||
4. **Use typed errors for rich domain errors**
|
||||
```go
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Caller unwraps with errors.As
|
||||
var ve *ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// access ve.Field, ve.Message
|
||||
}
|
||||
```
|
||||
|
||||
5. **Handle errors at the right level** — propagate upward until you have enough context to act on them; don't swallow or re-wrap the same error twice.
|
||||
|
||||
---
|
||||
|
||||
### Interfaces
|
||||
|
||||
1. **Keep interfaces small — one or two methods is ideal**
|
||||
```go
|
||||
// ✅ Focused, composable
|
||||
type Reader interface { Read(p []byte) (n int, err error) }
|
||||
type Writer interface { Write(p []byte) (n int, err error) }
|
||||
|
||||
// ❌ Monolithic
|
||||
type FileManager interface {
|
||||
Read(...); Write(...); Delete(...); List(...); Stat(...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **"Accept interfaces, return structs"**
|
||||
- Function parameters: accept interfaces for flexibility and testability
|
||||
- Return values: return concrete structs so callers can access all methods
|
||||
|
||||
3. **Define interfaces where they are *used*, not where they are *implemented***
|
||||
```go
|
||||
// ✅ Defined in the consumer package (task feature)
|
||||
// task/storage.go
|
||||
type Storage interface {
|
||||
GetByID(ctx context.Context, id string) (*Task, error)
|
||||
}
|
||||
|
||||
// postgres.go implements Storage — it does NOT define it
|
||||
```
|
||||
|
||||
4. **Implicit satisfaction is a feature — don't use embedding to "implement" interfaces**
|
||||
- Any type with the right method set satisfies an interface automatically
|
||||
- No `implements` keyword needed or wanted
|
||||
|
||||
---
|
||||
|
||||
### Goroutines and Channels
|
||||
|
||||
> For general concurrency principles (race conditions, deadlocks, message passing), see `concurrency-and-threading-principles.md`. This section covers Go-specific mechanics.
|
||||
|
||||
1. **Always pass `context.Context` as the first parameter**
|
||||
```go
|
||||
// ✅
|
||||
func (s *Service) GetTask(ctx context.Context, id string) (*Task, error)
|
||||
|
||||
// ❌ — no way to cancel or propagate deadlines
|
||||
func (s *Service) GetTask(id string) (*Task, error)
|
||||
```
|
||||
|
||||
2. **Never start a goroutine without knowing how it will stop**
|
||||
```go
|
||||
// ✅ Goroutine is bounded by context cancellation
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case item := <-ch:
|
||||
process(item)
|
||||
}
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Use `errgroup` for concurrent fan-out with error collection**
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.Go(func() error { return fetchUsers(ctx) })
|
||||
g.Go(func() error { return fetchOrders(ctx) })
|
||||
if err := g.Wait(); err != nil { ... }
|
||||
```
|
||||
|
||||
4. **Prefer channels for ownership transfer; mutexes for shared state**
|
||||
- Channel: "I'm handing this data to you"
|
||||
- Mutex: "We're both reading/writing this shared thing"
|
||||
|
||||
5. **Close channels from the sender, never the receiver**
|
||||
|
||||
---
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
1. **Receiver names: short, consistent, and the first letter of the type**
|
||||
```go
|
||||
func (s *Service) Create(...) {} // ✅
|
||||
func (svc *Service) Create(...) {} // ❌ — too verbose
|
||||
func (self *Service) Create(...) {} // ❌ — not Go
|
||||
```
|
||||
|
||||
2. **Package names: short, lowercase, no underscores, no plurals**
|
||||
```go
|
||||
package task // ✅
|
||||
package tasks // ❌ plural
|
||||
package task_service // ❌ underscore
|
||||
```
|
||||
|
||||
3. **Acronyms follow Go conventions (all caps or all lowercase)**
|
||||
```go
|
||||
userID // ✅
|
||||
userId // ❌
|
||||
HTTPClient // ✅
|
||||
HttpClient // ❌
|
||||
```
|
||||
|
||||
4. **Unexported identifiers omit the type name** — if it's private, keep it terse
|
||||
|
||||
5. **Don't stutter** — `task.Task` is fine; `task.TaskService` is not
|
||||
|
||||
---
|
||||
|
||||
### Idiomatic Patterns
|
||||
|
||||
1. **Functional options for optional configuration**
|
||||
```go
|
||||
type Option func(*Service)
|
||||
|
||||
func WithTimeout(d time.Duration) Option {
|
||||
return func(s *Service) { s.timeout = d }
|
||||
}
|
||||
|
||||
func NewService(store Storage, opts ...Option) *Service {
|
||||
s := &Service{store: store, timeout: 30 * time.Second}
|
||||
for _, o := range opts { o(s) }
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
2. **`defer` for cleanup — always use error-checked closures**
|
||||
|
||||
Every deferred cleanup call that returns an error MUST check and log the error.
|
||||
Never use bare `defer X.Close()` — the discarded error hides resource leak failures.
|
||||
|
||||
```go
|
||||
// ❌ NEVER: Error silently discarded
|
||||
defer rows.Close()
|
||||
|
||||
// ✅ ALWAYS: Error-checked closure with structured logging
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
if err != nil { return fmt.Errorf("querying tasks: %w", err) }
|
||||
defer func() {
|
||||
if err := rows.Close(); err != nil {
|
||||
slog.Warn("failed to close rows", "error", err, "operation", "ListTasks")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**Transaction rollback:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer tx.Rollback()
|
||||
|
||||
// ✅ ALWAYS: Guard against sql.ErrTxDone (already committed)
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil && !errors.Is(err, sql.ErrTxDone) {
|
||||
slog.Error("failed to rollback transaction", "error", err, "operation", "CreateOrder")
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
**HTTP response body:**
|
||||
```go
|
||||
// ❌ NEVER
|
||||
defer resp.Body.Close()
|
||||
|
||||
// ✅ ALWAYS: Drain then close (prevents connection reuse issues)
|
||||
defer func() {
|
||||
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
|
||||
slog.Warn("failed to drain response body", "error", err)
|
||||
}
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
slog.Warn("failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
3. **Avoid `init()` functions** — they run implicitly and make testing harder; prefer explicit initialization in `main` or constructors
|
||||
|
||||
4. **Prefer `struct` embedding over inheritance for code reuse**, but only when the embedded type truly represents an "is-a" relationship
|
||||
|
||||
5. **Use named return values only for documentation or `defer`-based cleanup** — never rely on naked returns in non-trivial functions
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
> Test file naming and pyramid proportions are defined in `testing-strategy.md`. This section covers Go-specific tooling only.
|
||||
|
||||
1. **Table-driven tests are the default pattern**
|
||||
```go
|
||||
func TestCalculateDiscount(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input float64
|
||||
expected float64
|
||||
wantErr bool
|
||||
}{
|
||||
{"zero items", 0, 0, false},
|
||||
{"negative input", -1, 0, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := calculateDiscount(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Use `testify` for assertions** (`require` for fatal assertions, `assert` for non-fatal)
|
||||
|
||||
3. **Run with the race detector in CI** — `go test -race ./...`
|
||||
|
||||
4. **Use `httptest.NewRecorder()` for HTTP handler tests** — no live server needed
|
||||
|
||||
5. **Test behaviour, not implementation** — assert on outputs and side effects, not internal field values
|
||||
|
||||
---
|
||||
|
||||
### Formatting and Static Analysis
|
||||
|
||||
All of the following **must pass with zero warnings/errors** before any commit. See `code-completion-mandate.md` for the full checklist.
|
||||
|
||||
| Tool | Purpose | Command |
|
||||
| ----------------------- | ------------------------ | -------------------- |
|
||||
| `gofumpt` / `goimports` | Canonical formatting | `gofumpt -l -w .` |
|
||||
| `go vet` | Correctness checks | `go vet ./...` |
|
||||
| `staticcheck` | Advanced static analysis | `staticcheck ./...` |
|
||||
| `gosec` | Security scanning | `gosec -quiet ./...` |
|
||||
| `golangci-lint` | Aggregated linter (CI) | `golangci-lint run` |
|
||||
| `govulncheck` | Dependency CVE scanning | `govulncheck ./...` |
|
||||
|
||||
- Never disable a linter without a comment explaining why
|
||||
- **`//nolint:errcheck` is NEVER acceptable.** If a function returns an error, handle it — even in `defer`. Use an error-checked closure (see § Idiomatic Patterns above). This is the #1 source of audit findings.
|
||||
- Other `//nolint:` directives require a `// NOLINT:` rationale comment AND must be approved during code review
|
||||
- Fast iteration during development: `go vet ./...` type-checks and catches correctness issues without producing binaries (analogous to `cargo check`) — reserve `golangci-lint` for pre-commit
|
||||
|
||||
> **Logging:** Never use `fmt.Println` or `log.Printf` in production service code — these produce unstructured output. Use `log/slog` (stdlib, Go 1.21+) or the project's chosen adapter. See `logging-and-observability-principles.md` for the required library and patterns.
|
||||
|
||||
---
|
||||
|
||||
### Related Principles
|
||||
- Code Idioms and Conventions @code-idioms-and-conventions.md
|
||||
- Project Structure — Go Backend @project-structure-go-backend.md
|
||||
- Testing Strategy @testing-strategy.md
|
||||
- Error Handling Principles @error-handling-principles.md
|
||||
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
|
||||
- Logging and Observability Principles @logging-and-observability-principles.md
|
||||
- Dependency Management Principles @dependency-management-principles.md
|
||||
Reference in new issue
Block a user