| name | golang |
| 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. |
| metadata | {"version":"2.0.0","updated":"2026-01-20T00:00:00.000Z","keywords":["Go","Golang","concurrency","goroutines","channels","error handling","testing","backend","API"],"plugin":"dev"} |
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:
package url
package json
package strings
package urls
package encodingjson
package stringutils
Getters and Setters:
type Account struct {
balance int
}
func (a *Account) Balance() int {
return a.balance
}
func (a *Account) SetBalance(amount int) {
a.balance = amount
}
func (a *Account) GetBalance() int {
return a.balance
}
Error Variables:
var ErrNotFound = errors.New("not found")
var ErrTimeout = errors.New("timeout")
var errInternal = errors.New("internal error")
Interface Naming:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type DataReader interface { ... }
type IReader interface { ... }
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):
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
}
type ReadCloser interface {
Reader
Closer
}
Consumer-Side Interface Placement:
package store
type CustomerStorage interface {
StoreCustomer(Customer) error
GetCustomer(string) (Customer, error)
UpdateCustomer(Customer) error
}
type PostgresStore struct {}
func (s *PostgresStore) StoreCustomer(...) { ... }
package client
type customerGetter interface {
GetCustomer(string) (store.Customer, error)
}
func ProcessCustomer(cg customerGetter) {
customer, _ := cg.GetCustomer("123")
}
Return Concrete Types, Accept Interfaces (Postel's Law):
func NewStore() *PostgresStore {
return &PostgresStore{}
}
func Process(storage CustomerStorage) error {
}
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.
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
}
}
}
}
}
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):
type Logger struct {
*log.Logger
prefix string
}
func NewLogger(prefix string) *Logger {
return &Logger{
Logger: log.New(os.Stdout, "", 0),
prefix: prefix,
}
}
logger := NewLogger("APP")
logger.Println("message")
Interface Composition:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
Warning: Avoid embedding in public APIs:
type MyHandler struct {
http.Handler
}
type MyHandler struct {
handler http.Handler
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handler.ServeHTTP(w, r)
}
1.5 Key Go Idioms
Defer for Cleanup:
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if condition {
return nil
}
return process(f)
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
Critical Rule: Call defer AFTER checking error:
defer f.Close()
f, err := os.Open(path)
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
Multiple Return Values:
func GetUser(id string) (*User, error) {
}
value, ok := myMap[key]
if !ok {
}
result, ok := someValue.(TargetType)
if !ok {
}
data, ok := <-channel
if !ok {
}
Blank Identifier _:
_, err := os.Open(filename)
var _ http.Handler = (*MyHandler)(nil)
import _ "net/http/pprof"
Useful Zero Values:
var mu sync.Mutex
mu.Lock()
var buf bytes.Buffer
buf.WriteString("hello")
var s []int
fmt.Println(len(s))
2. Error Handling
2.1 Error Wrapping with %w (Go 1.13+)
Core Pattern: Wrap errors with context using fmt.Errorf and %w.
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
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)
}
Checking Wrapped Errors:
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File doesn't exist")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error on: %s\n", pathErr.Path)
}
Critical: Use %w, NOT %v:
return fmt.Errorf("failed: %v", err)
return fmt.Errorf("failed: %w", err)
2.2 Sentinel Errors vs Custom Error Types
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
}
user, err := db.GetUser(123)
if errors.Is(err, db.ErrRecordNotFound) {
}
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
}
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:
func (e *ValidationError) Error() string { ... }
func (e ValidationError) Error() string { ... }
2.3 Handle Errors Once
Core Principle: Either log the error OR return it, not both.
if err != nil {
log.Printf("error: %v", err)
return err
}
if err != nil {
return fmt.Errorf("process: %w", err)
}
if err != nil {
log.Printf("non-fatal error: %v", err)
}
Error Message Conventions:
var ErrNotFound = errors.New("configuration file not found")
return fmt.Errorf("failed to read settings for user %d: %w", userID, err)
var ErrNotFound = errors.New("Error: Configuration file not found.")
return fmt.Errorf("Error occurred: %v", err)
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:
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)
}
func createUser(email string) error {
if !isValidEmail(email) {
return fmt.Errorf("invalid email format: %s", email)
}
return nil
}
Use Panic When:
func ProcessData(data *Data) {
if data == nil {
panic("ProcessData: data argument must not be nil")
}
}
func init() {
cfg, err := loadConfig()
if err != nil {
panic(fmt.Sprintf("fatal: failed to load config: %v", err))
}
globalConfig = cfg
}
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):
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):
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
g.Go(func() error {
return processFile(ctx, file)
})
}
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
}
3. Concurrency Patterns
3.1 Goroutine Lifecycle and Leak Prevention
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
default:
doWork(id)
}
}
}(i)
}
wg.Wait()
}
ctx, cancel := context.WithCancel(context.Background())
go runWorkers(ctx, 10)
cancel()
Common Leak: Unbuffered Channel Send:
func leak() {
ch := make(chan int)
go func() {
ch <- 42
}()
}
func fixed() {
ch := make(chan int, 1)
go func() {
ch <- 42
}()
}
3.2 Channel Patterns
Unbuffered vs Buffered Semantics:
done := make(chan bool)
go func() {
doWork()
done <- true
}()
<-done
jobs := make(chan Job, 100)
for w := 0; w < numWorkers; w++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
Channel Closing Rules:
jobs := make(chan Job)
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
for job := range jobs {
process(job)
}
Select Pattern for Cancellation:
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case job := <-jobs:
process(job)
case <-ctx.Done():
return
}
}
}
3.3 Context Package for Cancellation
Context Types:
ctx := context.Background()
ctx := context.TODO()
ctx, cancel := context.WithCancel(parent)
defer cancel()
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()
ctx = context.WithValue(parent, key, value)
Best Practices:
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
}
defer resp.Body.Close()
return nil
}
func longRunning(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
processChunk()
}
}
}
Context Rules:
- Pass context as first parameter:
func Do(ctx context.Context, ...)
- Never store context in struct
- Always call cancel function (prevents leak)
- Use
WithValue only for request-scoped data, not options
3.4 Sync Primitives
sync.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()
defer c.mu.RUnlock()
item, ok := c.items[key]
return item, ok
}
func (c *Cache) Set(key string, item Item) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = item
}
sync.WaitGroup:
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
process(i)
}(item)
}
wg.Wait()
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
}
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
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
}
result := processJob(job)
select {
case results <- result:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(w)
}
wg.Wait()
close(results)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
jobs := make(chan Job, 100)
results := make(chan Result, 100)
go workerPool(ctx, 10, jobs, results)
go func() {
for _, job := range allJobs {
jobs <- job
}
close(jobs)
}()
for result := range results {
handleResult(result)
}
3.6 Race Detection
Running Race Detector:
go test -race ./...
go build -race
go run -race main.go
Common Race Conditions:
var cache = make(map[string]string)
func get(key string) string {
return cache[key]
}
func set(key, value string) {
cache[key] = value
}
var cache sync.Map
func get(key string) string {
val, _ := cache.Load(key)
return val.(string)
}
for _, item := range items {
go func() {
process(item)
}()
}
for _, item := range items {
go func(i Item) {
process(i)
}(item)
}
4. Testing Patterns
4.1 Table-Driven Tests
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:
- 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()
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
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()
})
return db
}
4.3 Integration vs Unit Testing
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):
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 ./...
go test -short ./...
go test -tags=integration ./...
5. Quality Checks
5.1 golangci-lint Configuration
Recommended .golangci.yml:
run:
timeout: 5m
linters:
enable:
- errcheck
- gosimple
- govet
- staticcheck
- unused
- gofmt
- goimports
- revive
- gosec
- errorlint
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:
go fmt ./...
go vet ./...
golangci-lint run
go test -race ./...
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
6. Structured Logging Patterns
6.1 Structured Logging with slog (Go 1.21+)
Basic Usage:
import "log/slog"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("server starting",
slog.String("port", "8080"),
slog.Int("workers", 10))
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")
logger.Info("info message")
logger.Warn("warning message")
logger.Error("error message")
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)),
)
}
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))
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:
data, _ := fetchData()
data, err := fetchData()
if err != nil {
return fmt.Errorf("fetch failed: %w", err)
}
2. [CRITICAL] Using %v Instead of %w:
return fmt.Errorf("failed: %v", err)
return fmt.Errorf("failed: %w", err)
3. [HIGH] Defer in Hot Loops:
for _, item := range items {
mu.Lock()
defer mu.Unlock()
process(item)
}
for _, item := range items {
mu.Lock()
process(item)
mu.Unlock()
}
4. [CRITICAL] Goroutine Leaks:
go func() {
for {
doWork()
}
}()
go func() {
for {
select {
case <-ctx.Done():
return
default:
doWork()
}
}
}()
5. [CRITICAL] Loop Variable Capture:
for _, item := range items {
go func() {
process(item)
}()
}
for _, item := range items {
go func(i Item) {
process(i)
}(item)
}
6. [MEDIUM] Map Without Pre-allocation:
m := make(map[string]Item)
for _, item := range items {
m[item.ID] = item
}
m := make(map[string]Item, len(items))
7. [HIGH] time.After in Loops (Memory Leak):
for {
select {
case <-time.After(5 * time.Second):
timeout()
}
}
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:
if err != nil && strings.Contains(err.Error(), "not found") {
}
if errors.Is(err, sql.ErrNoRows) {
}
9. [CRITICAL] Not Calling cancel():
ctx, cancel := context.WithCancel(parent)
doWork(ctx)
ctx, cancel := context.WithCancel(parent)
defer cancel()
doWork(ctx)
10. [MEDIUM] Producer-Side Interfaces:
package store
type Storage interface { ... }
type Store struct {}
package client
type storage interface { ... }
References
Official Documentation:
Industry Standards:
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