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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
disable-model-invocation
true
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:
Small interfaces (1-3 methods ideal)
Consumer-side interface placement
Error values, not exceptions
Happy path at left margin
Goroutines must have explicit termination
1. Idiomatic Go Patterns
1.1 Naming Conventions
Package Names:
// โ GOOD: Package names are single lowercase identifiers// Import path: "net/url" โ package name: url// Import path: "encoding/json" โ package name: jsonpackage url // from "net/url"package json // from "encoding/json"package strings
// โ BADpackage urls // No pluralpackage encodingjson // Don't smash words togetherpackage stringutils // Too verbose
Getters and Setters:
type Account struct {
balance int
}
// โ GOOD: No "Get" prefixfunc(a *Account) Balance() int {
return a.balance
}
func(a *Account) SetBalance(amount int) {
a.balance = amount
}
// โ BAD: Java-style gettersfunc(a *Account) GetBalance() int {
return a.balance
}
// sync.Mutex - ready to usevar mu sync.Mutex
mu.Lock() // Works immediately// bytes.Buffer - valid empty buffervar buf bytes.Buffer
buf.WriteString("hello") // No initialization needed// Slices - safe to readvar 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.
funcprocessFile(path string)error {
file, err := os.Open(path)
if err != nil {
// Wrap with context using %wreturn 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 chainif errors.Is(err, os.ErrNotExist) {
fmt.Println("File doesn't exist")
}
// errors.As - Extract specific error typevar pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error on: %s\n", pathErr.Path)
}
// โ GOODvar ErrNotFound = errors.New("configuration file not found")
return fmt.Errorf("failed to read settings for user %d: %w", userID, err)
// โ BADvar ErrNotFound = errors.New("Error: Configuration file not found.") // No prefix, no punctuationreturn 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
funcprocessAll(items []Item) []error {
errChan := make(chanerror, len(items))
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
gofunc(i Item) {
defer wg.Done()
if err := process(i); err != nil {
errChan <- err
}
}(item)
}
gofunc() {
wg.Wait()
close(errChan)
}()
var errs []errorfor 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:
funcrunWorkers(ctx context.Context, n int) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
gofunc(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return// Clean exitdefault:
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()
// Unbuffered: Synchronous handoff
done := make(chanbool)
gofunc() {
doWork()
done <- true// Blocks until main receives
}()
<-done // Guaranteed: work completed// Buffered: Asynchronous
jobs := make(chan Job, 100)
for w := 0; w < numWorkers; w++ {
gofunc() {
for job := range jobs {
process(job)
}
}()
}
Channel Closing Rules:
// โ GOOD: Only sender closes
jobs := make(chan Job)
gofunc() {
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:
funcworker(ctx context.Context, jobs <-chan Job) {
for {
select {
case job := <-jobs:
process(job)
case <-ctx.Done():
return// Cancel signal
}
}
}
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1) // BEFORE starting goroutinegofunc(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
)
funcGetInstance() *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:
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
funcworkerPool(ctx context.Context, numWorkers int, jobs <-chan Job, results chan<- Result) {
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
gofunc(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 jobsgofunc() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
// Collect resultsfor result := range results {
handleResult(result)
}
3.6 Race Detection
Running Race Detector:
go test -race ./...
go build -race
go run -race main.go
funcTestAdd(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()
funcassertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Error points to caller, not hereif got != want {
t.Errorf("got %v, want %v", got, want)
}
}
funcsetupTestDB(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):
funcTestCalculatePrice(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 integrationpackage myapp_test
funcTestDatabaseOperations(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
5. Quality Checks
5.1 golangci-lint Configuration
Recommended .golangci.yml:
run:timeout:5mlinters: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 wrappinglinters-settings:errcheck:check-type-assertions:truecheck-blank:truegovet:enable-all:truerevive:rules:-name:error-strings-name:error-naming-name:exported-name:indent-error-flowissues:exclude-rules:-path:_test\.golinters:-errcheck-gosec
5.2 Running Quality Checks
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-lintuses:golangci/golangci-lint-action@v3with:version:latest-name:Testsrun:gotest-race-coverprofile=coverage.out./...-name:Coveragerun:|
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:
import"log/slog"funcmain() {
// 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:
funcrequestLogger(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 handlerfunchandleRequest(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()))
}
// โ WRONG: Defers accumulatefor _, item := range items {
mu.Lock()
defer mu.Unlock() // Never executes until function returns
process(item)
}
// โ CORRECTfor _, item := range items {
mu.Lock()
process(item)
mu.Unlock()
}
4. [CRITICAL] Goroutine Leaks:
// โ WRONG: No way to stopgofunc() {
for {
doWork()
}
}()
// โ CORRECT: Context cancellationgofunc() {
for {
select {
case <-ctx.Done():
returndefault:
doWork()
}
}
}()
5. [CRITICAL] Loop Variable Capture:
// โ WRONG: All goroutines see last valuefor _, item := range items {
gofunc() {
process(item) // RACE
}()
}
// โ CORRECT: Pass as parameterfor _, item := range items {
gofunc(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))
// โ WRONGpackage store
type Storage interface { ... }
type Store struct {}
// โ CORRECTpackage client
type storage interface { ... } // Define where used