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.
golang follows the SKILL.md standard. Use the install command to add it to your agent stack.
---
name: golang
version: 2.0.0
description: 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.
keywords:
- Go
- Golang
- concurrency
- goroutines
- channels
- error handling
- testing
- backend
- API
plugin: dev
updated: 2026-01-20
---
# Go Development Best Practices
**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
## Overview
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**:
- **Simplicity**: "Clear is better than clever" - favor readable code over abstractions
- **Explicit over implicit**: No exceptions, no hidden control flow, visible errors
- **Composition over inheritance**: Interfaces and embedding, not class hierarchies
- **Built-in concurrency**: Goroutines and channels as first-class primitives
- **Tooling-first**: Format, vet, test, and benchmark built into the language
**Key Design Principles**:
1. Small interfaces (1-3 methods ideal)
2. Consumer-side interface placement
3. Error values, not exceptions
4. Happy path at left margin
5. Goroutines must have explicit termination
---
## 1. Idiomatic Go Patterns
### 1.1 Naming Conventions
**Package Names**:
```go
// ✅ 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**:
```go
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**:
```go
// 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**:
```go
// ✅ 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
```
---
### 1.2 Interface Design - "The Bigger the Interface, the Weaker the Abstraction"
**Core Principle**: Small, consumer-side interfaces provide maximum flexibility.
**Single-Method Interfaces** (Ideal):
```go
// 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**:
```go
// ❌ 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):
```go
// ✅ 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**:
- Multiple implementations exist or are planned
- Need for testing (mocking dependencies)
- Decoupling packages
- **NOT for**: Single implementation with no testing need
---
### 1.3 Happy Path Left, Early Returns
**Core Principle**: Align success path to left margin, handle errors first.
```go
// ❌ 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**:
- Maximum 3-4 levels of nesting
- Omit `else` blocks when `if` returns
- Handle errors immediately
- Keep normal flow at lowest indentation
---
### 1.4 Composition Over Inheritance
**Type Embedding** (Struct Composition):
```go
// 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**:
```go
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:
```go
// ❌ 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)
}
```
---
### 1.5 Key Go Idioms
**Defer for Cleanup**:
```go
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:
```go
// ❌ 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**:
```go
// (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 `_`**:
```go
// 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**:
```go
// 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)
```
---
## 2. Error Handling
### 2.1 Error Wrapping with %w (Go 1.13+)
**Core Pattern**: Wrap errors with context using `fmt.Errorf` and `%w`.
```go
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**:
```go
// 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`:
```go
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)
// ✅ CORRECT: Preserves chain
return fmt.Errorf("failed: %w", err)
```
---
### 2.2 Sentinel Errors vs Custom Error Types
**Sentinel Errors** (Package-Level Variables):
```go
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):
```go
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**:
- **Sentinel errors**: Simple, global error conditions
- **Custom types**: Errors needing structured data or methods
**Important**: Use pointer receivers for error types:
```go
// ✅ CORRECT: Pointer receiver
func (e *ValidationError) Error() string { ... }
// ❌ WRONG: Value receiver (breaks errors.As)
func (e ValidationError) Error() string { ... }
```
---
### 2.3 Handle Errors Once
**Core Principle**: Either log the error OR return it, not both.
```go
// ❌ 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**:
```go
// ✅ 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
```
---
### 2.4 Panic vs Error Decision Tree
```
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**:
```go
// 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**:
```go
// 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):
```go
// 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)
}
}
```
---
### 2.5 Concurrent Error Handling
**Pattern 1: errgroup (Coordinated Goroutines)**:
```go
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)**:
```go
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
}
```
---
## 3. Concurrency Patterns
### 3.1 Goroutine Lifecycle and Leak Prevention
**Core Principle**: Every goroutine must have an explicit termination mechanism.
**Pattern: Context Cancellation + WaitGroup**:
```go
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**:
```go
// ❌ 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
}()
}
```
---
### 3.2 Channel Patterns
**Unbuffered vs Buffered Semantics**:
```go
// 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**:
```go
// ✅ 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**:
```go
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case job := <-jobs:
process(job)
case <-ctx.Done():
return // Cancel signal
}
}
}
```
---
### 3.3 Context Package for Cancellation
**Context Types**:
```go
// 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**:
```go
// ✅ 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**:
1. Pass context as first parameter: `func Do(ctx context.Context, ...)`
2. Never store context in struct
3. Always call cancel function (prevents leak)
4. Use `WithValue` only for request-scoped data, not options
---
### 3.4 Sync Primitives
**sync.Mutex**:
```go
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):
```go
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**:
```go
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):
```go
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
instance.init()
})
return instance
}
```
**sync/atomic** (Lock-Free):
```go
type Counter struct {
value atomic.Int64 // Go 1.19+
}
func (c *Counter) Increment() int64 {
return c.value.Add(1)
}
```
**When to Use What**:
- **Mutex**: Protecting compound operations, complex state
- **RWMutex**: Read-heavy (10:1 read:write ratio+)
- **WaitGroup**: Waiting for goroutines
- **Once**: Lazy initialization
- **Atomic**: Simple counters, flags
- **Channels**: Communication, coordination
---
### 3.5 Worker Pool Pattern
```go
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)
}
```
---
### 3.6 Race Detection
**Running Race Detector**:
```bash
go test -race ./...
go build -race
go run -race main.go
```
**Common Race Conditions**:
```go
// ❌ 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)
}
```
---
## 4. Testing Patterns
### 4.1 Table-Driven Tests
**Standard Pattern**:
```go
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**:
- Always use `t.Run()` for subtests
- Descriptive test case names
- Use anonymous structs for test data
- Enable parallel execution with `t.Parallel()`
---
### 4.2 Test Helpers with t.Helper()
```go
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
}
```
---
### 4.3 Integration vs Unit Testing
**Unit Test** (Fast, Isolated):
```go
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
//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**:
```bash
go test ./... # Unit tests only
go test -short ./... # Skip slow tests
go test -tags=integration ./... # Integration tests
```
---
## 5. Quality Checks
### 5.1 golangci-lint Configuration
**Recommended .golangci.yml**:
```yaml
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
```
---
### 5.2 Running Quality Checks
**Standard Workflow**:
```bash
# 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)**:
```yaml
- 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
```
---
## 6. Structured Logging Patterns
### 6.1 Structured Logging with slog (Go 1.21+)
**Basic Usage**:
```go
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**:
```go
logger.Debug("debug message") // Development
logger.Info("info message") // General info
logger.Warn("warning message") // Warnings
logger.Error("error message") // Errors
```
**Request Context Logging**:
```go
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()))
}
```
---
## 7. Anti-Patterns to Avoid
### Critical Anti-Patterns with Severity Tags
**1. [CRITICAL] Swallowing Errors**:
```go
// ❌ WRONG
data, _ := fetchData()
// ✅ CORRECT
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
```
**2. [CRITICAL] Using %v Instead of %w**:
```go
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)
// ✅ CORRECT
return fmt.Errorf("failed: %w", err)
```
**3. [HIGH] Defer in Hot Loops**:
```go
// ❌ 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**:
```go
// ❌ 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**:
```go
// ❌ 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**:
```go
// ❌ 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)**:
```go
// ❌ 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**:
```go
// ❌ 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()**:
```go
// ❌ 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**:
```go
// ❌ WRONG
package store
type Storage interface { ... }
type Store struct {}
// ✅ CORRECT
package client
type storage interface { ... } // Define where used
```
---
## References
**Official Documentation**:
- [Effective Go](https://go.dev/doc/effective_go)
- [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
- [Go Blog: Error Handling](https://go.dev/blog/go1.13-errors)
- [Go Blog: Context](https://go.dev/blog/context)
**Industry Standards**:
- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md)
- [100 Go Mistakes](https://www.manning.com/books/100-go-mistakes-and-how-to-avoid-them)
**Research Source**:
- Comprehensive research: `/ai-docs/sessions/dev-research-golang-best-practices-20260106-233135-773b0173/report.md`
- 15 unanimous consensus patterns
- 42 high-quality sources (100% official/industry standards)
- Zero contradictions found
---
**Skill Version**: 2.0.0
**Last Updated**: January 7, 2026
**Maintainer**: MadAppGang/claude-code