Go Style Guide¶
Purpose: Idiomatic Go coding standards for this repository.
Scope¶
This document covers: code formatting, concurrency patterns, interface design, error handling, and testing.
Related: - Python Style Guide - Python coding conventions - Shell Script Standards - Bash scripting conventions
Quick Reference¶
| Standard | Value | Validation |
|---|---|---|
| Go Version | 1.24+ | go version |
| Formatter | gofmt / goimports |
gofmt -l . |
| Linter | golangci-lint |
.golangci.yml at repo root |
| Module | Required | go.mod in project root |
golangci-lint Configuration¶
Create .golangci.yml at repo root:
YAML
# .golangci.yml
version: "2"
run:
timeout: 5m
go: "1.24"
linters:
default: none
enable:
- errcheck # Check error returns
- govet # Vet examines Go source
- staticcheck # Static analysis
- ineffassign # Detect ineffective assignments
- unused # Check for unused code
- misspell # Find misspellings
- revive # Replacement for golint
- gocritic # Opinionated linter
- errname # Error naming conventions
- errorlint # Error wrapping checks
settings:
revive:
rules:
- name: exported
arguments: [checkPrivateReceivers]
- name: var-naming
- name: blank-imports
gocritic:
enabled-tags:
- diagnostic
- style
- performance
errcheck:
check-blank: true
exclusions:
rules:
- path: _test\.go
linters:
- errcheck
Usage:
Bash
# Run linter
golangci-lint run ./...
# Auto-fix issues
golangci-lint run --fix ./...
# Check specific package
golangci-lint run ./pkg/...
Concurrency Patterns¶
Goroutines with WaitGroup¶
Go
func processItems(items []Item) error {
var wg sync.WaitGroup
errCh := make(chan error, len(items))
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
if err := process(it); err != nil {
errCh <- fmt.Errorf("process %s: %w", it.Name, err)
}
}(item)
}
wg.Wait()
close(errCh)
// Collect errors
var errs []error
for err := range errCh {
errs = append(errs, err)
}
return errors.Join(errs...)
}
Channel Select with Context¶
Go
func worker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
results <- process(job)
}
}
}
Bounded Concurrency (Worker Pool)¶
Go
func processWithLimit(ctx context.Context, items []Item, limit int) error {
sem := make(chan struct{}, limit)
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item // Capture for goroutine
g.Go(func() error {
select {
case sem <- struct{}{}:
defer func() { <-sem }()
case <-ctx.Done():
return ctx.Err()
}
return process(ctx, item)
})
}
return g.Wait()
}
Interface Design¶
Accept Interfaces, Return Structs¶
Go
// Good - Accept interface
func ProcessData(r io.Reader) error {
// Can accept *os.File, *bytes.Buffer, etc.
}
// Good - Return concrete type
func NewClient(cfg Config) *Client {
return &Client{cfg: cfg}
}
// Bad - Return interface (hides implementation)
func NewClient(cfg Config) ClientInterface {
return &Client{cfg: cfg}
}
Small, Focused Interfaces¶
Go
// Good - Single method interface
type Processor interface {
Process(ctx context.Context, data []byte) error
}
type Validator interface {
Validate() error
}
// Composition when needed
type ValidatingProcessor interface {
Processor
Validator
}
// Bad - Kitchen sink interface
type Manager interface {
Create() error
Update() error
Delete() error
List() ([]Item, error)
Validate() error
Process() error
// ... 10 more methods
}
Error Handling¶
Wrap Errors with Context¶
Go
import "fmt"
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Custom Error Types¶
Go
// Sentinel errors for comparison
var (
ErrNotFound = errors.New("not found")
ErrPermission = errors.New("permission denied")
)
// Structured error with context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Usage with errors.Is/As
func process(id string) error {
item, err := fetch(id)
if errors.Is(err, ErrNotFound) {
return fmt.Errorf("item %s: %w", id, err)
}
var valErr *ValidationError
if errors.As(err, &valErr) {
log.Printf("validation issue: %s", valErr.Field)
}
return err
}
Don't Panic in Libraries¶
Go
// Good - Return error
func Parse(data []byte) (*Config, error) {
if len(data) == 0 {
return nil, errors.New("empty data")
}
// ...
}
// Bad - Panic in library code
func Parse(data []byte) *Config {
if len(data) == 0 {
panic("empty data")
}
// ...
}
Table-Driven Tests¶
Go
func TestValidateNamespace(t *testing.T) {
tests := []struct {
name string
namespace string
wantErr bool
}{
{
name: "valid namespace",
namespace: "my-namespace",
wantErr: false,
},
{
name: "empty namespace",
namespace: "",
wantErr: true,
},
{
name: "invalid characters",
namespace: "My_Namespace",
wantErr: true,
},
{
name: "too long",
namespace: strings.Repeat("a", 64),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateNamespace(tt.namespace)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateNamespace(%q) error = %v, wantErr %v",
tt.namespace, err, tt.wantErr)
}
})
}
}
Test Helpers¶
Go
func TestClient(t *testing.T) {
// t.Helper() marks function as test helper
// failures report caller's line, not helper's
client := newTestClient(t)
// ...
}
func newTestClient(t *testing.T) *Client {
t.Helper()
client, err := NewClient(testConfig)
if err != nil {
t.Fatalf("failed to create test client: %v", err)
}
return client
}
Code Template¶
Go
// Package config provides configuration loading for tools.
package config
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
)
// Errors for this package.
var (
ErrNotFound = errors.New("config not found")
ErrInvalid = errors.New("invalid config")
)
// Config holds application configuration.
type Config struct {
Namespace string `json:"namespace"`
Timeout int `json:"timeout"`
}
// Loader loads configuration from various sources.
type Loader interface {
Load(ctx context.Context, path string) (*Config, error)
}
// FileLoader loads config from filesystem.
type FileLoader struct {
basePath string
}
// NewFileLoader creates a FileLoader with the given base path.
func NewFileLoader(basePath string) *FileLoader {
return &FileLoader{basePath: basePath}
}
// Load reads and parses config from a file.
func (l *FileLoader) Load(ctx context.Context, path string) (*Config, error) {
fullPath := l.basePath + "/" + path
data, err := os.ReadFile(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("%w: %s", ErrNotFound, path)
}
return nil, fmt.Errorf("read %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("%w: parse %s: %v", ErrInvalid, path, err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalid, err)
}
return &cfg, nil
}
func (c *Config) validate() error {
if c.Namespace == "" {
return errors.New("namespace required")
}
if c.Timeout <= 0 {
c.Timeout = 30 // Default
}
return nil
}
Code Complexity¶
Complexity Measurement¶
Use gocyclo or gocognit to measure function complexity:
Bash
# Install
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
# Check complexity (threshold 10)
gocyclo -over 10 ./...
# Show all functions sorted by complexity
gocyclo -top 20 ./...
Complexity Grades¶
| Grade | CC Range | Action |
|---|---|---|
| A | 1-5 | Ideal |
| B | 6-10 | Acceptable |
| C | 11-15 | Refactor when touching |
| D | 16-20 | Must refactor |
| F | 21+ | Block merge |
Reducing Complexity in Go¶
Pattern 1: Early Returns
Go
// Bad - nested conditionals (CC=6)
func process(item *Item) error {
if item != nil {
if item.Valid {
if item.Ready {
return doWork(item)
}
}
}
return errors.New("invalid")
}
// Good - guard clauses (CC=3)
func process(item *Item) error {
if item == nil {
return errors.New("nil item")
}
if !item.Valid {
return errors.New("invalid item")
}
if !item.Ready {
return errors.New("not ready")
}
return doWork(item)
}
Pattern 2: Strategy Maps
Go
// Bad - switch statement (CC grows with cases)
func handle(cmd string) error {
switch cmd {
case "create":
return handleCreate()
case "update":
return handleUpdate()
case "delete":
return handleDelete()
// ... more cases
}
return errors.New("unknown command")
}
// Good - handler map (CC=2)
var handlers = map[string]func() error{
"create": handleCreate,
"update": handleUpdate,
"delete": handleDelete,
}
func handle(cmd string) error {
h, ok := handlers[cmd]
if !ok {
return errors.New("unknown command")
}
return h()
}
Common Errors¶
| Symptom | Cause | Fix |
|---|---|---|
undefined: X |
Missing import or typo | Check imports, spelling |
cannot use X as Y |
Type mismatch | Check interface compliance |
nil pointer dereference |
Uninitialized pointer | Add nil check before use |
deadlock |
Goroutine waiting forever | Check channel/mutex usage |
race detected |
Data race | Use mutex or channels |
context canceled |
Parent context done | Handle ctx.Err() |
go.mod outdated |
Dependency drift | Run go mod tidy |
Anti-Patterns¶
| Name | Pattern | Why Bad | Instead |
|---|---|---|---|
| Naked Returns | return without values |
Unclear what's returned | Explicit: return result, nil |
| Init Abuse | Heavy logic in init() |
Hidden side effects, test issues | Explicit initialization |
| Interface Pollution | Interfaces with 10+ methods | Hard to implement/mock | Small interfaces, compose |
| Error Strings | errors.New("User not found") |
Can't check programmatically | Sentinel errors or types |
| Ignoring Errors | result, _ := fn() |
Silent failures | Handle or document why ignored |
| Premature Channel | Channels for simple sync | Overhead, complexity | Use mutex for simple cases |
Summary Checklist¶
| Category | Requirement |
|---|---|
| Tooling | Go 1.24+, gofmt, golangci-lint |
| Formatting | All code passes gofmt -l . |
| Linting | All code passes golangci-lint run |
| Complexity | CC ≤ 10 per function (gocyclo -over 10) |
| Errors | Wrap with context using fmt.Errorf("...: %w", err) |
| Errors | Use errors.Is/errors.As for comparison |
| Errors | No panic in library code |
| Interfaces | Accept interfaces, return structs |
| Interfaces | Keep interfaces small (1-3 methods) |
| Concurrency | Use context.Context for cancellation |
| Concurrency | Use errgroup for bounded concurrency |
| Tests | Table-driven tests with subtests |
| Tests | Use t.Helper() in test helpers |
| Dependencies | Prefer standard library |