effective-go

SKILL.md

Effective Go

Idiomatic Go development guidance based on Effective Go principles. Use for writing new Go code, reviewing PRs, or as reference material for go-reviewer agents.

Go Version Coverage: Go 1.18+ (generics), Go 1.22+ (iterators), Go 1.23+ (range-over-func)

Modes of Operation

Mode Trigger Action
Development Writing new Go code Apply patterns proactively
Review Reviewing Go PRs/code Check against anti-patterns
Reference "How do I X in Go?" Provide idiomatic solution

Formatting

Mandatory: Run gofmt on all Go code. No exceptions.

Rule Correct Wrong
Indentation Tabs Spaces
Opening brace Same line as declaration New line
Line length No hard limit (gofmt handles) Arbitrary wrapping
// Correct: brace on same line
func process(data []byte) error {
    if len(data) == 0 {
        return errors.New("empty data")
    }
    return nil
}

// Wrong: brace on new line (not Go style)
func process(data []byte) error
{
    // ...
}

Naming Conventions

Package Names

Rule Good Bad
Lowercase, single word http, json, template httpUtil, json_parser
Short, descriptive bytes, io utilities, helpers
No stuttering with types time.Duration time.TimeDuration
// Good: package name doesn't repeat in type name
package user
type Service struct{} // usage: user.Service

// Bad: stuttering
package user
type UserService struct{} // usage: user.UserService

Getters and Setters

Pattern Correct Wrong
Getter obj.Name() obj.GetName()
Setter obj.SetName(n) obj.Name(n)
type User struct {
    name string
}

// Correct: getter without "Get" prefix
func (u *User) Name() string { return u.name }

// Correct: setter with "Set" prefix
func (u *User) SetName(n string) { u.name = n }

Interface Names

Method Count Naming Pattern Examples
Single method Method + "-er" Reader, Writer, Stringer
Multiple methods Descriptive noun ReadWriter, FileInfo
// Single method: -er suffix
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Combination: compose the names
type ReadWriter interface {
    Reader
    Writer
}

Export Rules

Visibility Case Example
Exported (public) Uppercase first letter Config, Parse, DefaultTimeout
Unexported (private) Lowercase first letter config, parse, defaultTimeout

MixedCaps

Use MixedCaps or mixedCaps, never underscores.

// Correct
var userID int
var httpClient *http.Client
const maxRetryCount = 3

// Wrong
var user_id int
var http_client *http.Client
const max_retry_count = 3

Acronyms

Keep acronyms consistently cased.

// Correct
var userID string    // ID is acronym
var httpURL string   // HTTP and URL are acronyms
type XMLParser struct{}

// Wrong
var UserId string
var httpUrl string
type XmlParser struct{}

Error Handling

Always Check Errors

// Correct: check the error
f, err := os.Open(filename)
if err != nil {
    return fmt.Errorf("opening file: %w", err)
}
defer f.Close()

// Wrong: ignoring error
f, _ := os.Open(filename) // silent failure

Error Wrapping

Use %w for wrapping errors to maintain the chain.

// Correct: wrap with context
if err := db.Query(sql); err != nil {
    return fmt.Errorf("querying users: %w", err)
}

// Use errors.Is and errors.As for checking
if errors.Is(err, sql.ErrNoRows) {
    return nil, ErrNotFound
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {
    log.Printf("path error on %s", pathErr.Path)
}

Custom Error Types

// Structured error with context
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// Usage
return &ValidationError{Field: "email", Message: "invalid format"}

Multiple Errors with errors.Join (Go 1.20+)

// Combine multiple errors
func validateUser(u User) error {
    var errs []error

    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if !strings.Contains(u.Email, "@") {
        errs = append(errs, errors.New("invalid email format"))
    }
    if u.Age < 0 {
        errs = append(errs, errors.New("age cannot be negative"))
    }

    return errors.Join(errs...) // Returns nil if errs is empty
}

// Check for specific error in joined errors
err := validateUser(user)
if errors.Is(err, ErrInvalidEmail) {
    // Handle invalid email
}

// Cleanup with multiple errors
func cleanup(resources ...io.Closer) error {
    var errs []error
    for _, r := range resources {
        if err := r.Close(); err != nil {
            errs = append(errs, err)
        }
    }
    return errors.Join(errs...)
}

Sentinel Errors vs. Error Types

// Sentinel errors: for well-known conditions
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("conflict")
)

