plugins/dev/skills/backend/golang/SKILL.md
Use when building Go backend services, implementing goroutines/channels, handling errors idiomatically, writing tests with testify, or following Go best practices for APIs/CLI tools.
npx skillsauth add madappgang/claude-code golangInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Version: 2.0.0 Purpose: Comprehensive Go development patterns covering idioms, error handling, concurrency, testing, and quality Scope: Backend development with Go - API services, CLI tools, system software Prerequisites: Basic Go syntax knowledge
Go (Golang) is designed for simplicity, explicit error handling, and safe concurrent programming. This skill covers production-ready patterns validated by the Go community, official documentation, and industry standards (Uber Engineering, Google).
Core Philosophy:
Key Design Principles:
Package Names:
// ✅ GOOD: Package names are single lowercase identifiers
// Import path: "net/url" → package name: url
// Import path: "encoding/json" → package name: json
package url // from "net/url"
package json // from "encoding/json"
package strings
// ❌ BAD
package urls // No plural
package encodingjson // Don't smash words together
package stringutils // Too verbose
Getters and Setters:
type Account struct {
balance int
}
// ✅ GOOD: No "Get" prefix
func (a *Account) Balance() int {
return a.balance
}
func (a *Account) SetBalance(amount int) {
a.balance = amount
}
// ❌ BAD: Java-style getters
func (a *Account) GetBalance() int {
return a.balance
}
Error Variables:
// Exported sentinel errors (capitalized)
var ErrNotFound = errors.New("not found")
var ErrTimeout = errors.New("timeout")
// Unexported internal errors (lowercase)
var errInternal = errors.New("internal error")
Interface Naming:
// ✅ GOOD: Short, descriptive
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ❌ BAD: Verbose or unclear
type DataReader interface { ... }
type IReader interface { ... } // No "I" prefix
Core Principle: Small, consumer-side interfaces provide maximum flexibility.
Single-Method Interfaces (Ideal):
// Standard library examples
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Compose interfaces
type ReadCloser interface {
Reader
Closer
}
Consumer-Side Interface Placement:
// ❌ WRONG: Producer defines interface
package store
type CustomerStorage interface {
StoreCustomer(Customer) error
GetCustomer(string) (Customer, error)
UpdateCustomer(Customer) error
// 10+ methods...
}
type PostgresStore struct {}
func (s *PostgresStore) StoreCustomer(...) { ... }
// ✅ CORRECT: Consumer defines what it needs
package client
type customerGetter interface {
GetCustomer(string) (store.Customer, error)
}
func ProcessCustomer(cg customerGetter) {
customer, _ := cg.GetCustomer("123")
// Only depends on GetCustomer method
}
Return Concrete Types, Accept Interfaces (Postel's Law):
// ✅ GOOD
func NewStore() *PostgresStore {
return &PostgresStore{}
}
func Process(storage CustomerStorage) error {
// Accepts interface
}
// ❌ BAD: Returning interface
func NewStore() CustomerStorage {
return &PostgresStore{}
}
When to Create Interfaces:
Core Principle: Align success path to left margin, handle errors first.
// ❌ BAD: Deep nesting
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
} else {
if s2 == "" {
return "", errors.New("s2 is empty")
} else {
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
} else {
if len(concat) > max {
return concat[:max], nil
} else {
return concat, nil
}
}
}
}
}
// ✅ GOOD: Happy path aligned left
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
}
if s2 == "" {
return "", errors.New("s2 is empty")
}
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
}
if len(concat) > max {
return concat[:max], nil
}
return concat, nil
}
Guidelines:
else blocks when if returnsType Embedding (Struct Composition):
// Embedding for method promotion
type Logger struct {
*log.Logger
prefix string
}
func NewLogger(prefix string) *Logger {
return &Logger{
Logger: log.New(os.Stdout, "", 0),
prefix: prefix,
}
}
// Logger methods automatically available
logger := NewLogger("APP")
logger.Println("message") // Calls embedded log.Logger.Println
Interface Composition:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Compose interfaces
type ReadCloser interface {
Reader
Closer
}
Warning: Avoid embedding in public APIs:
// ❌ BAD: Exposes implementation details
type MyHandler struct {
http.Handler // Leaks all Handler methods
}
// ✅ GOOD: Explicit delegation
type MyHandler struct {
handler http.Handler
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Custom logic
h.handler.ServeHTTP(w, r)
}
Defer for Cleanup:
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // Guaranteed cleanup
// Multiple returns, all close file
if condition {
return nil // File closed
}
return process(f) // File closed
}
// Mutex pattern
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++ // All paths unlock
}
Critical Rule: Call defer AFTER checking error:
// ❌ WRONG
defer f.Close() // f is nil if Open failed
f, err := os.Open(path)
// ✅ CORRECT
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
Multiple Return Values:
// (value, error) - Standard error handling
func GetUser(id string) (*User, error) {
// ...
}
// (value, bool) - "comma ok" idiom
value, ok := myMap[key]
if !ok {
// key not found
}
result, ok := someValue.(TargetType)
if !ok {
// type assertion failed
}
data, ok := <-channel
if !ok {
// channel closed
}
Blank Identifier _:
// Ignore unwanted values
_, err := os.Open(filename)
// Compile-time interface check
var _ http.Handler = (*MyHandler)(nil)
// Import for side effects
import _ "net/http/pprof"
Useful Zero Values:
// sync.Mutex - ready to use
var mu sync.Mutex
mu.Lock() // Works immediately
// bytes.Buffer - valid empty buffer
var buf bytes.Buffer
buf.WriteString("hello") // No initialization needed
// Slices - safe to read
var s []int
fmt.Println(len(s)) // 0 (safe)
Core Pattern: Wrap errors with context using fmt.Errorf and %w.
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
// Wrap with context using %w
return fmt.Errorf("failed to open file %s: %w", path, err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}
return processData(data)
}
// Result when error bubbles up:
// "failed to initialize: failed to open file config.json: open config.json: no such file or directory"
Checking Wrapped Errors:
// errors.Is - Check for specific error in chain
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File doesn't exist")
}
// errors.As - Extract specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error on: %s\n", pathErr.Path)
}
Critical: Use %w, NOT %v:
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)
// ✅ CORRECT: Preserves chain
return fmt.Errorf("failed: %w", err)
Sentinel Errors (Package-Level Variables):
package db
var (
ErrConnectionFailed = errors.New("database connection failed")
ErrRecordNotFound = errors.New("record not found")
ErrDuplicateKey = errors.New("duplicate key violation")
)
func GetUser(id int) (*User, error) {
// ...
if notFound {
return nil, ErrRecordNotFound
}
return user, nil
}
// Caller checks with errors.Is
user, err := db.GetUser(123)
if errors.Is(err, db.ErrRecordNotFound) {
// Handle not found
}
Custom Error Types (Rich Context):
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for field '%s': %s (value: %v)",
e.Field, e.Message, e.Value)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Value: age,
Message: "must be non-negative",
}
}
return nil
}
// Caller extracts rich information
if err := validateAge(-5); err != nil {
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field: %s, Value: %v\n", valErr.Field, valErr.Value)
}
}
Decision Guide:
Important: Use pointer receivers for error types:
// ✅ CORRECT: Pointer receiver
func (e *ValidationError) Error() string { ... }
// ❌ WRONG: Value receiver (breaks errors.As)
func (e ValidationError) Error() string { ... }
Core Principle: Either log the error OR return it, not both.
// ❌ BAD: Handle twice (log AND return)
if err != nil {
log.Printf("error: %v", err) // Logged here
return err // And returned
}
// ✅ GOOD: Return error, let caller handle
if err != nil {
return fmt.Errorf("process: %w", err)
}
// ✅ GOOD: Log and handle completely
if err != nil {
log.Printf("non-fatal error: %v", err)
// Continue execution (error handled)
}
Error Message Conventions:
// ✅ GOOD
var ErrNotFound = errors.New("configuration file not found")
return fmt.Errorf("failed to read settings for user %d: %w", userID, err)
// ❌ BAD
var ErrNotFound = errors.New("Error: Configuration file not found.") // No prefix, no punctuation
return fmt.Errorf("Error occurred: %v", err) // Too generic
Is this condition expected during normal operation?
├─ Yes → Return error
└─ No → Is this a programmer error?
├─ Yes → Panic (with clear message)
└─ No → Is the program in an invalid state?
├─ Yes → Panic
└─ No → Return error
Use Errors When:
// Expected failures
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
return parseConfig(data)
}
// Business logic failures
func createUser(email string) error {
if !isValidEmail(email) {
return fmt.Errorf("invalid email format: %s", email)
}
return nil
}
Use Panic When:
// Nil argument (programmer error, document this!)
func ProcessData(data *Data) {
if data == nil {
panic("ProcessData: data argument must not be nil")
}
// ...
}
// Initialization failure
func init() {
cfg, err := loadConfig()
if err != nil {
panic(fmt.Sprintf("fatal: failed to load config: %v", err))
}
globalConfig = cfg
}
// Impossible condition (indicates bug)
func (sm *StateMachine) transition(event Event) {
newState := sm.computeNextState(event)
if !sm.isValidTransition(newState) {
panic(fmt.Sprintf("BUG: invalid state transition from %v to %v",
sm.currentState, newState))
}
sm.currentState = newState
}
Recovery (Use Sparingly):
// HTTP server recovering from handler panics
func safeHandler(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("Handler panic: %v\n%s", rec, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
h(w, r)
}
}
Pattern 1: errgroup (Coordinated Goroutines):
import "golang.org/x/sync/errgroup"
func processFiles(ctx context.Context, files []string) error {
g, ctx := errgroup.WithContext(ctx)
for _, file := range files {
file := file // Capture loop variable
g.Go(func() error {
return processFile(ctx, file)
})
}
// Wait for all, return first error
if err := g.Wait(); err != nil {
return fmt.Errorf("file processing failed: %w", err)
}
return nil
}
Pattern 2: Error Channel (Collect All Errors):
func processAll(items []Item) []error {
errChan := make(chan error, len(items))
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
if err := process(i); err != nil {
errChan <- err
}
}(item)
}
go func() {
wg.Wait()
close(errChan)
}()
var errs []error
for err := range errChan {
errs = append(errs, err)
}
return errs
}
Core Principle: Every goroutine must have an explicit termination mechanism.
Pattern: Context Cancellation + WaitGroup:
func runWorkers(ctx context.Context, n int) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return // Clean exit
default:
doWork(id)
}
}
}(i)
}
wg.Wait() // Wait for all goroutines
}
// Usage
ctx, cancel := context.WithCancel(context.Background())
go runWorkers(ctx, 10)
// Later: stop all workers
cancel()
Common Leak: Unbuffered Channel Send:
// ❌ LEAK: Goroutine blocks forever if no receiver
func leak() {
ch := make(chan int)
go func() {
ch <- 42 // Blocks forever
}()
// Function returns, goroutine leaked
}
// ✅ FIX: Buffered channel or ensure receiver
func fixed() {
ch := make(chan int, 1) // Buffer size 1
go func() {
ch <- 42 // Won't block
}()
}
Unbuffered vs Buffered Semantics:
// Unbuffered: Synchronous handoff
done := make(chan bool)
go func() {
doWork()
done <- true // Blocks until main receives
}()
<-done // Guaranteed: work completed
// Buffered: Asynchronous
jobs := make(chan Job, 100)
for w := 0; w < numWorkers; w++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
Channel Closing Rules:
// ✅ GOOD: Only sender closes
jobs := make(chan Job)
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs) // Signal: no more jobs
}()
for job := range jobs {
process(job) // Exits when channel closed
}
// ❌ NEVER: Close from receiver
// ❌ NEVER: Close closed channel (panics)
// ❌ NEVER: Send on closed channel (panics)
Select Pattern for Cancellation:
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case job := <-jobs:
process(job)
case <-ctx.Done():
return // Cancel signal
}
}
}
Context Types:
// Root contexts
ctx := context.Background() // Main/init
ctx := context.TODO() // Placeholder
// Cancellation
ctx, cancel := context.WithCancel(parent)
defer cancel() // Always call
// Timeout
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
// Deadline
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()
// Values (use sparingly, only for request-scoped data)
ctx = context.WithValue(parent, key, value)
Best Practices:
// ✅ GOOD: Context as first parameter
func makeRequest(ctx context.Context, url string) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
if err != nil {
return err // Returns context.DeadlineExceeded on timeout
}
defer resp.Body.Close()
return nil
}
// Check cancellation in loops
func longRunning(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
processChunk()
}
}
}
Context Rules:
func Do(ctx context.Context, ...)WithValue only for request-scoped data, not optionssync.Mutex:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
sync.RWMutex (Read-Heavy Workloads):
type Cache struct {
mu sync.RWMutex
items map[string]Item
}
func (c *Cache) Get(key string) (Item, bool) {
c.mu.RLock() // Multiple readers allowed
defer c.mu.RUnlock()
item, ok := c.items[key]
return item, ok
}
func (c *Cache) Set(key string, item Item) {
c.mu.Lock() // Exclusive write
defer c.mu.Unlock()
c.items[key] = item
}
sync.WaitGroup:
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1) // BEFORE starting goroutine
go func(i Item) {
defer wg.Done()
process(i)
}(item)
}
wg.Wait() // Block until all complete
sync.Once (One-Time Initialization):
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
instance.init()
})
return instance
}
sync/atomic (Lock-Free):
type Counter struct {
value atomic.Int64 // Go 1.19+
}
func (c *Counter) Increment() int64 {
return c.value.Add(1)
}
When to Use What:
func workerPool(ctx context.Context, numWorkers int, jobs <-chan Job, results chan<- Result) {
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case job, ok := <-jobs:
if !ok {
return // Jobs channel closed
}
result := processJob(job)
select {
case results <- result:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(w)
}
wg.Wait()
close(results) // Signal completion
}
// Usage
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan Job, 100)
results := make(chan Result, 100)
go workerPool(ctx, 10, jobs, results)
// Send jobs
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
// Collect results
for result := range results {
handleResult(result)
}
Running Race Detector:
go test -race ./...
go build -race
go run -race main.go
Common Race Conditions:
// ❌ RACE: Unsynchronized map
var cache = make(map[string]string)
func get(key string) string {
return cache[key] // RACE
}
func set(key, value string) {
cache[key] = value // RACE
}
// ✅ FIX: Use sync.Map
var cache sync.Map
func get(key string) string {
val, _ := cache.Load(key)
return val.(string)
}
// ❌ RACE: Loop variable capture
for _, item := range items {
go func() {
process(item) // RACE
}()
}
// ✅ FIX: Pass as parameter
for _, item := range items {
go func(i Item) {
process(i)
}(item)
}
Standard Pattern:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Best Practices:
t.Run() for subtestst.Parallel()func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Error points to caller, not here
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open test db: %v", err)
}
t.Cleanup(func() {
db.Close() // Automatic cleanup
})
return db
}
Unit Test (Fast, Isolated):
func TestCalculatePrice(t *testing.T) {
t.Parallel()
tests := []struct {
name string
quantity int
price float64
expected float64
}{
{"single item", 1, 10.0, 10.0},
{"multiple items", 5, 10.0, 50.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculatePrice(tt.quantity, tt.price)
if result != tt.expected {
t.Errorf("got %v, want %v", result, tt.expected)
}
})
}
}
Integration Test (Build Tag):
//go:build integration
// +build integration
package myapp_test
func TestDatabaseOperations(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
db := setupTestDatabase(t)
defer db.Close()
err := InsertUser(db, &User{Name: "John"})
if err != nil {
t.Fatalf("failed to insert user: %v", err)
}
}
Running Tests:
go test ./... # Unit tests only
go test -short ./... # Skip slow tests
go test -tags=integration ./... # Integration tests
Recommended .golangci.yml:
run:
timeout: 5m
linters:
enable:
- errcheck # Unchecked errors
- gosimple # Simplify code
- govet # Go vet
- staticcheck # Static analysis
- unused # Unused code
- gofmt # Formatting
- goimports # Imports
- revive # Fast linter
- gosec # Security
- errorlint # Error wrapping
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
govet:
enable-all: true
revive:
rules:
- name: error-strings
- name: error-naming
- name: exported
- name: indent-error-flow
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- gosec
Standard Workflow:
# Format Go code
go fmt ./...
# Static analysis
go vet ./...
# Comprehensive linting (if golangci-lint installed)
golangci-lint run
# Run tests with race detector
go test -race ./...
# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
CI/CD Integration (GitHub Actions):
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
- name: Tests
run: go test -race -coverprofile=coverage.out ./...
- name: Coverage
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
exit 1
fi
Basic Usage:
import "log/slog"
func main() {
// JSON handler for production
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("server starting",
slog.String("port", "8080"),
slog.Int("workers", 10))
// With context
logger.InfoContext(ctx, "request processed",
slog.String("method", "GET"),
slog.String("path", "/api/users"),
slog.Duration("latency", 45*time.Millisecond))
}
Log Levels:
logger.Debug("debug message") // Development
logger.Info("info message") // General info
logger.Warn("warning message") // Warnings
logger.Error("error message") // Errors
Request Context Logging:
func requestLogger(ctx context.Context, logger *slog.Logger) *slog.Logger {
requestID := ctx.Value("request_id").(string)
return logger.With(
slog.String("request_id", requestID),
slog.String("user_id", getUserID(ctx)),
)
}
// Usage in handler
func handleRequest(w http.ResponseWriter, r *http.Request) {
log := requestLogger(r.Context(), baseLogger)
log.Info("processing request",
slog.String("path", r.URL.Path),
slog.String("method", r.Method))
// All logs include request_id and user_id
log.Error("database query failed",
slog.String("error", err.Error()))
}
1. [CRITICAL] Swallowing Errors:
// ❌ WRONG
data, _ := fetchData()
// ✅ CORRECT
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
2. [CRITICAL] Using %v Instead of %w:
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)
// ✅ CORRECT
return fmt.Errorf("failed: %w", err)
3. [HIGH] Defer in Hot Loops:
// ❌ WRONG: Defers accumulate
for _, item := range items {
mu.Lock()
defer mu.Unlock() // Never executes until function returns
process(item)
}
// ✅ CORRECT
for _, item := range items {
mu.Lock()
process(item)
mu.Unlock()
}
4. [CRITICAL] Goroutine Leaks:
// ❌ WRONG: No way to stop
go func() {
for {
doWork()
}
}()
// ✅ CORRECT: Context cancellation
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}()
5. [CRITICAL] Loop Variable Capture:
// ❌ WRONG: All goroutines see last value
for _, item := range items {
go func() {
process(item) // RACE
}()
}
// ✅ CORRECT: Pass as parameter
for _, item := range items {
go func(i Item) {
process(i)
}(item)
}
6. [MEDIUM] Map Without Pre-allocation:
// ❌ WRONG: Multiple rehashes
m := make(map[string]Item)
for _, item := range items {
m[item.ID] = item
}
// ✅ CORRECT: Pre-sized
m := make(map[string]Item, len(items))
7. [HIGH] time.After in Loops (Memory Leak):
// ❌ WRONG: Creates timer each iteration
for {
select {
case <-time.After(5 * time.Second):
timeout()
}
}
// ✅ CORRECT: Reuse timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
select {
case <-timer.C:
timeout()
timer.Reset(5 * time.Second)
}
}
8. [HIGH] Checking Error Strings:
// ❌ WRONG: Fragile
if err != nil && strings.Contains(err.Error(), "not found") {
// ...
}
// ✅ CORRECT: Use errors.Is
if errors.Is(err, sql.ErrNoRows) {
// ...
}
9. [CRITICAL] Not Calling cancel():
// ❌ WRONG: Context leak
ctx, cancel := context.WithCancel(parent)
doWork(ctx)
// ✅ CORRECT: Always defer cancel
ctx, cancel := context.WithCancel(parent)
defer cancel()
doWork(ctx)
10. [MEDIUM] Producer-Side Interfaces:
// ❌ WRONG
package store
type Storage interface { ... }
type Store struct {}
// ✅ CORRECT
package client
type storage interface { ... } // Define where used
Official Documentation:
Industry Standards:
Research Source:
/ai-docs/sessions/dev-research-golang-best-practices-20260106-233135-773b0173/report.mdSkill Version: 2.0.0 Last Updated: January 7, 2026 Maintainer: MadAppGang/claude-code
testing
A test skill for validation testing. Use when testing skill parsing and validation logic.
tools
--- name: bad-skill description: This skill has invalid YAML in frontmatter allowed-tools: [invalid, array, syntax prerequisites: not-an-array --- # Bad Skill This skill has malformed frontmatter that should fail parsing. The YAML has: - Unclosed array bracket - Wrong type for prerequisites (should be array, not string)
tools
Plugin release process for MAG Claude Plugins marketplace. Covers version bumping, marketplace.json updates, git tagging, and common mistakes. Use when releasing new plugin versions or troubleshooting update issues.
testing
Fetch trending programming models from OpenRouter rankings. Use when selecting models for multi-model review, updating model recommendations, or researching current AI coding trends. Provides model IDs, context windows, pricing, and usage statistics from the most recent week.