

Also from Kynth Studios


Also from Kynth Studios


Also from Kynth Studios
1# Go Coding Standards23## Project Requirements45- **Go Version**: 1.266- **Release Notes**: [https://go.dev/doc/go1.26](https://go.dev/doc/go1.26)78## Quick Reference910- Use `internal/errors` package (never standard `errors`)11- Structured logging with `internal/logging`12- Test with `-race` flag always13- No magic numbers - use constants14- Document all exports15- **Zero linter tolerance** - fix all issues before commit1617## Go 1.26 Features1819### Enhanced new() Function2021Use `new()` with expressions for pointer initialization:2223```go24// ✅ Go 1.26 - new() accepts expressions25existingFirstSeen: new(time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC))2627// ❌ Old pattern - wrapper functions unnecessary28func ptr[T any](v T) *T { return &v }29existingFirstSeen: ptr(time.Date(2025, 6, 15, 10, 0, 0, 0, time.UTC))30```3132### Green Tea GC (Default)3334- **10-40% reduction in GC overhead** (enabled by default)35- Additional 10% improvement on newer CPUs (Intel Ice Lake, AMD Zen 4+)36- Opt-out: `GOEXPERIMENT=nogreenteagc` (will be removed in Go 1.27)3738### Performance Improvements3940- **30% faster cgo calls** - reduced baseline overhead41- **Faster io.ReadAll()** - ~2x improvement with better allocation strategy42- **JPEG decoder/encoder** - new, faster, more accurate implementation43- **Reduced fmt allocations** for unformatted strings4445### Modern Standard Library4647- **strings.Cut()** - replaces `strings.Index` + slicing patterns:4849```go50 // ✅ Go 1.26 - cleaner and more efficient51 if host, port, found := strings.Cut(rawURL, ":"); found {52 return host53 }5455 // ❌ Old pattern56 if colonIdx := strings.Index(rawURL, ":"); colonIdx != -1 {57 return rawURL[:colonIdx]58 }59```6061- **errors.AsType()** - type-safe version of `errors.As()`:6263```go64 // ✅ Go 1.26 - type-safe, ~3x faster65 if pathErr, ok := errors.AsType[*fs.PathError](err); ok {66 // Use pathErr67 }6869 // ❌ Old pattern - slower, requires pointer setup70 var pathErr *fs.PathError71 if errors.As(err, &pathErr) {72 // Use pathErr73 }74```7576- **filepath.IsLocal()** - comprehensive path validation (Go 1.20+):7778 **IMPORTANT**: `filepath.IsLocal()` cleans paths internally! Use carefully based on context:7980```go81 // ✅ For FILE paths after cleaning - detects Windows reserved names, CVE-2023-45284/4528382 cleanPath := filepath.Clean(userInput)83 if !filepath.IsLocal(cleanPath) {84 return errors.New("invalid path")85 }8687 // ✅ For URL paths - explicit check needed (IsLocal cleans "path/../etc" → "etc" → valid!)88 if strings.Contains(urlPath, "..") {89 return errors.New("traversal attempt")90 }9192 // ❌ Wrong - misses Windows reserved names93 if strings.Contains(userPath, "..") {94 return errors.New("invalid path")95 }96```9798 **What filepath.IsLocal() detects**:99 - Windows reserved names: `COM1`, `LPT1`, `NUL`, etc.100 - Space-padded reserved names: `COM1` with trailing space (CVE-2023-45284)101 - Windows `\??\` prefix attacks (CVE-2023-45283)102 - Absolute paths and empty paths103104 **Use cases**:105 - File system operations AFTER cleaning106 - Validating final destination paths107 - NOT for detecting literal ".." in untrusted URLs108109- **net.JoinHostPort()** - IPv6-safe host:port formatting:110111```go112 // ✅ Correct - handles IPv6 addresses with brackets113 addr := net.JoinHostPort(host, strconv.Itoa(port))114 // Result: "[2001:4860:4860::8888]:443" for IPv6115116 // ❌ Wrong - breaks IPv6117 addr := fmt.Sprintf("%s:%d", host, port)118 // Result: "2001:4860:4860::8888:443" (invalid)119```120121- **reflect.Type.Fields()** - iterate struct fields (Go 1.26):122123```go124 // ✅ Go 1.26 - cleaner iteration125 for field := range structType.Fields() {126 // field is reflect.StructField127 }128129 // ❌ Old pattern130 for i := range structType.NumField() {131 field := structType.Field(i)132 }133```134135- **bytes.Buffer.Peek()** - read without advancing position136137### Preallocation Patterns138139Preallocate slice capacity when size is known:140141```go142// ✅ Preallocate capacity143fields := make([]logger.Field, 0, 5+len(extraFields))144fields = append(fields, baseFields...)145fields = append(fields, extraFields...)146147// ❌ Multiple reallocations148fields := []logger.Field{}149fields = append(fields, baseFields...)150fields = append(fields, extraFields...)151```152153### Security & Cryptography154155- **Post-quantum ML-KEM** enabled by default in crypto/tls156- **crypto/hpke** - Hybrid Public Key Encryption (RFC 9180)157- **Secure randomness** - Random parameter in crypto functions now ignored (always secure)158159## Import Rules160161- **Use** `"github.com/tphakala/birdnet-go/internal/errors"` (never standard `"errors"`)162- **Use** `internal/logging` for structured logging163- Specify `.Component()` and `.Category()` for telemetry164- Register new components in error package's `init()`165166## Error Handling167168- Wrap errors: `fmt.Errorf("operation failed: %w", err)`169- Use sentinel errors: `var ErrNotFound = errors.New("not found")`170- Log but continue on batch operation failures171- Provide detailed context in messages172173## Testing174175- **Prefer `testing/synctest` over `time.Sleep()`** (Go 1.25)176- `t.Parallel()` only for independent tests177- **Use `t.TempDir()` for scratch space** (auto-cleanup)178- **Use `t.ArtifactDir()` for test outputs to preserve** (Go 1.26+)179- Test with `go test -race`180- Table-driven tests with `t.Run()`181- `b.ResetTimer()` after benchmark setup182- Use `t.Attr()` for test metadata (Go 1.25)183184### Test Directory Guidelines (Go 1.26)185186- **`t.TempDir()`** - Temporary scratch space, auto-deleted after test187- **`t.ArtifactDir()`** - Preserved test outputs (with `-artifacts` flag)188- **Replace `os.MkdirTemp()` in tests** - use appropriate testing method189190```go191// ✅ Scratch space that should be cleaned up192func TestProcess(t *testing.T) {193 tmpDir := t.TempDir() // auto-deleted194 processFiles(tmpDir)195}196197// ✅ Test artifacts to preserve (logs, debug output)198func TestAnalysis(t *testing.T) {199 outDir := t.ArtifactDir() // preserved with -artifacts flag200 writeDebugLog(outDir + "/debug.log")201}202203// ❌ Manual temp dirs - no auto-cleanup204tmpDir, _ := os.MkdirTemp("", "test_*")205```206207### Test Cleanup Best Practices208209- **Use `t.Cleanup()` instead of `defer`** for test resource cleanup210- `t.Cleanup()` runs after all defers, providing more predictable cleanup order211- Particularly important for tests that restore global state212- Example:213214```go215 func TestWithGlobalState(t *testing.T) {216 // ❌ Wrong - defer may run at unpredictable times217 originalValue := GetGlobalValue()218 defer SetGlobalValue(originalValue)219220 // ✅ Correct - cleanup runs after all test defers221 originalValue := GetGlobalValue()222 t.Cleanup(func() {223 SetGlobalValue(originalValue)224 })225 }226```227228### Test Parallelization Guidelines229230- Add `t.Parallel()` to **test functions** and **subtests** for speed231- **NEVER parallelize tests that**:232 - Mutate global state (e.g., `conftest.SetTestSettings()`)233 - Share mutable data structures without synchronization234 - Use shared map references without cloning235- **Always clone shared test data** in subtests:236237```go238 import "maps"239240 customSeasons := map[string]Season{...}241 for _, tt := range tests {242 t.Run(tt.name, func(t *testing.T) {243 t.Parallel()244 // Clone to avoid aliasing - prevents false positives245 settings.Seasons = maps.Clone(customSeasons)246 })247 }248```249250### Test Helper File Naming251252- Name test-only helper files with `_test.go` suffix253- **Wrong**: `internal/foo/test_helpers.go` (included in production builds)254- **Correct (package-local helpers)**: `internal/foo/test_helpers_test.go` (test-only)255- **Correct (helpers shared across packages' tests)**: a dedicated testing-support subpackage, e.g. `internal/conf/conftest` or `internal/api/v2/apitest`. A `_test.go` file cannot be imported by other packages, so cross-package helpers must live in their own importable package that production code never imports.256- This ensures helpers with `*testing.T` parameters (and the `testing` import) don't bloat production binaries257258### Benchmark Best Practices259260- Always call `b.ReportAllocs()` before `b.ResetTimer()` to track allocations261- Use `b.Loop()` (Go 1.24+) for cleaner benchmark loops (optional)262- Benchmark example:263264```go265 func BenchmarkValidation(b *testing.B) {266 cfg := &Config{...}267 b.ReportAllocs()268 b.ResetTimer()269 for i := 0; i < b.N; i++ {270 _ = Validate(cfg)271 }272 }273```274275### Mock Generation with Mockery276277**IMPORTANT**: Never manually write mocks. Use mockery for automated mock generation.278279**Quick Start:**280281```bash282# Generate mocks for all interfaces283go generate ./internal/datastore284285# Or use mockery directly286mockery --config .mockery.yaml287```288289**Using Generated Mocks in Tests:**290291```go292import (293 "testing"294 "github.com/stretchr/testify/mock"295 "github.com/tphakala/birdnet-go/internal/datastore/mocks"296)297298func TestMyFunction(t *testing.T) {299 // Create mock300 mockDS := mocks.NewMockInterface(t)301302 // Set expectations using .EXPECT() pattern303 mockDS.EXPECT().304 Save(mock.Anything, mock.Anything).305 Return(nil).306 Once()307308 // Use the mock309 err := myFunction(mockDS)310311 // Assertions happen automatically312}313```314315**Critical Rules:**316317- **Conditional Mock Calls**: Use `.Maybe()` for methods called conditionally318319```go320// Method only called when NotificationSuppressionHours > 0321mockDS.EXPECT().322 GetActiveNotificationHistory(mock.AnythingOfType("time.Time")).323 Return([]datastore.NotificationHistory{}, nil).324 Maybe() // Won't fail if not called325```326327- **Async Operations**: Use `.Maybe()` for methods called in goroutines328329```go330// Called asynchronously in RecordNotificationSent331mockDS.EXPECT().332 SaveNotificationHistory(mock.AnythingOfType("*datastore.NotificationHistory")).333 Return(nil).334 Maybe() // Non-blocking operation335```336337- **Test Helpers**: Always use `t.Helper()` in setup functions338339```go340func createTestTracker(t *testing.T) *Tracker {341 t.Helper() // Stack traces point to caller, not this function342 // ... setup343}344```345346**Common Patterns:**347348```go349// Match any argument type350mockDS.EXPECT().Get(mock.Anything).Return(note, nil)351352// Match specific type353mockDS.EXPECT().Save(mock.AnythingOfType("*datastore.Note")).Return(nil)354355// Multiple calls356mockDS.EXPECT().Get(mock.Anything).Return(note, nil).Times(3)357358// Return different values on subsequent calls359mockDS.EXPECT().Get("123").Return(note1, nil).Once()360mockDS.EXPECT().Get("123").Return(note2, nil).Once()361```362363**When Interface Changes:**3643651. Update the interface in `internal/datastore/interfaces.go`3662. Run `go generate ./internal/datastore`3673. Mocks automatically regenerate with all methods3684. **Never** manually edit files in `internal/datastore/mocks/`369370**Documentation:**371372- Complete guide: `internal/datastore/mocks/README.md`373- Configuration: `.mockery.yaml`374- Migration guide and examples in README375376## Go 1.25 Testing Features377378See [Go 1.25 Release Notes](https://go.dev/doc/go1.25) for complete changelog.379380### testing/synctest - Deterministic Concurrent Testing381382Replace flaky sleep-based tests with deterministic scheduling:383384```go385// ❌ Old pattern - unreliable timing386time.Sleep(100 * time.Millisecond)387388// ✅ New pattern - deterministic389import "testing/synctest"390391func TestConcurrent(t *testing.T) {392 synctest.Test(t, func() {393 // Time moves instantly when all goroutines are blocked394 // Perfect for testing timeouts, retries, rate limiting395 })396}397```398399### sync.WaitGroup.Go() - Cleaner Goroutines400401```go402// ❌ Old pattern403var wg sync.WaitGroup404wg.Add(1)405go func() {406 defer wg.Done()407 // work408}()409410// ✅ New pattern - automatic Add/Done411var wg sync.WaitGroup412wg.Go(func() {413 // work414})415```416417### Test Output & Attributes418419```go420func TestAPI(t *testing.T) {421 // Add test metadata422 t.Attr("component", "api")423 t.Attr("version", "v2")424425 // Structured output426 output := t.Output()427 fmt.Fprintf(output, "Request: %v\n", req)428}429```430431### runtime/trace.FlightRecorder - Production Diagnostics432433Capture lightweight traces only when needed:434435```go436import "runtime/trace"437438recorder := trace.NewFlightRecorder()439defer recorder.Stop()440441// Process audio/data442if err != nil {443 // Save trace only on error444 recorder.WriteTo(errorLog)445}446```447448### encoding/json/v2 (Experimental)449450For performance-critical JSON operations:451452```go453import jsonv2 "encoding/json/v2"454455// ~2x faster for API responses456data, err := jsonv2.Marshal(response)457```458459## Benchmarks (Go 1.25)460461- Use `b.Loop()` instead of manual `for i := 0; i < b.N; i++`462- Use `b.TempDir()` instead of `os.MkdirTemp()`463- Call `b.ReportAllocs()` to track memory allocations464- Container-aware GOMAXPROCS adjusts CPU automatically465466## Modern Go (1.25+)467468- `any` not `interface{}`469- `for i := range n` for loops470- Pre-compile regex at package level471- Store interfaces in `atomic.Value` directly472- Use `os.Root` for filesystem sandboxing (<https://go.dev/blog/osroot>)473- Use `sync.WaitGroup.Go()` for goroutines (<https://pkg.go.dev/sync#WaitGroup.Go>)474- Use `testing/synctest` for concurrent tests (<https://go.dev/blog/synctest>)475476## Standard Library First477478- URLs: `url.Parse()`479- IPs: `net.ParseIP()`, `ip.IsPrivate()`480- Paths: `filepath.Join()`, `filepath.Clean()`481- Never manual string parsing for these482483## Common Patterns484485- Safe type assertions: `if v, ok := x.(Type); ok { }`486- Avoid circular dependencies in init487- Accept interfaces, return concrete types488- Copy data under read lock (RWMutex)489- Chain contexts properly490- Use dependency injection491- Document all exports: `// TypeName does...`492493## Dependency Injection for Testability494495- **Pass dependencies as interfaces** through constructors or struct fields496- **Avoid global state** - inject configuration, loggers, and clients497- **Define minimal interfaces** close to where they're used498- **Constructor pattern**: `NewService(deps...) *Service`499- **Identify untestable code** - if you see direct instantiation of external dependencies, flag it500- **Example pattern**:501502```go503 type Store interface {504 Get(id string) (*Item, error)505 }506507 type Service struct {508 store Store // inject interface, not concrete type509 }510511 func NewService(store Store) *Service {512 return &Service{store: store}513 }514```515516- **Common injection targets**: databases, HTTP clients, file systems, time providers517- **If you encounter code that would benefit from DI**, communicate it rather than leaving it untestable518519## Security520521- Validate all user input522- Check path traversal, injection attacks523- Validate UUIDs properly524525## Goroutine Leak Detection526527Add to tests that create services/goroutines:528529```go530defer goleak.VerifyNone(t,531 goleak.IgnoreTopFunction("testing.(*T).Run"),532 goleak.IgnoreTopFunction("runtime.gopark"),533 goleak.IgnoreTopFunction("gopkg.in/natefinch/lumberjack%2ev2.(*Logger).millRun"),534)535```536537- Always `defer service.Stop()` after creating services538- Use local service instances, not global singletons539- Use 500ms+ timeouts for async operations (CI reliability)540541## Linter Compliance (Zero Tolerance)542543### Active Linters & Common Fixes544545| Linter | Purpose | Common Fixes |546| ---------------- | ----------------------- | -------------------------------------------- |547| **errorlint** | Error handling | Use `errors.Is()`, `errors.As()` not `==` |548| **errname** | Error naming | Prefix errors with `Err`: `var ErrNotFound` |549| **nilerr** | Nil error returns | Don't return nil error with non-nil value |550| **nilnil** | Nil returns | Avoid `return nil, nil` - return zero value |551| **bodyclose** | HTTP bodies | Always `defer resp.Body.Close()` |552| **ineffassign** | Unused assignments | Remove or use assigned values |553| **staticcheck** | Static analysis | Fix all SA\* warnings |554| **gocritic** | Style/performance | Follow suggestions (rangeValCopy, etc.) |555| **gocognit** | Complexity | Split functions >50 complexity |556| **gocyclo** | Cyclomatic complexity | Refactor complex functions |557| **dupl** | Duplication | Extract common code |558| **misspell** | Spelling | Fix typos in comments/strings |559| **unconvert** | Unnecessary conversions | Remove redundant type conversions |560| **wastedassign** | Wasted assignments | Remove unused assignments |561| **prealloc** | Slice preallocation | Use `make([]T, 0, cap)` when size known |562| **exhaustive** | Switch exhaustiveness | Handle all enum cases or add default |563| **testifylint** | Testify usage | Use `assert.Equal` not `assert.True(a == b)` |564| **thelper** | Test helpers | Add `t.Helper()` to test functions |565| **fatcontext** | Context usage | Don't store context in structs |566| **iface** | Interface pollution | Accept interfaces, return structs |567568### Common Fixes by Category569570#### Error Handling571572```go573// ❌ Wrong574if err == io.EOF { }575576// ✅ Correct577if errors.Is(err, io.EOF) { }578579// ❌ Wrong - nilerr580if err != nil {581 return nil, nil582}583584// ✅ Correct585if err != nil {586 return nil, err587}588```589590#### Resource Management591592```go593// ❌ Wrong - bodyclose594resp, _ := http.Get(url)595596// ✅ Correct597resp, err := http.Get(url)598if err != nil {599 return err600}601defer resp.Body.Close()602```603604#### Test Helpers605606```go607// ❌ Wrong - thelper608func assertSomething(t *testing.T, val int) {609 if val != 42 {610 t.Errorf("expected 42")611 }612}613614// ✅ Correct615func assertSomething(t *testing.T, val int) {616 t.Helper() // Add this617 if val != 42 {618 t.Errorf("expected 42")619 }620}621```622623#### Performance624625```go626// ❌ Wrong - prealloc627var results []string628for _, item := range items {629 results = append(results, item)630}631632// ✅ Correct633results := make([]string, 0, len(items))634for _, item := range items {635 results = append(results, item)636}637```638639## Pre-Commit Checklist640641- [ ] Run `golangci-lint run -v` - **MUST have zero errors**642 - **Always run on full project** - never single files/packages (incomplete results)643 - **Primary compilation validation** - don't run `go build` separately644- [ ] Run `go test -race -v`645- [ ] Check all linter categories above646- [ ] No disabled linters with `//nolint` without justification647- [ ] Document all exports648- [ ] Handle all errors properly649650## Linter Configuration Notes651652- Config: `.golangci.yaml` (v2 format)653- Complexity threshold: 50 (gocognit)654- Disabled checks: commentFormatting, commentedOutCode (gocritic)655- Exhaustive switches: `default` case marks as exhaustive656- gosec disabled but configured for future use657
One repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| tphakala/birdnet-go.cursor/rules/database.mdc · 1.6k | Cursor rules | databasedo-not | 45/100 | today | |
| tphakala/birdnet-go.cursor/rules/frontend.mdc · 1.6k | Cursor rules | dependenciesuido-not | 61/100 | today | |
| tphakala/birdnet-go.cursor/rules/go.mdc · 1.6k | Cursor rules | buildteststylearch+5 | 69/100 | today | |
| tphakala/birdnet-go.cursor/rules/go_test.mdc · 1.6k | Cursor rules | setupteststyletesting-strategy+1 | 56/100 | today | |
| tphakala/birdnet-goAGENTS.md · 1.6k | AGENTS.md | teststylegitdo-not+1 | 78/100 | today | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tphakala/birdnet-gofrontend/CLAUDE.md · 1.6k | CLAUDE.md | setuptestlint-formatstyle+7 | 84/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/components/CLAUDE.md · 1.6k | CLAUDE.md | teststylearchui | 70/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/components/ui/CLAUDE.md · 1.6k | CLAUDE.md | styleuidocs | 54/100 | today | |
| tphakala/birdnet-gofrontend/src/lib/desktop/features/settings/CLAUDE.md · 1.6k | CLAUDE.md | buildstylearchtypes+2 | 66/100 | today | |
| tphakala/birdnet-gofrontend/static/messages/CLAUDE.md · 1.6k | CLAUDE.md | archuido-notagent-behaviour | 67/100 | today | |
| tphakala/birdnet-gofrontend/tools/CLAUDE.md · 1.6k | CLAUDE.md | no sections | 65/100 | today | |
| tphakala/birdnet-gointernal/api/v2/CLAUDE.md · 1.6k | CLAUDE.md | teststylesecurityapi+1 | 84/100 | today | |
| tphakala/birdnet-gointernal/errors/CLAUDE.md · 1.6k | CLAUDE.md | styleuiperformancedo-not+1 | 61/100 | today |
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| stacklok/toolhiveCLAUDE.md · 2.0k | CLAUDE.md | buildteststylearch+4 | 100/100 | 14 days ago | |
| Adit-Jain-srm/NightmareNetCLAUDE.md · 46 | CLAUDE.md | buildtestlint-formatstyle+6 | 100/100 | 14 days ago | |
| microsoft/playwrightCLAUDE.md · 95k | CLAUDE.md | buildtestlint-formatstyle+7 | 100/100 | 7 days ago | |
| nimbalyst/nimbalystpackages/android/CLAUDE.md · 1.5k | CLAUDE.md | setupbuildstylearch+2 | 100/100 | 14 days ago | |
| dotCMS/corecore-web/CLAUDE.md · 949 | CLAUDE.md | teststylearchtesting-strategy+3 | 100/100 | 14 days ago | |
| tphakala/birdnet-goCLAUDE.md · 1.6k | CLAUDE.md | buildtestlint-formatstyle+8 | 100/100 | today | |
| tyrchen/geektime-bootcamp-aiw7/genslides/backend/CLAUDE.md · 230 | CLAUDE.md | testlint-formatstylearch+6 | 100/100 | 9 days ago | |
| bagisto/bagistoCLAUDE.md · 28k | CLAUDE.md | setupbuildteststyle+5 | 100/100 | 7 days ago |
A badge carrying the measured quality of the strongest agent config file in this repository, out of 100. It reads from this index every time somebody loads your page, so it changes when the measurement changes and there is nothing to keep up to date. Free, no account, and the value is not something you or we can set by hand.
[](https://rulestack.kynth.studio/configs/tphakala-birdnet-go-internal-claude)Would rather not hotlink us? Every badge is also served in shields.io’s endpoint schema, so shields renders the image and your readers never talk to our domain:
Published by Toolproof, the masthead over this index and eight others. The method behind the number is at toolproof.kynth.studio/methodology, and the whole thing is readable as JSON with no key at /api.