// When to use: simple conditions, no extra context needed
func Get(id string) (*Item, error) {
    item, ok := store[id]
    if !ok {
        return nil, ErrNotFound // Caller uses errors.Is
    }
    return item, nil
}

// Error types: when you need structured data
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s %s not found", e.Resource, e.ID)
}

// When to use: need context, multiple fields, or methods
func GetUser(id string) (*User, error) {
    user, ok := users[id]
    if !ok {
        return nil, &NotFoundError{Resource: "user", ID: id}
    }
    return user, nil
}

// Caller extracts info
var nfe *NotFoundError
if errors.As(err, &nfe) {
    log.Printf("Missing %s: %s", nfe.Resource, nfe.ID)
}

Error Handling Patterns

Pattern When to Use
Return early Most cases
Wrap with context Crossing package boundaries
Sentinel errors Well-known conditions (io.EOF)
Custom types Need structured data
errors.Join Multiple independent errors

Control Flow

No Unnecessary Else

// Correct: return early, no else needed
func validate(x int) error {
    if x < 0 {
        return errors.New("negative value")
    }
    // happy path continues
    return nil
}

// Wrong: unnecessary else
func validate(x int) error {
    if x < 0 {
        return errors.New("negative value")
    } else {
        return nil
    }
}

Happy Path Flows Down

Keep the main logic at the lowest indentation level.

// Correct: errors handled, happy path flows down
func process(data []byte) (*Result, error) {
    if len(data) == 0 {
        return nil, errors.New("empty data")
    }

    parsed, err := parse(data)
    if err != nil {
        return nil, fmt.Errorf("parsing: %w", err)
    }

    validated, err := validate(parsed)
    if err != nil {
        return nil, fmt.Errorf("validating: %w", err)
    }

    return transform(validated), nil
}

Switch Over Long If-Else

// Correct: switch for multiple conditions
switch ext := filepath.Ext(filename); ext {
case ".json":
    return parseJSON(data)
case ".yaml", ".yml":
    return parseYAML(data)
case ".toml":
    return parseTOML(data)
default:
    return nil, fmt.Errorf("unsupported format: %s", ext)
}

// Wrong: long if-else chain
if ext == ".json" {
    return parseJSON(data)
} else if ext == ".yaml" || ext == ".yml" {
    return parseYAML(data)
} else if ext == ".toml" {
    return parseTOML(data)
} else {
    return nil, fmt.Errorf("unsupported format: %s", ext)
}

Labeled Breaks

// Correct: labeled break for nested loops
outer:
for _, user := range users {
    for _, role := range user.Roles {
        if role == "admin" {
            found = user
            break outer
        }
    }
}

Functions

Named Return Parameters

Use sparingly for documentation; avoid in long functions.

// Good: short function, named returns document purpose
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

// Avoid: long function with naked return
func complexOperation() (result string, err error) {
    // 50+ lines...
    return // What is being returned? Unclear.
}

Defer for Cleanup

Place defer immediately after resource acquisition.

// Correct: defer immediately after Open
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close() // Immediately after successful open

    return io.ReadAll(f)
}

// Wrong: defer far from acquisition
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    // ... many lines ...
    defer f.Close() // Easy to forget or miss
    // ... more code ...
}

Multiple Return Values

Use for error handling and optional values.

// Standard pattern: (result, error)
func Parse(s string) (Value, error) {
    // ...
}

// Comma-ok idiom for optionals
value, ok := cache[key]
if !ok {
    value = computeDefault()
}

Generics (Go 1.18+)

When to Use Generics vs. Interfaces

Use Case Prefer Example
Behavior abstraction Interface io.Reader, fmt.Stringer
Type-safe collections Generics Stack[T], Set[T]
Algorithm over types Generics slices.Sort, maps.Keys
Runtime polymorphism Interface Plugins, handlers
Compile-time safety Generics Containers, utilities
// Interface: when you care about behavior
type Handler interface {
    Handle(ctx context.Context, req Request) Response
}

// Generics: when you care about type safety across types
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) }
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

Type Constraints

// Built-in constraints from constraints package
import "golang.org/x/exp/constraints"

func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Custom constraints
type Number interface {
    ~int | ~int32 | ~int64 | ~float32 | ~float64
}

func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v
    }
    return total
}

// Comparable constraint for maps
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

