- Add principle 27: 'Language-Agnostic Auto-Trigger' to engineering-principles - Remove hardcoded English-only keyword lists from all 26 skill descriptions - Replace with concept-based detection: triggers from code context, project files, and file types regardless of spoken language - Update detect-project.sh hook output with language-agnostic message
6.9 KiB
6.9 KiB
name, description
| name | description |
|---|---|
| go | Go best practices — idiomatic Go, project layout, error handling, interfaces, concurrency, testing, and dependency management. Use when writing Go code, reviewing Go projects. Triggers when working with this language's files, regardless of spoken language." |
Go Best Practices
Project Layout
project/
├── cmd/
│ └── server/
│ └── main.go # Entry point
├── internal/
│ ├── domain/ # Business entities, value objects
│ ├── application/ # Use cases, ports
│ ├── infrastructure/ # DB, HTTP, cache adapters
│ └── api/ # HTTP handlers, middleware
├── pkg/ # Public library code (for external consumption)
├── tests/ # Integration/E2E tests
├── go.mod
├── go.sum
└── Makefile
cmd/— one directory per binary. Small main function, defer to package.internal/— not importable outside this module. Clean architecture layering.pkg/— safe for external packages to import.
Idiomatic Go
// Named returns are OK but prefer bare in simple cases
func Parse(input string) (result Result, err error) {
if input == "" {
return result, errors.New("empty input")
}
result.Value = input
return
}
Rules:
- Favor files over functions — one logical unit per file, not one function per file.
- Short variable names —
ctx,r(request),w(response writer),cfg(config). - Getters don't have
Getprefix —user.Name()notuser.GetName(). - Zero-value is useful — initialize your structs so zero value is usable.
gofmtis the law — no style debates. Rungofmt(orgofumpt).go vetbefore commit — catches subtle bugs.
Error Handling
// Always handle errors. No discard.
result, err := doSomething()
if err != nil {
return fmt.Errorf("doing something: %w", err) // wrap with context
}
// Sentinel errors for specific cases
var ErrNotFound = errors.New("not found")
// Custom error types for additional context
type ValidationError struct {
Field string
Err error
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %v", e.Field, e.Err)
}
func (e *ValidationError) Unwrap() error { return e.Err }
- Error wrapping —
fmt.Errorf("context: %w", err)for the call chain. errors.Is()for sentinel errors.errors.As()for custom types.- Don't use
_to discard errors — unless truly intentional (e.g.fmt.Fprint). - Defer for cleanup —
defer file.Close(),defer mu.Unlock().
Interfaces
// Define interfaces in the consumer package (application/domain), not producer
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
// Small interfaces are Go's superpower
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }
- Accept interfaces, return structs — consumer declares what it needs.
- Interface satisfaction is implicit — no
implementskeyword. - Prefer single-method interfaces (IO pattern).
- Don't export interfaces:
Concurrency
// Goroutines + channels for communication
func processBatch(ctx context.Context, items []Item) error {
results := make(chan Result, len(items))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Fan-out
for _, item := range items {
item := item // copy for closure
go func() {
select {
case results <- process(item):
case <-ctx.Done():
}
}()
}
// Fan-in
for range items {
select {
case r := <-results:
if r.err != nil { return r.err }
case <-ctx.Done():
return ctx.Err()
}
}
return nil
}
// sync.WaitGroup for waiting
var wg sync.WaitGroup
for _, v := range items {
wg.Add(1)
go func(v Item) {
defer wg.Done()
process(v)
}(v)
}
wg.Wait()
- Don't communicate by sharing memory; share memory by communicating.
- Channel or mutex? — channel when passing ownership or signaling; mutex for shared state.
- Context first param —
ctx context.Contextis always the first function parameter. - Never start goroutines without knowing when they stop.
- Use
errgroupfor concurrent operations that share context.
Testing
func TestCreateUser(t *testing.T) {
t.Parallel()
// Table-driven tests
tests := []struct {
name string
input CreateUserInput
wantErr bool
}{
{name: "valid user", input: CreateUserInput{Email: "a@b.com"}, wantErr: false},
{name: "empty email", input: CreateUserInput{}, wantErr: true},
}
for _, tt := range tests {
tt := tt // capture for t.Parallel
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repo := NewInMemoryUserRepo() // fake
svc := NewUserService(repo)
err := svc.CreateUser(context.Background(), tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("got err = %v, wantErr = %v", err, tt.wantErr)
}
})
}
}
func BenchmarkCreateUser(b *testing.B) {
for b.Loop() { // go 1.24+
repo := NewInMemoryUserRepo()
svc := NewUserService(repo)
if err := svc.CreateUser(context.Background(), validInput); err != nil {
b.Fatal(err)
}
}
}
testing/slogfor test logging.testing/fstestfor filesystem fakes.- Fakes over mocks — in-memory implementations are simpler and more maintainable.
testifyis common but stdlibtestinghas improved — prefergot/wantcomparison.
Standard Library First
Go's stdlib is extensive. Reach for it before frameworks:
net/http— for basic HTTP servers (chi/echo for routing, but stdlib for the basics).encoding/json— JSON serialization (better than most third-party libs).database/sql— database access (withpgxfor Postgres).testing— built-in test runner, fuzzing, benchmarks.context— cancellation, deadlines, request-scoped values.slog— structured logging (go 1.21+).
Anti-patterns
- ❌
context.Background()in handlers — usereq.Context() - ❌ Global variables — pass dependencies explicitly
- ❌
init()functions — they break control flow and test isolation - ❌
_ = foo()— silent error discard; handle or at least log - ❌ Embedding HTTP server logic —
ListenAndServeinmain.go, not hidden in a library - ❌ Deeply nested if-err chains — flatten with early returns
- ❌
interface{}/anywhere concrete type works