Generic Data Structures

// Thread-safe generic cache
type Cache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{items: make(map[K]V)}
}

func (c *Cache[K, V]) Get(key K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.items[key]
    return v, ok
}

func (c *Cache[K, V]) Set(key K, value V) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[key] = value
}

// Generic result type (like Rust's Result)
type Result[T any] struct {
    Value T
    Err   error
}

func Ok[T any](v T) Result[T]    { return Result[T]{Value: v} }
func Err[T any](e error) Result[T] { return Result[T]{Err: e} }

Generics Anti-Patterns

Anti-Pattern Issue Fix
any everywhere Loses type safety Use specific constraints
Generic when interface works Over-engineering Interface for behavior
Complex constraint unions Hard to read Simplify or use interface
// Wrong: any loses type safety
func Process[T any](items []T) []T { /* ... */ }

// Better: constrain when operations are needed
func SortAndDedup[T constraints.Ordered](items []T) []T { /* ... */ }

Iterators (Go 1.22+)

iter Package Patterns

import "iter"

// Basic iterator function signature
// iter.Seq[V] = func(yield func(V) bool)
// iter.Seq2[K, V] = func(yield func(K, V) bool)

// Creating an iterator
func Countdown(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := n; i > 0; i-- {
            if !yield(i) {
                return
            }
        }
    }
}

// Usage
for v := range Countdown(5) {
    fmt.Println(v) // 5, 4, 3, 2, 1
}

Range-Over-Func Syntax (Go 1.23+)

// Iterate over custom sequences
type Tree[T any] struct {
    Left, Right *Tree[T]
    Value       T
}

func (t *Tree[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        if t == nil {
            return
        }
        for v := range t.Left.All() {
            if !yield(v) {
                return
            }
        }
        if !yield(t.Value) {
            return
        }
        for v := range t.Right.All() {
            if !yield(v) {
                return
            }
        }
    }
}

// Iterate over map in sorted key order
func SortedKeys[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        keys := slices.Sorted(maps.Keys(m))
        for _, k := range keys {
            if !yield(k, m[k]) {
                return
            }
        }
    }
}

Iterator Composition

// Filter iterator
func Filter[T any](seq iter.Seq[T], predicate func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if predicate(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

// Map iterator
func Map[T, U any](seq iter.Seq[T], transform func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for v := range seq {
            if !yield(transform(v)) {
                return
            }
        }
    }
}

// Take first n items
func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] {
    return func(yield func(T) bool) {
        count := 0
        for v := range seq {
            if count >= n {
                return
            }
            if !yield(v) {
                return
            }
            count++
        }
    }
}

// Compose: filter, map, and take
for user := range Take(
    Map(
        Filter(users.All(), func(u User) bool { return u.Active }),
        func(u User) string { return u.Email },
    ),
    10,
) {
    fmt.Println(user)
}

Standard Library Iterator Functions

import (
    "maps"
    "slices"
)

// maps package (Go 1.23+)
for k, v := range maps.All(m) { /* ... */ }
keys := slices.Collect(maps.Keys(m))
values := slices.Collect(maps.Values(m))

// slices package (Go 1.23+)
for i, v := range slices.All(s) { /* ... */ }
for v := range slices.Values(s) { /* ... */ }
for v := range slices.Backward(s) { /* ... */ }

// Collect iterator to slice
slice := slices.Collect(iterator)

// Chain iterators
combined := slices.Collect(iter.Chain(seq1, seq2))

Data Structures

new() vs make()

Function Use For Returns
new(T) Allocate zero-value *T (pointer)
make(T, ...) Slices, maps, channels T (initialized)
// new: allocates zeroed memory, returns pointer
p := new(User) // *User with zero values

// make: initializes slices, maps, channels
slice := make([]int, 0, 10)   // len=0, cap=10
m := make(map[string]int)      // initialized map
ch := make(chan int, 5)        // buffered channel

Prefer Slices Over Arrays

// Correct: slice for flexibility
func process(data []byte) error {
    // ...
}

// Avoid: fixed array limits flexibility
func process(data [1024]byte) error {
    // ...
}

Composite Literals

// Struct literal with field names
user := User{
    Name:  "Alice",
    Email: "alice@example.com",
    Age:   30,
}

// Slice literal
primes := []int{2, 3, 5, 7, 11}

// Map literal
lookup := map[string]int{
    "one":   1,
    "two":   2,
    "three": 3,
}

Comma-Ok Idiom

// Map access
value, ok := m[key]
if !ok {
    // key not present
}

// Type assertion
str, ok := v.(string)
if !ok {
    // v is not a string
}

// Channel receive
value, ok := <-ch
if !ok {
    // channel closed
}

Interfaces

Small Interfaces

Prefer interfaces with 1-2 methods.

// Good: small, focused interfaces
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Compose when needed
type ReadCloser interface {
    Reader
    Closer
}

// Avoid: large interfaces
type Repository interface {
    Create(ctx context.Context, item Item) error
    Read(ctx context.Context, id string) (Item, error)
    Update(ctx context.Context, item Item) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context, filter Filter) ([]Item, error)
    Count(ctx context.Context, filter Filter) (int, error)
    // ... too many methods
}

Accept Interfaces, Return Concrete

// Correct: accept interface, return concrete
func NewProcessor(r io.Reader) *Processor {
    return &Processor{reader: r}
}

// Wrong: return interface (hides implementation)
func NewProcessor(r io.Reader) Processor {
    return &processor{reader: r}
}

Implicit Satisfaction

Interfaces are satisfied implicitly; no implements keyword needed.

// Type implicitly satisfies io.Reader by having Read method
type MyReader struct {
    data []byte
    pos  int
}

func (r *MyReader) Read(p []byte) (n int, err error) {
    if r.pos >= len(r.data) {
        return 0, io.EOF
    }
    n = copy(p, r.data[r.pos:])
    r.pos += n
    return n, nil
}

// No need to declare: var _ io.Reader = (*MyReader)(nil)
// (though compile-time checks like this are acceptable)

Concurrency

Share by Communicating

// Correct: communicate via channels
func worker(jobs <-chan Job, results chan<- Result) {
    for job := range jobs {
        results <- process(job)
    }
}

// Instead of sharing memory with locks
type SharedState struct {
    mu    sync.Mutex
    count int
}

Channel Patterns

// Fan-out: multiple workers reading from one channel
jobs := make(chan Job, 100)
for i := 0; i < numWorkers; i++ {
    go worker(jobs, results)
}

// Fan-in: merge multiple channels into one
func merge(cs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, c := range cs {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Context for Cancellation

func fetch(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

// Usage with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

data, err := fetch(ctx, "https://api.example.com/data")

Goroutine Lifecycle Management

Always ensure goroutines can exit.

// Correct: goroutine respects cancellation
func startWorker(ctx context.Context) {
    go func() {
        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-ctx.Done():
                return // Clean exit
            case <-ticker.C:
                doWork()
            }
        }
    }()
}

// Wrong: goroutine runs forever
func startWorker() {
    go func() {
        for {
            time.Sleep(time.Second)
            doWork() // Never stops
        }
    }()
}

sync.WaitGroup for Coordination

func processAll(items []Item) {
    var wg sync.WaitGroup

    for _, item := range items {
        wg.Add(1)
        go func(it Item) {
            defer wg.Done()
            process(it)
        }(item) // Pass item to avoid closure capture bug
    }

    wg.Wait()
}

Modern Testing

Table-Driven Tests with Subtests

func TestParse(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    Result
        wantErr bool
    }{
        {
            name:  "valid input",
            input: "42",
            want:  Result{Value: 42},
        },
        {
            name:    "empty input",
            input:   "",
            wantErr: true,
        },
        {
            name:    "invalid format",
            input:   "not-a-number",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Parse(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !tt.wantErr && got != tt.want {
                t.Errorf("Parse() = %v, want %v", got, tt.want)
            }
        })
    }
}

Parallel Tests

func TestConcurrent(t *testing.T) {
    tests := []struct {
        name  string
        input string
    }{
        {"case1", "input1"},
        {"case2", "input2"},
    }

    for _, tt := range tests {
        tt := tt // Capture range variable (not needed in Go 1.22+)
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // Run subtests in parallel
            result := Process(tt.input)
            // assertions...
            _ = result
        })
    }
}

Fuzzing (go test -fuzz)

// Fuzz test discovers edge cases automatically
func FuzzParse(f *testing.F) {
    // Add seed corpus
    f.Add("42")
    f.Add("-1")
    f.Add("0")
    f.Add("")

    f.Fuzz(func(t *testing.T, input string) {
        result, err := Parse(input)
        if err != nil {
            return // Invalid input is ok
        }
        // Verify invariants on valid input
        if result.Value < 0 && !result.Negative {
            t.Errorf("negative value but Negative flag not set")
        }
        // Round-trip test
        if Format(result) != input {
            t.Errorf("round trip failed: %q -> %v -> %q", input, result, Format(result))
        }
    })
}

// Run: go test -fuzz=FuzzParse -fuzztime=30s

Testcontainers for Integration Tests

import (
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestWithDatabase(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    ctx := context.Background()

    // Start PostgreSQL container
    pgContainer, err := postgres.Run(ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer pgContainer.Terminate(ctx)

    // Get connection string
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    if err != nil {
        t.Fatal(err)
    }

    // Run tests against real database
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        t.Fatal(err)
    }
    defer db.Close()

    // Test your repository
    repo := NewUserRepository(db)
    // assertions...
    _ = repo
}

Test Helpers and Assertions

// t.Helper marks function as test helper
func assertNil(t testing.TB, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("expected nil error, got: %v", err)
    }
}

func assertEqual[T comparable](t testing.TB, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

// t.Cleanup for resource cleanup
func setupTestServer(t *testing.T) *httptest.Server {
    srv := httptest.NewServer(http.HandlerFunc(handler))
    t.Cleanup(srv.Close) // Automatic cleanup
    return srv
}

// Golden file testing
func TestOutput(t *testing.T) {
    got := Generate()
    golden := filepath.Join("testdata", t.Name()+".golden")

    if *update {
        os.WriteFile(golden, []byte(got), 0644)
    }

    want, _ := os.ReadFile(golden)
    if got != string(want) {
        t.Errorf("output mismatch, run with -update to refresh golden files")
    }
}

Module Management

Workspace Mode (go.work)

// go.work - multi-module development
go 1.22

use (
    ./api
    ./sdk
    ./internal/shared
)

// Commands
// go work init ./api ./sdk     - create workspace
// go work use ./new-module     - add module to workspace
// go work sync                 - sync dependencies

Private Module Authentication

# .netrc for private modules
machine github.com
    login USERNAME
    password TOKEN

# Or use GOPRIVATE
export GOPRIVATE=github.com/myorg/*,gitlab.com/mycompany/*

# Git config for SSH
git config --global url."git@github.com:".insteadOf "https://github.com/"

go.mod Best Practices

// go.mod
module github.com/myorg/myproject

go 1.22

require (
    github.com/lib/pq v1.10.9
    golang.org/x/sync v0.6.0
)

// Tooling dependencies (not imported)
tool (
    golang.org/x/tools/cmd/stringer
    github.com/golangci/golangci-lint/cmd/golangci-lint
)

Vulnerability Scanning

# Install govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest

# Scan project
govulncheck ./...

# Scan specific binary
govulncheck -mode=binary ./cmd/server

# Common workflow in CI
govulncheck -format json ./... > vuln-report.json

Version Selection

# Get specific version
go get github.com/pkg/errors@v0.9.1

# Get latest minor version
go get github.com/pkg/errors@v0.9

# Get latest version
go get github.com/pkg/errors@latest

# Upgrade all dependencies
go get -u ./...

# Upgrade only patch versions
go get -u=patch ./...

# Show why a dependency is needed
go mod why github.com/pkg/errors

# Show available versions
go list -m -versions github.com/pkg/errors

Tooling

golangci-lint Configuration

# .golangci.yml
run:
  timeout: 5m
  tests: true

linters:
  enable:
    - errcheck
    - govet
    - staticcheck
    - unused
    - gosimple
    - ineffassign
    - typecheck
    - gocritic
    - revive
    - gofmt
    - goimports
    - misspell
    - unconvert
    - unparam
    - nilerr
    - errorlint      # error wrapping checks
    - exhaustive     # enum switch exhaustiveness
    - nilnil         # nil return with nil error
    - noctx          # HTTP requests without context

linters-settings:
  errcheck:
    check-type-assertions: true
    check-blank: true

  gocritic:
    enabled-tags:
      - diagnostic
      - style
      - performance

  revive:
    rules:
      - name: exported
        arguments: [checkPrivateReceivers]
      - name: blank-imports
      - name: context-as-argument
      - name: error-return
      - name: error-strings

  errorlint:
    errorf: true
    asserts: true
    comparison: true

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - errcheck
        - gocritic

Makefile Integration

.PHONY: lint test build

lint:
	golangci-lint run ./...

test:
	go test -race -coverprofile=coverage.out ./...
	go tool cover -html=coverage.out -o coverage.html

fuzz:
	go test -fuzz=. -fuzztime=60s ./...

vuln:
	govulncheck ./...

build:
	go build -o bin/server ./cmd/server

all: lint test vuln build

gopls Integration (Editor Setup)

// VSCode settings.json
{
  "go.useLanguageServer": true,
  "gopls": {
    "analyses": {
      "unusedparams": true,
      "shadow": true,
      "nilness": true
    },
    "staticcheck": true,
    "gofumpt": true,
    "hints": {
      "assignVariableTypes": true,
      "compositeLiteralFields": true,
      "compositeLiteralTypes": true,
      "constantValues": true,
      "functionTypeParameters": true,
      "parameterNames": true,
      "rangeVariableTypes": true
    }
  }
}

Performance

Profile-Guided Optimization (PGO)

# Step 1: Build and run with profiling
go build -o server ./cmd/server
./server &
# Generate load...
curl http://localhost:8080/api/heavy-endpoint
# Collect CPU profile
go tool pprof -proto -output=default.pgo http://localhost:8080/debug/pprof/profile?seconds=30

# Step 2: Rebuild with PGO
go build -pgo=default.pgo -o server ./cmd/server

# Or place profile in source directory (auto-detected)
mv default.pgo ./cmd/server/default.pgo
go build -pgo=auto -o server ./cmd/server

Memory Profiling Patterns

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
)

func main() {
    // Enable pprof endpoint
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()

    // Your application...
}

// Analyze with:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/allocs
// Reduce allocations
// Wrong: creates new slice each call
func process(items []Item) []Result {
    results := []Result{} // Allocates with default capacity
    for _, item := range items {
        results = append(results, transform(item))
    }
    return results
}

// Correct: pre-allocate
func process(items []Item) []Result {
    results := make([]Result, 0, len(items)) // Pre-allocate
    for _, item := range items {
        results = append(results, transform(item))
    }
    return results
}

// Use sync.Pool for frequently allocated objects
var bufPool = sync.Pool{
    New: func() any { return new(bytes.Buffer) },
}

func processRequest(data []byte) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()

    buf.Write(data)
    // process...
    return buf.String()
}

Benchmarking Best Practices

func BenchmarkParse(b *testing.B) {
    input := []byte(`{"name": "test", "value": 42}`)

    b.ResetTimer() // Reset after setup
    b.ReportAllocs() // Report allocations

    for i := 0; i < b.N; i++ {
        _, err := Parse(input)
        if err != nil {
            b.Fatal(err)
        }
    }
}

// Sub-benchmarks for comparison
func BenchmarkEncode(b *testing.B) {
    data := generateTestData()

    b.Run("JSON", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            json.Marshal(data)
        }
    })

    b.Run("Gob", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var buf bytes.Buffer
            gob.NewEncoder(&buf).Encode(data)
        }
    })
}

// Prevent compiler optimization
var result Result

func BenchmarkCompute(b *testing.B) {
    var r Result
    for i := 0; i < b.N; i++ {
        r = Compute(input) // Use result
    }
    result = r // Assign to package var to prevent optimization
}
# Run benchmarks
go test -bench=. -benchmem ./...

# Compare benchmarks
go test -bench=. -count=10 > old.txt
# Make changes...
go test -bench=. -count=10 > new.txt
benchstat old.txt new.txt

Anti-Patterns to Flag

Use this checklist during code review.

Critical (Request Changes)

Anti-Pattern Issue Fix
Ignored errors f, _ := os.Open(file) Check and handle error
Goroutine leak No exit condition Add context cancellation
Data race Shared mutable state without sync Use channels or mutex
Naked return in long function Unclear what's returned Explicit return values
%v instead of %w for wrapping Breaks error chain Use fmt.Errorf("...: %w", err)
Known vulnerabilities Outdated dependencies Run govulncheck ./...

High (Request Changes)

Anti-Pattern Issue Fix
Stuttering names user.UserService user.Service
Get prefix on getters GetName() Name()
Large interface 5+ methods Split into small interfaces
Unnecessary else if x { return } else { ... } Remove else
Generic with any constraint Loses type safety Use specific constraint
Missing context parameter Can't cancel/timeout Accept context.Context first

Medium (Comment)

Anti-Pattern Issue Fix
Underscores in names max_count maxCount
Long if-else chains 3+ conditions Use switch
Defer far from resource Hard to track cleanup Defer immediately after acquisition
Fixed-size array params [N]byte Use slice []byte
Not using errors.Join Multiple errors discarded Collect and join errors
No test subtests Hard to identify failures Use t.Run(name, func...)

Low (Suggestion)

Anti-Pattern Issue Fix
Inconsistent acronym casing userId, Url userID, URL
Missing doc comment on exported No documentation Add // Name does X
Complex boolean expression Hard to read Extract to named variable
Not using iterators Verbose collection code Use iter.Seq patterns
No benchmark tests Unknown performance Add BenchmarkX functions

Review Checklist

When reviewing Go code, verify:

Fundamentals

  • gofmt has been run (no formatting issues)
  • All errors are checked (no _ for error values)
  • Naming follows conventions (no stuttering, correct casing)
  • Interfaces are small (1-2 methods preferred)
  • Goroutines have exit conditions (context, done channel)
  • Resources are cleaned up with defer (immediately after acquisition)
  • Happy path flows down (errors handled and returned early)
  • No unnecessary else after return
  • Context passed through for cancellation
  • Composite literals use field names

Modern Go (1.18+)

  • Generics used appropriately (not overused, proper constraints)
  • Error wrapping uses %w (not %v)
  • Multiple errors combined with errors.Join
  • Dependencies scanned with govulncheck
  • Tests use subtests (t.Run) for clarity
  • Benchmarks exist for performance-critical code
  • golangci-lint passes with project config

Quick Reference

Naming

Package:    lowercase, single word
Exported:   UpperCamelCase
Unexported: lowerCamelCase
Acronyms:   consistent case (ID, URL, HTTP)
Getter:     Name() not GetName()
Setter:     SetName()
Interface:  Reader, Writer (method + er)

Error Handling

// Check
if err != nil { return err }

// Wrap (use %w, not %v)
return fmt.Errorf("context: %w", err)

// Check type
errors.Is(err, ErrNotFound)
errors.As(err, &target)

// Multiple errors (Go 1.20+)
return errors.Join(err1, err2)

Generics (Go 1.18+)

// Type constraint
func Min[T constraints.Ordered](a, b T) T

// Generic type
type Cache[K comparable, V any] struct{}

// Custom constraint
type Number interface { ~int | ~float64 }

Iterators (Go 1.22+)

// Define iterator
func All() iter.Seq[T] {
    return func(yield func(T) bool) { /* ... */ }
}

// Use iterator
for v := range collection.All() { }

// Collect to slice
slice := slices.Collect(iterator)

Concurrency

// Channel: unbuffered
ch := make(chan T)

// Channel: buffered
ch := make(chan T, size)

// Context: with timeout
ctx, cancel := context.WithTimeout(parent, duration)
defer cancel()

// Wait group
var wg sync.WaitGroup
wg.Add(n)
go func() { defer wg.Done(); /* work */ }()
wg.Wait()

Testing

# Table tests with subtests
t.Run("name", func(t *testing.T) { ... })

# Fuzzing
go test -fuzz=FuzzParse -fuzztime=30s

# Benchmarking
go test -bench=. -benchmem ./...

Tooling

# Lint
golangci-lint run ./...

# Vulnerability scan
govulncheck ./...

# Profile-guided optimization
go build -pgo=default.pgo ./...

Integration with Reviewers

This skill provides reference material for:

Agent Usage
code-quality-reviewer Anti-pattern detection, complexity
security-reviewer Goroutine leaks, race conditions, vulnerabilities
go-reviewer (if created) Language-specific idiom enforcement

When reviewing Go code, agents should cross-reference this skill for language-specific guidance.


Version History

Go Version Key Features in This Guide
1.18 Generics, type constraints
1.20 errors.Join, arena package
1.21 log/slog, maps, slices packages
1.22 Range over integers, iter package
1.23 Range-over-func, iterator standard library
Weekly Installs
2
First Seen
Mar 2, 2026
Installed on
opencode2
gemini-cli2
claude-code2
github-copilot2
codex2
kimi-cli2