saisudhir14

golang

2
0
# Install this skill:
npx skills add saisudhir14/golang-agent-skill

Or install specific skill: npx add-skill https://github.com/saisudhir14/golang-agent-skill

# Description

Best practices for writing production Go code. Use when writing, reviewing, or refactoring Go code. Covers error handling, concurrency, naming conventions, testing patterns, performance optimization, generics, and common pitfalls. Based on Google Go Style Guide, Uber Go Style Guide, Effective Go, and Go Code Review Comments. Updated for Go 1.25.

# SKILL.md


name: golang
description: Best practices for writing production Go code. Use when writing, reviewing, or refactoring Go code. Covers error handling, concurrency, naming conventions, testing patterns, performance optimization, generics, and common pitfalls. Based on Google Go Style Guide, Uber Go Style Guide, Effective Go, and Go Code Review Comments. Updated for Go 1.25.


Go Best Practices

Battle-tested patterns from Google, Uber, and the Go team. These are practices proven in large-scale production systems, updated for modern Go (1.25).

Core Principles

Readable code prioritizes these attributes in order:

  1. Clarity: purpose and rationale are obvious to the reader
  2. Simplicity: accomplishes the goal in the simplest way
  3. Concision: high signal to noise ratio
  4. Maintainability: easy to modify correctly
  5. Consistency: matches surrounding codebase

Error Handling

Return Errors, Do Not Panic

Production code must avoid panics. Return errors and let callers decide how to handle them.

// Wrong
func run(args []string) {
    if len(args) == 0 {
        panic("an argument is required")
    }
}

// Correct
func run(args []string) error {
    if len(args) == 0 {
        return errors.New("an argument is required")
    }
    return nil
}

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Error Wrapping

Use %w when callers need to inspect the underlying error with errors.Is or errors.As. Use %v when you want to hide implementation details or at system boundaries.

// Preserve error chain for programmatic inspection
if err != nil {
    return fmt.Errorf("load config: %w", err)
}

// Hide internal details at API boundaries
if err != nil {
    return fmt.Errorf("database unavailable: %v", err)
}

Keep context succinct. Avoid phrases like "failed to" that pile up as errors propagate.

// Wrong: produces "failed to x: failed to y: failed to create store: the error"
return fmt.Errorf("failed to create new store: %w", err)

// Correct: produces "x: y: new store: the error"
return fmt.Errorf("new store: %w", err)

Joining Multiple Errors (Go 1.20+)

Use errors.Join when multiple operations can fail independently.

func validateUser(u User) error {
    var errs []error
    if u.Name == "" {
        errs = append(errs, errors.New("name required"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email required"))
    }
    return errors.Join(errs...)
}

// Checking joined errors
if err := validateUser(u); err != nil {
    if errors.Is(err, ErrNameRequired) {
        // handles even when joined with other errors
    }
}

Error Types

Choose based on caller needs:

Caller needs to match? Message type Approach
No Static errors.New("something bad")
No Dynamic fmt.Errorf("file %q not found", file)
Yes Static Exported var ErrNotFound = errors.New("not found")
Yes Dynamic Custom error type with Error() method

Sentinel Errors and errors.Is

Define sentinel errors for conditions callers need to check.

var (
    ErrNotFound    = errors.New("not found")
    ErrInvalidUser = errors.New("invalid user")
)

// Checking wrapped errors
if errors.Is(err, ErrNotFound) {
    // handles ErrNotFound even when wrapped
}

// Custom error types use errors.As
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("failed path:", pathErr.Path)
}

Error Naming

Exported error variables use Err prefix. Custom error types use Error suffix.

var (
    ErrNotFound    = errors.New("not found")
    ErrInvalidUser = errors.New("invalid user")
)

type NotFoundError struct {
    Resource string
}

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

Handle Errors Once

Do not log an error and also return it. The caller will likely log it again.

// Wrong: logs and returns, causing duplicate logs
if err != nil {
    log.Printf("could not get user %q: %v", id, err)
    return err
}

// Correct: wrap and return, let caller decide
if err != nil {
    return fmt.Errorf("get user %q: %w", id, err)
}

// Also correct: log and degrade gracefully without returning error
if err := emitMetrics(); err != nil {
    log.Printf("could not emit metrics: %v", err)
}

Error Strings

Do not capitalize error strings or end with punctuation. They often appear mid-sentence in logs.

// Wrong
fmt.Errorf("Something bad happened.")

// Correct
fmt.Errorf("something bad happened")

Indent Error Flow

Keep the happy path at minimal indentation. Handle errors first.

// Wrong
if err != nil {
    // error handling
} else {
    // normal code
}

// Correct
if err != nil {
    return err
}
// normal code continues

Concurrency

Channel Size

Channels should have size zero (unbuffered) or one. Any other size requires justification about what prevents filling under load.

// Wrong: arbitrary buffer
c := make(chan int, 64)

// Correct
c := make(chan int)    // unbuffered: synchronous handoff
c := make(chan int, 1) // buffered: allows one pending send

Goroutine Lifetimes

Document when and how goroutines exit. Goroutines blocked on channels will not be garbage collected even if the channel is unreachable.

// Document exit conditions
func (w *Worker) Run(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case job := <-w.jobs:
            w.process(job)
        }
    }
}

Use errgroup for Concurrent Operations

Prefer errgroup.Group over manual sync.WaitGroup for error-returning goroutines.

import "golang.org/x/sync/errgroup"

func processItems(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, item := range items {
        g.Go(func() error {
            return process(ctx, item)
        })
    }

    return g.Wait() // returns first error, cancels others via ctx
}

// With concurrency limit
func processItemsLimited(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // max 10 concurrent goroutines

    for _, item := range items {
        g.Go(func() error {
            return process(ctx, item)
        })
    }

    return g.Wait()
}

Prefer Synchronous Functions

Synchronous functions are easier to reason about and test. Let callers add concurrency when needed.

// Wrong: forces concurrency on caller
func Fetch(url string) <-chan Result

// Correct: caller can wrap in goroutine if needed
func Fetch(url string) (Result, error)

Zero Value Mutexes

The zero value of sync.Mutex is valid. Do not use pointers to mutexes or embed them in exported structs.

// Wrong
mu := new(sync.Mutex)

// Wrong: exposes Lock/Unlock in API
type SMap struct {
    sync.Mutex
    data map[string]string
}

// Correct
type SMap struct {
    mu   sync.Mutex
    data map[string]string
}

Atomic Operations (Go 1.19+)

Use the standard library's typed atomics. External packages are no longer necessary.

import "sync/atomic"

type Counter struct {
    value atomic.Int64
}

func (c *Counter) Inc() {
    c.value.Add(1)
}

func (c *Counter) Value() int64 {
    return c.value.Load()
}

// Also available: atomic.Bool, atomic.Pointer[T], atomic.Uint32, etc.

sync.Map Performance (Go 1.24+)

The sync.Map implementation was significantly improved in Go 1.24. Modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads.


Naming

MixedCaps Always

Go uses MixedCaps, never underscores. This applies even when it breaks other language conventions.

// Wrong
MAX_LENGTH, max_length, HTTP_Server

// Correct
MaxLength, maxLength, HTTPServer

Initialisms

Initialisms maintain consistent case: URL not Url, ID not Id, HTTP not Http.

// Wrong
xmlHttpRequest, serverId, apiUrl

// Correct
xmlHTTPRequest, serverID, apiURL

Short Variable Names

Variables should be short, especially with limited scope. The further from declaration a name is used, the more descriptive it needs to be.

// Good for local scope
for i, v := range items { }
r := bufio.NewReader(f)

// Global or struct fields need more context
var DefaultTimeout = 30 * time.Second

Receiver Names

Use one or two letter abbreviations of the type. Be consistent across methods. Do not use generic names like this, self, or me.

// Wrong
func (this *Client) Get() {}
func (c *Client) Get() {}
func (cl *Client) Post() {} // inconsistent

// Correct
func (c *Client) Get() {}
func (c *Client) Post() {}

Pointer vs Value Receivers

Use pointer receiver when Use value receiver when
Method modifies the receiver Struct is small and immutable
Struct is large (avoid copying) Method doesn't modify state
Consistency with other methods Receiver is a map, func, or chan
Struct contains sync.Mutex Basic types (int, string, etc.)
// Pointer: modifies state
func (s *Server) Shutdown() error {
    s.running = false
    return s.listener.Close()
}

// Value: small, read-only
func (p Point) Distance(q Point) float64 {
    return math.Hypot(p.X-q.X, p.Y-q.Y)
}

Package Names

Package names are lowercase, single words. Avoid util, common, misc, api, types. The package name becomes part of the identifier at call sites.

// Wrong
package chubby
type ChubbyFile struct{}  // chubby.ChubbyFile is redundant

// Correct
package chubby
type File struct{}  // chubby.File reads well

Avoid Repetition in Names

Do not repeat package or receiver names in function names.

// Wrong
package http
func HTTPServe() {}  // http.HTTPServe is redundant

func (c *Config) WriteConfigTo(w io.Writer) {}  // Config repeated

// Correct
package http
func Serve() {}  // http.Serve

func (c *Config) WriteTo(w io.Writer) {}

Imports

Grouping

Organize imports in three groups separated by blank lines: standard library, external packages, internal packages.

import (
    "context"
    "fmt"
    "os"

    "github.com/google/uuid"
    "golang.org/x/sync/errgroup"

    "yourcompany/internal/config"
    "yourcompany/internal/metrics"
)

Avoid Renaming

Rename imports only to avoid collisions. Prefer renaming the most local import.

Avoid Import Dot

The dot import (import . "pkg") makes code harder to read. Use only in test files with circular dependencies.

Blank Imports

Import for side effects (import _ "pkg") only in main packages or tests.


Module Management

Tool Directives (Go 1.24+)

Go modules can now track executable dependencies using tool directives in go.mod. This removes the need for the previous workaround of adding tools as blank imports to a file conventionally named "tools.go".

// go.mod
module example.com/myproject

go 1.24

tool (
    golang.org/x/tools/cmd/stringer
    github.com/golangci/golangci-lint/cmd/golangci-lint
)
# Add a tool dependency
go get -tool golang.org/x/tools/cmd/stringer

# Run a tool
go tool stringer -type=Status

# Update all tools
go get tool

# Install all tools to GOBIN
go install tool

Structs

Use Field Names in Initialization

Always use field names. Positional arguments break when fields are added.

// Wrong: breaks if fields change
k := User{"John", "[email protected]", true}

// Correct
k := User{
    Name:   "John",
    Email:  "[email protected]",
    Active: true,
}

Omit Zero Value Fields

Do not initialize fields to their zero values.

// Wrong
user := User{
    Name:   "John",
    Active: false,  // false is zero value
    Count:  0,      // 0 is zero value
}

// Correct
user := User{
    Name: "John",
}

Embedding

Do not embed types in public structs. Embedding exposes methods and fields to the public API unintentionally.

// Wrong: Lock and Unlock become part of SMap's API
type SMap struct {
    sync.Mutex
    data map[string]string
}

// Correct
type SMap struct {
    mu   sync.Mutex
    data map[string]string
}

Use var for Zero Value Structs

// Correct
var user User

// Also acceptable
user := User{}

Slices and Maps

Nil Slice Declaration

Prefer nil slices over empty slices. They are functionally equivalent but nil is the preferred style.

// Preferred
var t []string

// Use only when JSON must encode as [] instead of null
t := []string{}

Copy at Boundaries

Slices and maps hold references. Copy them when storing or returning to prevent mutation.

// Wrong: caller can modify internal state
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = trips
}

// Correct
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = make([]Trip, len(trips))
    copy(d.trips, trips)
}

// For maps
func (d *Driver) SetMetadata(m map[string]string) {
    d.metadata = maps.Clone(m)
}

Specify Capacity

Preallocate when size is known. This reduces allocations.

// Wrong
var result []Item
for _, v := range input {
    result = append(result, transform(v))
}

// Correct
result := make([]Item, 0, len(input))
for _, v := range input {
    result = append(result, transform(v))
}

Use slices and maps Packages

Prefer standard library functions for common operations.

import (
    "cmp"
    "maps"
    "slices"
)

// Sorting
slices.Sort(numbers)
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Name, b.Name)
})

// Searching
idx, found := slices.BinarySearch(sorted, target)

// Cloning
copy := slices.Clone(original)
mapCopy := maps.Clone(original)

// Comparison
if slices.Equal(a, b) { }
if maps.Equal(m1, m2) { }

Generics (Go 1.18+)

When to Use Generics

Use generics when you find yourself writing the same code for different types.

// Generic helper functions
func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := make([]T, 0, len(slice))
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Map[T, U any](slice []T, transform func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = transform(v)
    }
    return result
}

// Usage
adults := Filter(users, func(u User) bool { return u.Age >= 18 })
names := Map(users, func(u User) string { return u.Name })

Type Constraints

Use constraints for type safety.

import "cmp"

// cmp.Ordered covers all comparable types
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

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

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

Generic Type Aliases (Go 1.24+)

Type aliases can now be parameterized like defined types.

// Generic type alias
type Set[T comparable] = map[T]struct{}

// Usage
var s Set[string]
s = make(Set[string])
s["hello"] = struct{}{}

// With constraints
type OrderedSlice[T cmp.Ordered] = []T

Avoid Over-Generalization

Do not use generics when a concrete type or interface suffices.

// Wrong: unnecessary generic
func PrintAll[T fmt.Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

// Correct: interface is sufficient
func PrintAll(items []fmt.Stringer) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

Iterators (Go 1.23+)

Range Over Functions

Go 1.23 introduced range-over-func, allowing custom iterators.

// Iterator function signature
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

// Custom iterator
func Backward[T any](s []T) func(yield func(int, T) bool) {
    return func(yield func(int, T) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
    }
}

// Usage
for i, v := range Backward(items) {
    fmt.Println(i, v)
}

String and Bytes Iterators (Go 1.24+)

New iterator functions for efficient string and byte processing.

import "strings"

// Iterate over lines (includes newline characters)
text := "line1\nline2\nline3"
for line := range strings.Lines(text) {
    fmt.Print(line)
}

// Split by delimiter (iterator, no slice allocation)
csvData := "apple,banana,cherry"
for value := range strings.SplitSeq(csvData, ",") {
    fmt.Println(value)
}

// Split after delimiter
for part := range strings.SplitAfterSeq(csvData, ",") {
    fmt.Println(part) // "apple," "banana," "cherry"
}

// Equivalent functions exist in bytes package

Structured Logging (Go 1.21+)

Use slog for New Code

The standard library now includes structured logging.

import "log/slog"

// Basic usage
slog.Info("user created", "id", userID, "email", email)
slog.Error("request failed", "err", err, "method", r.Method)

// With context
logger := slog.With("service", "auth", "version", "1.0")
logger.Info("starting")

// JSON output for production
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
})
slog.SetDefault(slog.New(handler))

slog.DiscardHandler (Go 1.24+)

Use the built-in discard handler for suppressing logs in tests.

// Before Go 1.24
log := slog.New(slog.NewJSONHandler(io.Discard, nil))

// Go 1.24+
log := slog.New(slog.DiscardHandler)

Structured Logging Best Practices

// Use consistent key names
slog.Info("request completed",
    "method", r.Method,
    "path", r.URL.Path,
    "status", statusCode,
    "duration_ms", duration.Milliseconds(),
)

// Group related fields
slog.Info("user action",
    slog.Group("user",
        "id", user.ID,
        "role", user.Role,
    ),
    slog.Group("request",
        "method", r.Method,
        "path", r.URL.Path,
    ),
)

Performance

Prefer strconv Over fmt

strconv is faster for primitive conversions.

// Slower
s := fmt.Sprintf("%d", n)

// Faster
s := strconv.Itoa(n)

Avoid Repeated String to Byte Conversions

// Wrong: converts on every iteration
for i := 0; i < n; i++ {
    w.Write([]byte("hello"))
}

// Correct
data := []byte("hello")
for i := 0; i < n; i++ {
    w.Write(data)
}

Specify Map Capacity

// Wrong
m := make(map[string]int)

// Correct when size is known
m := make(map[string]int, len(items))

Use strings.Builder for Concatenation

// Wrong: creates many allocations
var s string
for _, part := range parts {
    s += part
}

// Correct
var b strings.Builder
b.Grow(totalLen) // optional: preallocate
for _, part := range parts {
    b.WriteString(part)
}
s := b.String()

Testing

Table Driven Tests with Parallel Execution

Use table driven tests to avoid code duplication. Run subtests in parallel when safe.

func TestSplit(t *testing.T) {
    tests := []struct {
        name  string
        input string
        sep   string
        want  []string
    }{
        {
            name:  "simple",
            input: "a/b/c",
            sep:   "/",
            want:  []string{"a", "b", "c"},
        },
        {
            name:  "empty",
            input: "",
            sep:   "/",
            want:  []string{""},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // run subtests concurrently

            got := strings.Split(tt.input, tt.sep)
            if diff := cmp.Diff(tt.want, got); diff != "" {
                t.Errorf("Split() mismatch (-want +got):\n%s", diff)
            }
        })
    }
}

T.Context and T.Chdir (Go 1.24+)

New helper methods for test context and working directory.

func TestWithContext(t *testing.T) {
    // T.Context returns a context canceled after test completes
    // but before cleanup functions run
    ctx := t.Context()

    result, err := doWork(ctx)
    if err != nil {
        t.Fatal(err)
    }
    // ...
}

func TestWithChdir(t *testing.T) {
    // T.Chdir changes working directory for duration of test
    // and automatically restores it after
    t.Chdir("testdata")

    // Now in testdata directory
    data, err := os.ReadFile("input.txt")
    // ...
}

Benchmark with b.Loop (Go 1.24+)

Use b.Loop() for cleaner, more accurate benchmarks.

// Old way - error prone
func BenchmarkOld(b *testing.B) {
    input := setupInput() // counted in benchmark time!
    b.ResetTimer()        // easy to forget
    for i := 0; i < b.N; i++ {
        process(input)    // compiler might optimize away
    }
}

// Go 1.24+ - preferred
func BenchmarkNew(b *testing.B) {
    input := setupInput() // setup runs once, excluded from timing
    for b.Loop() {
        process(input)    // compiler cannot optimize away
    }
}

Benefits of b.Loop():
- Setup code runs exactly once per -count, automatically excluded from timing
- No need to call b.ResetTimer()
- Function call parameters and results are kept alive, preventing compiler optimization

Testing Concurrent Code with synctest (Go 1.25+)

The testing/synctest package provides deterministic testing for concurrent code using synthetic time.

import "testing/synctest"

func TestTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
        defer cancel()

        // Inside the "bubble", time is synthetic
        // This sleep completes instantly in real time
        time.Sleep(4 * time.Second)

        // Context should not be expired yet
        if err := ctx.Err(); err != nil {
            t.Fatalf("unexpected timeout: %v", err)
        }

        // Advance past the timeout
        time.Sleep(2 * time.Second)

        // Now it should be expired
        if ctx.Err() != context.DeadlineExceeded {
            t.Fatal("expected deadline exceeded")
        }
    })
}

Key concepts:
- synctest.Test creates an isolated "bubble" with synthetic time
- Time only advances when all goroutines in the bubble are blocked
- Initial time is midnight UTC 2000-01-01
- synctest.Wait() waits for all goroutines to be durably blocked

func TestConcurrentCounter(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var counter atomic.Int64
        var wg sync.WaitGroup

        // Start concurrent workers
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                counter.Add(1)
            }()
        }

        // Wait for all goroutines to complete
        wg.Wait()

        // Counter is now deterministically 10
        if got := counter.Load(); got != 10 {
            t.Errorf("got %d, want 10", got)
        }
    })
}

// Example with time-based operations
func TestPeriodicTask(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var count atomic.Int64
        ctx, cancel := context.WithCancel(t.Context())

        // Start a periodic task
        go func() {
            ticker := time.NewTicker(100 * time.Millisecond)
            defer ticker.Stop()
            for {
                select {
                case <-ctx.Done():
                    return
                case <-ticker.C:
                    count.Add(1)
                }
            }
        }()

        // Advance synthetic time by 350ms
        time.Sleep(350 * time.Millisecond)
        synctest.Wait() // wait for goroutine to process

        cancel()
        synctest.Wait() // wait for goroutine to exit

        // Should have ticked 3 times (at 100ms, 200ms, 300ms)
        if got := count.Load(); got != 3 {
            t.Errorf("got %d ticks, want 3", got)
        }
    })
}

Important restrictions in synctest bubbles:
- Do not call t.Run(), t.Parallel(), or t.Deadline()
- Channels created outside the bubble behave differently
- External I/O operations are not durably blocking

Use go-cmp for Comparisons

Prefer github.com/google/go-cmp/cmp over reflect.DeepEqual.

import "github.com/google/go-cmp/cmp"

// Clear diff output on failure
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}

// With options for custom comparison
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(User{})); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}

Useful Test Failures

Include: what was wrong, inputs, actual result, expected result.

// Wrong
if got != want {
    t.Error("wrong result")
}

// Correct
if got != want {
    t.Errorf("Foo(%q) = %d; want %d", input, got, want)
}

Use t.Fatal for Setup Failures

f, err := os.CreateTemp("", "test")
if err != nil {
    t.Fatal("failed to set up test")
}

Interfaces Belong to Consumers

Define interfaces in the package that uses them, not the package that implements them.

// Wrong: defining interface in producer
package producer
type Thinger interface { Thing() bool }
func NewThinger() Thinger { return &thinger{} }

// Correct: producer returns concrete type
package producer
type Thinger struct{}
func (t *Thinger) Thing() bool { return true }
func NewThinger() *Thinger { return &Thinger{} }

// Consumer defines interface it needs
package consumer
type Thinger interface { Thing() bool }
func Process(t Thinger) { }

Resource Management

runtime.AddCleanup (Go 1.24+)

Prefer runtime.AddCleanup over runtime.SetFinalizer for cleanup operations.

import "runtime"

type Resource struct {
    handle uintptr
}

func NewResource() *Resource {
    r := &Resource{handle: allocHandle()}

    // AddCleanup is more flexible than SetFinalizer:
    // - Multiple cleanups can be attached to one object
    // - Works with interior pointers
    // - Doesn't cause leaks with cycles
    // - Doesn't delay freeing the object
    runtime.AddCleanup(r, func(handle uintptr) {
        freeHandle(handle)
    }, r.handle)

    return r
}

Key advantages over SetFinalizer:
- Multiple cleanups per object
- Works with interior pointers
- No cycle-related leaks
- Object freed promptly (single GC cycle)

Weak Pointers (Go 1.24+)

The weak package provides weak references that don't prevent garbage collection.

import "weak"

// Create a weak pointer from a strong pointer
type ExpensiveResource struct {
    data []byte
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]weak.Pointer[ExpensiveResource]),
    }
}

type Cache struct {
    mu    sync.Mutex
    items map[string]weak.Pointer[ExpensiveResource]
}

func (c *Cache) Get(key string) *ExpensiveResource {
    c.mu.Lock()
    defer c.mu.Unlock()

    if wp, ok := c.items[key]; ok {
        // Value returns the original pointer, or nil if collected
        if r := wp.Value(); r != nil {
            return r
        }
        // Resource was garbage collected, remove stale entry
        delete(c.items, key)
    }
    return nil
}

func (c *Cache) Set(key string, r *ExpensiveResource) {
    c.mu.Lock()
    defer c.mu.Unlock()
    // Make creates a weak pointer from a strong pointer
    c.items[key] = weak.Make(r)
}

Use cases for weak pointers:
- Caches that shouldn't prevent garbage collection
- Canonicalization maps (interning)
- Observer patterns where observers may be collected

Secure Directory Access with os.Root (Go 1.24+)

The os.Root type provides safe, scoped file system access that prevents path traversal attacks.

import "os"

func ServeUserFiles(userDir string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // OpenRoot opens a directory as a root for safe access
        root, err := os.OpenRoot(userDir)
        if err != nil {
            http.Error(w, "directory not found", http.StatusNotFound)
            return
        }
        defer root.Close()

        // Open is safe: paths are resolved relative to root
        // Attempts to escape (like "../etc/passwd") are rejected
        filename := r.URL.Query().Get("file")
        f, err := root.Open(filename)
        if err != nil {
            http.Error(w, "file not found", http.StatusNotFound)
            return
        }
        defer f.Close()

        io.Copy(w, f)
    }
}

// Available methods on os.Root:
// - Open(name) - open file for reading
// - Create(name) - create or truncate file
// - OpenFile(name, flag, perm) - open with flags
// - Mkdir(name, perm) - create directory
// - Remove(name) - remove file or empty directory
// - Stat(name), Lstat(name) - file info
// - ReadDir(name) - list directory contents

Key benefits:
- Prevents path traversal vulnerabilities ("../" attacks)
- Symlinks cannot escape the root directory
- Race-condition safe (uses openat2 on Linux)
- Drop-in replacement for typical file operations


Patterns

Functional Options

Use functional options for configurable constructors with many optional parameters.

type Server struct {
    addr    string
    timeout time.Duration
    logger  *slog.Logger
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithLogger(l *slog.Logger) Option {
    return func(s *Server) { s.logger = l }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
        addr:    addr,
        timeout: 30 * time.Second,
        logger:  slog.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
srv := NewServer("localhost:8080",
    WithTimeout(60*time.Second),
    WithLogger(logger),
)

Verify Interface Compliance

Use compile time checks to verify interface implementations.

type Handler struct{}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}

Defer for Cleanup

Use defer to clean up resources. The small overhead is worth the readability and safety.

p.Lock()
defer p.Unlock()

if p.count < 10 {
    return p.count
}
p.count++
return p.count

Graceful Shutdown Pattern

Production servers need graceful shutdown to drain connections.

func main() {
    srv := &http.Server{Addr: ":8080", Handler: handler}

    // Start server in goroutine
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("server error", "err", err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown error", "err", err)
    }
    slog.Info("server stopped")
}

Start Enums at One

Zero values should represent invalid or unset state.

type Operation int

const (
    OperationUnknown Operation = iota // 0 = invalid
    OperationAdd                      // 1
    OperationSubtract                 // 2
)

Use time Package for Time

Do not use integers for time. Use time.Time for instants and time.Duration for periods.

// Wrong
func poll(delay int) {
    time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10) // is this seconds or milliseconds?

// Correct
func poll(delay time.Duration) {
    time.Sleep(delay)
}
poll(10 * time.Second)

Handle Type Assertions

Always use the two value form to avoid panics.

// Wrong: panics on wrong type
t := i.(string)

// Correct
t, ok := i.(string)
if !ok {
    // handle error
}

Context as First Parameter

Context should be the first parameter, named ctx. Do not store context in structs.

func (s *Service) Process(ctx context.Context, req *Request) (*Response, error) {
    // ...
}

Avoid Mutable Globals

Use dependency injection instead of modifying global state.

// Wrong
var db *sql.DB

func init() {
    db, _ = sql.Open("postgres", os.Getenv("DSN"))
}

// Correct
type Server struct {
    db *sql.DB
}

func NewServer(db *sql.DB) *Server {
    return &Server{db: db}
}

Avoid init()

Prefer explicit initialization in main. init() makes code harder to reason about and test.

Embed Static Files (Go 1.16+)

Use //go:embed for static assets.

import "embed"

//go:embed templates/*
var templates embed.FS

//go:embed config.json
var configData []byte

Use Field Tags in Marshaled Structs

Explicit field names protect against accidental contract changes from refactoring.

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

Container and Runtime Considerations

Container-Aware GOMAXPROCS (Go 1.25+)

Go 1.25 automatically adjusts GOMAXPROCS based on container CPU limits.

// On Linux with cgroups, GOMAXPROCS now considers:
// - CPU bandwidth limits (CPU limit in Kubernetes)
// - Changes dynamically if limits change

// The runtime periodically updates GOMAXPROCS if:
// - Number of logical CPUs changes
// - cgroup CPU bandwidth limit changes

// Automatic behavior is disabled if you set GOMAXPROCS explicitly:
// - Via GOMAXPROCS environment variable
// - Via runtime.GOMAXPROCS() call

This means Go programs in containers should now perform better out-of-the-box without manual GOMAXPROCS tuning.


Common Gotchas

Loop Variable Capture (Fixed in Go 1.22+)

Prior to Go 1.22, loop variables were reused. This is no longer an issue.

// Pre-Go 1.22: All goroutines see last value
for _, v := range values {
    go func() {
        process(v) // Wrong: captures loop variable
    }()
}

// Fix for pre-Go 1.22
for _, v := range values {
    v := v // shadow the loop variable
    go func() {
        process(v)
    }()
}

// Go 1.22+: Loop variables are per-iteration (no fix needed)
for _, v := range values {
    go func() {
        process(v) // Safe: v is unique per iteration
    }()
}

Defer Argument Evaluation

Defer evaluates arguments immediately, not when deferred function runs.

// Wrong: always prints 0
for i := 0; i < 5; i++ {
    defer fmt.Println(i) // i evaluated when defer is called
}
// Prints: 4 3 2 1 0

// Gotcha with file handles
for _, f := range files {
    defer f.Close() // All defer the same f!
}

// Fix: capture in closure
for _, f := range files {
    f := f
    defer f.Close()
}

Nil Interface vs Nil Pointer

An interface containing a nil pointer is not nil.

type MyError struct{}
func (e *MyError) Error() string { return "error" }

func returnsError() error {
    var e *MyError = nil
    return e // Returns non-nil interface containing nil pointer!
}

if err := returnsError(); err != nil {
    fmt.Println("error is not nil!") // This prints
}

// Fix: return nil explicitly
func returnsError() error {
    var e *MyError = nil
    if e == nil {
        return nil
    }
    return e
}

Use Result Before Checking Error (Go 1.25 Fix)

Go 1.25 fixed a compiler bug where using a result before checking for error sometimes didn't panic. Your code should always check errors first.

// Wrong: uses f before checking err
f, err := os.Open("file.txt")
fmt.Println(f.Name()) // May panic if f is nil
if err != nil {
    return err
}

// Correct: always check error first
f, err := os.Open("file.txt")
if err != nil {
    return err
}
fmt.Println(f.Name()) // Safe: err was nil, so f is valid

In Go 1.21-1.24, a compiler bug sometimes suppressed the panic. Go 1.25 correctly panics, so ensure your code follows the proper pattern.

Map Iteration Order

Map iteration order is randomized. Do not depend on it.

// Wrong: results vary between runs
for k, v := range m {
    results = append(results, v)
}

// Correct: sort keys first if order matters
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
    results = append(results, m[k])
}

Slice Append Gotcha

Append may or may not allocate new backing array.

a := []int{1, 2, 3}
b := a[:2]
b = append(b, 4)
// a is now [1, 2, 4]! They share backing array

// Fix: use full slice expression to limit capacity
b := a[:2:2] // len=2, cap=2
b = append(b, 4) // forces new allocation
// a is still [1, 2, 3]

Experimental Features

encoding/json/v2 (Go 1.25, Experimental)

A new JSON engine is available with improved performance and streaming support.

// Enable with: GOEXPERIMENT=jsonv2

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
)

// The v2 API offers:
// - Better performance
// - Streaming-friendly jsontext package  
// - Custom marshalers/unmarshalers per call
// - Existing encoding/json can use v2 engine internally

This is experimental and subject to change.


Documentation

Comment Sentences

Comments documenting declarations should be full sentences starting with the name being described.

// Request represents a request to run a command.
type Request struct{}

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) error {}

Package Comments

Package comments appear before the package declaration with no blank line.

// Package math provides basic constants and mathematical functions.
package math

References

  1. Google Go Style Guide
  2. Uber Go Style Guide
  3. Effective Go
  4. Go Code Review Comments
  5. Go 1.23 Release Notes
  6. Go 1.24 Release Notes
  7. Go 1.25 Release Notes

# README.md


name: golang
description: Best practices for writing production Go code. Use when writing, reviewing, or refactoring Go code. Covers error handling, concurrency, naming conventions, testing patterns, performance optimization, generics, and common pitfalls. Based on Google Go Style Guide, Uber Go Style Guide, Effective Go, and Go Code Review Comments. Updated for Go 1.25.


Go Best Practices

Battle-tested patterns from Google, Uber, and the Go team. These are practices proven in large-scale production systems, updated for modern Go (1.25).

Core Principles

Readable code prioritizes these attributes in order:

  1. Clarity: purpose and rationale are obvious to the reader
  2. Simplicity: accomplishes the goal in the simplest way
  3. Concision: high signal to noise ratio
  4. Maintainability: easy to modify correctly
  5. Consistency: matches surrounding codebase

Error Handling

Return Errors, Do Not Panic

Production code must avoid panics. Return errors and let callers decide how to handle them.

// Wrong
func run(args []string) {
    if len(args) == 0 {
        panic("an argument is required")
    }
}

// Correct
func run(args []string) error {
    if len(args) == 0 {
        return errors.New("an argument is required")
    }
    return nil
}

func main() {
    if err := run(os.Args[1:]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Error Wrapping

Use %w when callers need to inspect the underlying error with errors.Is or errors.As. Use %v when you want to hide implementation details or at system boundaries.

// Preserve error chain for programmatic inspection
if err != nil {
    return fmt.Errorf("load config: %w", err)
}

// Hide internal details at API boundaries
if err != nil {
    return fmt.Errorf("database unavailable: %v", err)
}

Keep context succinct. Avoid phrases like "failed to" that pile up as errors propagate.

// Wrong: produces "failed to x: failed to y: failed to create store: the error"
return fmt.Errorf("failed to create new store: %w", err)

// Correct: produces "x: y: new store: the error"
return fmt.Errorf("new store: %w", err)

Joining Multiple Errors (Go 1.20+)

Use errors.Join when multiple operations can fail independently.

func validateUser(u User) error {
    var errs []error
    if u.Name == "" {
        errs = append(errs, errors.New("name required"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email required"))
    }
    return errors.Join(errs...)
}

// Checking joined errors
if err := validateUser(u); err != nil {
    if errors.Is(err, ErrNameRequired) {
        // handles even when joined with other errors
    }
}

Error Types

Choose based on caller needs:

Caller needs to match? Message type Approach
No Static errors.New("something bad")
No Dynamic fmt.Errorf("file %q not found", file)
Yes Static Exported var ErrNotFound = errors.New("not found")
Yes Dynamic Custom error type with Error() method

Sentinel Errors and errors.Is

Define sentinel errors for conditions callers need to check.

var (
    ErrNotFound    = errors.New("not found")
    ErrInvalidUser = errors.New("invalid user")
)

// Checking wrapped errors
if errors.Is(err, ErrNotFound) {
    // handles ErrNotFound even when wrapped
}

// Custom error types use errors.As
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("failed path:", pathErr.Path)
}

Error Naming

Exported error variables use Err prefix. Custom error types use Error suffix.

var (
    ErrNotFound    = errors.New("not found")
    ErrInvalidUser = errors.New("invalid user")
)

type NotFoundError struct {
    Resource string
}

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

Handle Errors Once

Do not log an error and also return it. The caller will likely log it again.

// Wrong: logs and returns, causing duplicate logs
if err != nil {
    log.Printf("could not get user %q: %v", id, err)
    return err
}

// Correct: wrap and return, let caller decide
if err != nil {
    return fmt.Errorf("get user %q: %w", id, err)
}

// Also correct: log and degrade gracefully without returning error
if err := emitMetrics(); err != nil {
    log.Printf("could not emit metrics: %v", err)
}

Error Strings

Do not capitalize error strings or end with punctuation. They often appear mid-sentence in logs.

// Wrong
fmt.Errorf("Something bad happened.")

// Correct
fmt.Errorf("something bad happened")

Indent Error Flow

Keep the happy path at minimal indentation. Handle errors first.

// Wrong
if err != nil {
    // error handling
} else {
    // normal code
}

// Correct
if err != nil {
    return err
}
// normal code continues

Concurrency

Channel Size

Channels should have size zero (unbuffered) or one. Any other size requires justification about what prevents filling under load.

// Wrong: arbitrary buffer
c := make(chan int, 64)

// Correct
c := make(chan int)    // unbuffered: synchronous handoff
c := make(chan int, 1) // buffered: allows one pending send

Goroutine Lifetimes

Document when and how goroutines exit. Goroutines blocked on channels will not be garbage collected even if the channel is unreachable.

// Document exit conditions
func (w *Worker) Run(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case job := <-w.jobs:
            w.process(job)
        }
    }
}

Use errgroup for Concurrent Operations

Prefer errgroup.Group over manual sync.WaitGroup for error-returning goroutines.

import "golang.org/x/sync/errgroup"

func processItems(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, item := range items {
        g.Go(func() error {
            return process(ctx, item)
        })
    }

    return g.Wait() // returns first error, cancels others via ctx
}

// With concurrency limit
func processItemsLimited(ctx context.Context, items []Item) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10) // max 10 concurrent goroutines

    for _, item := range items {
        g.Go(func() error {
            return process(ctx, item)
        })
    }

    return g.Wait()
}

Prefer Synchronous Functions

Synchronous functions are easier to reason about and test. Let callers add concurrency when needed.

// Wrong: forces concurrency on caller
func Fetch(url string) <-chan Result

// Correct: caller can wrap in goroutine if needed
func Fetch(url string) (Result, error)

Zero Value Mutexes

The zero value of sync.Mutex is valid. Do not use pointers to mutexes or embed them in exported structs.

// Wrong
mu := new(sync.Mutex)

// Wrong: exposes Lock/Unlock in API
type SMap struct {
    sync.Mutex
    data map[string]string
}

// Correct
type SMap struct {
    mu   sync.Mutex
    data map[string]string
}

Atomic Operations (Go 1.19+)

Use the standard library's typed atomics. External packages are no longer necessary.

import "sync/atomic"

type Counter struct {
    value atomic.Int64
}

func (c *Counter) Inc() {
    c.value.Add(1)
}

func (c *Counter) Value() int64 {
    return c.value.Load()
}

// Also available: atomic.Bool, atomic.Pointer[T], atomic.Uint32, etc.

sync.Map Performance (Go 1.24+)

The sync.Map implementation was significantly improved in Go 1.24. Modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads.


Naming

MixedCaps Always

Go uses MixedCaps, never underscores. This applies even when it breaks other language conventions.

// Wrong
MAX_LENGTH, max_length, HTTP_Server

// Correct
MaxLength, maxLength, HTTPServer

Initialisms

Initialisms maintain consistent case: URL not Url, ID not Id, HTTP not Http.

// Wrong
xmlHttpRequest, serverId, apiUrl

// Correct
xmlHTTPRequest, serverID, apiURL

Short Variable Names

Variables should be short, especially with limited scope. The further from declaration a name is used, the more descriptive it needs to be.

// Good for local scope
for i, v := range items { }
r := bufio.NewReader(f)

// Global or struct fields need more context
var DefaultTimeout = 30 * time.Second

Receiver Names

Use one or two letter abbreviations of the type. Be consistent across methods. Do not use generic names like this, self, or me.

// Wrong
func (this *Client) Get() {}
func (c *Client) Get() {}
func (cl *Client) Post() {} // inconsistent

// Correct
func (c *Client) Get() {}
func (c *Client) Post() {}

Pointer vs Value Receivers

Use pointer receiver when Use value receiver when
Method modifies the receiver Struct is small and immutable
Struct is large (avoid copying) Method doesn't modify state
Consistency with other methods Receiver is a map, func, or chan
Struct contains sync.Mutex Basic types (int, string, etc.)
// Pointer: modifies state
func (s *Server) Shutdown() error {
    s.running = false
    return s.listener.Close()
}

// Value: small, read-only
func (p Point) Distance(q Point) float64 {
    return math.Hypot(p.X-q.X, p.Y-q.Y)
}

Package Names

Package names are lowercase, single words. Avoid util, common, misc, api, types. The package name becomes part of the identifier at call sites.

// Wrong
package chubby
type ChubbyFile struct{}  // chubby.ChubbyFile is redundant

// Correct
package chubby
type File struct{}  // chubby.File reads well

Avoid Repetition in Names

Do not repeat package or receiver names in function names.

// Wrong
package http
func HTTPServe() {}  // http.HTTPServe is redundant

func (c *Config) WriteConfigTo(w io.Writer) {}  // Config repeated

// Correct
package http
func Serve() {}  // http.Serve

func (c *Config) WriteTo(w io.Writer) {}

Imports

Grouping

Organize imports in three groups separated by blank lines: standard library, external packages, internal packages.

import (
    "context"
    "fmt"
    "os"

    "github.com/google/uuid"
    "golang.org/x/sync/errgroup"

    "yourcompany/internal/config"
    "yourcompany/internal/metrics"
)

Avoid Renaming

Rename imports only to avoid collisions. Prefer renaming the most local import.

Avoid Import Dot

The dot import (import . "pkg") makes code harder to read. Use only in test files with circular dependencies.

Blank Imports

Import for side effects (import _ "pkg") only in main packages or tests.


Module Management

Tool Directives (Go 1.24+)

Go modules can now track executable dependencies using tool directives in go.mod. This removes the need for the previous workaround of adding tools as blank imports to a file conventionally named "tools.go".

// go.mod
module example.com/myproject

go 1.24

tool (
    golang.org/x/tools/cmd/stringer
    github.com/golangci/golangci-lint/cmd/golangci-lint
)
# Add a tool dependency
go get -tool golang.org/x/tools/cmd/stringer

# Run a tool
go tool stringer -type=Status

# Update all tools
go get tool

# Install all tools to GOBIN
go install tool

Structs

Use Field Names in Initialization

Always use field names. Positional arguments break when fields are added.

// Wrong: breaks if fields change
k := User{"John", "[email protected]", true}

// Correct
k := User{
    Name:   "John",
    Email:  "[email protected]",
    Active: true,
}

Omit Zero Value Fields

Do not initialize fields to their zero values.

// Wrong
user := User{
    Name:   "John",
    Active: false,  // false is zero value
    Count:  0,      // 0 is zero value
}

// Correct
user := User{
    Name: "John",
}

Embedding

Do not embed types in public structs. Embedding exposes methods and fields to the public API unintentionally.

// Wrong: Lock and Unlock become part of SMap's API
type SMap struct {
    sync.Mutex
    data map[string]string
}

// Correct
type SMap struct {
    mu   sync.Mutex
    data map[string]string
}

Use var for Zero Value Structs

// Correct
var user User

// Also acceptable
user := User{}

Slices and Maps

Nil Slice Declaration

Prefer nil slices over empty slices. They are functionally equivalent but nil is the preferred style.

// Preferred
var t []string

// Use only when JSON must encode as [] instead of null
t := []string{}

Copy at Boundaries

Slices and maps hold references. Copy them when storing or returning to prevent mutation.

// Wrong: caller can modify internal state
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = trips
}

// Correct
func (d *Driver) SetTrips(trips []Trip) {
    d.trips = make([]Trip, len(trips))
    copy(d.trips, trips)
}

// For maps
func (d *Driver) SetMetadata(m map[string]string) {
    d.metadata = maps.Clone(m)
}

Specify Capacity

Preallocate when size is known. This reduces allocations.

// Wrong
var result []Item
for _, v := range input {
    result = append(result, transform(v))
}

// Correct
result := make([]Item, 0, len(input))
for _, v := range input {
    result = append(result, transform(v))
}

Use slices and maps Packages

Prefer standard library functions for common operations.

import (
    "cmp"
    "maps"
    "slices"
)

// Sorting
slices.Sort(numbers)
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Name, b.Name)
})

// Searching
idx, found := slices.BinarySearch(sorted, target)

// Cloning
copy := slices.Clone(original)
mapCopy := maps.Clone(original)

// Comparison
if slices.Equal(a, b) { }
if maps.Equal(m1, m2) { }

Generics (Go 1.18+)

When to Use Generics

Use generics when you find yourself writing the same code for different types.

// Generic helper functions
func Filter[T any](slice []T, predicate func(T) bool) []T {
    result := make([]T, 0, len(slice))
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Map[T, U any](slice []T, transform func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = transform(v)
    }
    return result
}

// Usage
adults := Filter(users, func(u User) bool { return u.Age >= 18 })
names := Map(users, func(u User) string { return u.Name })

Type Constraints

Use constraints for type safety.

import "cmp"

// cmp.Ordered covers all comparable types
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

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

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

Generic Type Aliases (Go 1.24+)

Type aliases can now be parameterized like defined types.

// Generic type alias
type Set[T comparable] = map[T]struct{}

// Usage
var s Set[string]
s = make(Set[string])
s["hello"] = struct{}{}

// With constraints
type OrderedSlice[T cmp.Ordered] = []T

Avoid Over-Generalization

Do not use generics when a concrete type or interface suffices.

// Wrong: unnecessary generic
func PrintAll[T fmt.Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

// Correct: interface is sufficient
func PrintAll(items []fmt.Stringer) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

Iterators (Go 1.23+)

Range Over Functions

Go 1.23 introduced range-over-func, allowing custom iterators.

// Iterator function signature
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

// Custom iterator
func Backward[T any](s []T) func(yield func(int, T) bool) {
    return func(yield func(int, T) bool) {
        for i := len(s) - 1; i >= 0; i-- {
            if !yield(i, s[i]) {
                return
            }
        }
    }
}

// Usage
for i, v := range Backward(items) {
    fmt.Println(i, v)
}

String and Bytes Iterators (Go 1.24+)

New iterator functions for efficient string and byte processing.

import "strings"

// Iterate over lines (includes newline characters)
text := "line1\nline2\nline3"
for line := range strings.Lines(text) {
    fmt.Print(line)
}

// Split by delimiter (iterator, no slice allocation)
csvData := "apple,banana,cherry"
for value := range strings.SplitSeq(csvData, ",") {
    fmt.Println(value)
}

// Split after delimiter
for part := range strings.SplitAfterSeq(csvData, ",") {
    fmt.Println(part) // "apple," "banana," "cherry"
}

// Equivalent functions exist in bytes package

Structured Logging (Go 1.21+)

Use slog for New Code

The standard library now includes structured logging.

import "log/slog"

// Basic usage
slog.Info("user created", "id", userID, "email", email)
slog.Error("request failed", "err", err, "method", r.Method)

// With context
logger := slog.With("service", "auth", "version", "1.0")
logger.Info("starting")

// JSON output for production
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
})
slog.SetDefault(slog.New(handler))

slog.DiscardHandler (Go 1.24+)

Use the built-in discard handler for suppressing logs in tests.

// Before Go 1.24
log := slog.New(slog.NewJSONHandler(io.Discard, nil))

// Go 1.24+
log := slog.New(slog.DiscardHandler)

Structured Logging Best Practices

// Use consistent key names
slog.Info("request completed",
    "method", r.Method,
    "path", r.URL.Path,
    "status", statusCode,
    "duration_ms", duration.Milliseconds(),
)

// Group related fields
slog.Info("user action",
    slog.Group("user",
        "id", user.ID,
        "role", user.Role,
    ),
    slog.Group("request",
        "method", r.Method,
        "path", r.URL.Path,
    ),
)

Performance

Prefer strconv Over fmt

strconv is faster for primitive conversions.

// Slower
s := fmt.Sprintf("%d", n)

// Faster
s := strconv.Itoa(n)

Avoid Repeated String to Byte Conversions

// Wrong: converts on every iteration
for i := 0; i < n; i++ {
    w.Write([]byte("hello"))
}

// Correct
data := []byte("hello")
for i := 0; i < n; i++ {
    w.Write(data)
}

Specify Map Capacity

// Wrong
m := make(map[string]int)

// Correct when size is known
m := make(map[string]int, len(items))

Use strings.Builder for Concatenation

// Wrong: creates many allocations
var s string
for _, part := range parts {
    s += part
}

// Correct
var b strings.Builder
b.Grow(totalLen) // optional: preallocate
for _, part := range parts {
    b.WriteString(part)
}
s := b.String()

Testing

Table Driven Tests with Parallel Execution

Use table driven tests to avoid code duplication. Run subtests in parallel when safe.

func TestSplit(t *testing.T) {
    tests := []struct {
        name  string
        input string
        sep   string
        want  []string
    }{
        {
            name:  "simple",
            input: "a/b/c",
            sep:   "/",
            want:  []string{"a", "b", "c"},
        },
        {
            name:  "empty",
            input: "",
            sep:   "/",
            want:  []string{""},
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // run subtests concurrently

            got := strings.Split(tt.input, tt.sep)
            if diff := cmp.Diff(tt.want, got); diff != "" {
                t.Errorf("Split() mismatch (-want +got):\n%s", diff)
            }
        })
    }
}

T.Context and T.Chdir (Go 1.24+)

New helper methods for test context and working directory.

func TestWithContext(t *testing.T) {
    // T.Context returns a context canceled after test completes
    // but before cleanup functions run
    ctx := t.Context()

    result, err := doWork(ctx)
    if err != nil {
        t.Fatal(err)
    }
    // ...
}

func TestWithChdir(t *testing.T) {
    // T.Chdir changes working directory for duration of test
    // and automatically restores it after
    t.Chdir("testdata")

    // Now in testdata directory
    data, err := os.ReadFile("input.txt")
    // ...
}

Benchmark with b.Loop (Go 1.24+)

Use b.Loop() for cleaner, more accurate benchmarks.

// Old way - error prone
func BenchmarkOld(b *testing.B) {
    input := setupInput() // counted in benchmark time!
    b.ResetTimer()        // easy to forget
    for i := 0; i < b.N; i++ {
        process(input)    // compiler might optimize away
    }
}

// Go 1.24+ - preferred
func BenchmarkNew(b *testing.B) {
    input := setupInput() // setup runs once, excluded from timing
    for b.Loop() {
        process(input)    // compiler cannot optimize away
    }
}

Benefits of b.Loop():
- Setup code runs exactly once per -count, automatically excluded from timing
- No need to call b.ResetTimer()
- Function call parameters and results are kept alive, preventing compiler optimization

Testing Concurrent Code with synctest (Go 1.25+)

The testing/synctest package provides deterministic testing for concurrent code using synthetic time.

import "testing/synctest"

func TestTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
        defer cancel()

        // Inside the "bubble", time is synthetic
        // This sleep completes instantly in real time
        time.Sleep(4 * time.Second)

        // Context should not be expired yet
        if err := ctx.Err(); err != nil {
            t.Fatalf("unexpected timeout: %v", err)
        }

        // Advance past the timeout
        time.Sleep(2 * time.Second)

        // Now it should be expired
        if ctx.Err() != context.DeadlineExceeded {
            t.Fatal("expected deadline exceeded")
        }
    })
}

Key concepts:
- synctest.Test creates an isolated "bubble" with synthetic time
- Time only advances when all goroutines in the bubble are blocked
- Initial time is midnight UTC 2000-01-01
- synctest.Wait() waits for all goroutines to be durably blocked

func TestConcurrentCounter(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var counter atomic.Int64
        var wg sync.WaitGroup

        // Start concurrent workers
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                counter.Add(1)
            }()
        }

        // Wait for all goroutines to complete
        wg.Wait()

        // Counter is now deterministically 10
        if got := counter.Load(); got != 10 {
            t.Errorf("got %d, want 10", got)
        }
    })
}

// Example with time-based operations
func TestPeriodicTask(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var count atomic.Int64
        ctx, cancel := context.WithCancel(t.Context())

        // Start a periodic task
        go func() {
            ticker := time.NewTicker(100 * time.Millisecond)
            defer ticker.Stop()
            for {
                select {
                case <-ctx.Done():
                    return
                case <-ticker.C:
                    count.Add(1)
                }
            }
        }()

        // Advance synthetic time by 350ms
        time.Sleep(350 * time.Millisecond)
        synctest.Wait() // wait for goroutine to process

        cancel()
        synctest.Wait() // wait for goroutine to exit

        // Should have ticked 3 times (at 100ms, 200ms, 300ms)
        if got := count.Load(); got != 3 {
            t.Errorf("got %d ticks, want 3", got)
        }
    })
}

Important restrictions in synctest bubbles:
- Do not call t.Run(), t.Parallel(), or t.Deadline()
- Channels created outside the bubble behave differently
- External I/O operations are not durably blocking

Use go-cmp for Comparisons

Prefer github.com/google/go-cmp/cmp over reflect.DeepEqual.

import "github.com/google/go-cmp/cmp"

// Clear diff output on failure
if diff := cmp.Diff(want, got); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}

// With options for custom comparison
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(User{})); diff != "" {
    t.Errorf("mismatch (-want +got):\n%s", diff)
}

Useful Test Failures

Include: what was wrong, inputs, actual result, expected result.

// Wrong
if got != want {
    t.Error("wrong result")
}

// Correct
if got != want {
    t.Errorf("Foo(%q) = %d; want %d", input, got, want)
}

Use t.Fatal for Setup Failures

f, err := os.CreateTemp("", "test")
if err != nil {
    t.Fatal("failed to set up test")
}

Interfaces Belong to Consumers

Define interfaces in the package that uses them, not the package that implements them.

// Wrong: defining interface in producer
package producer
type Thinger interface { Thing() bool }
func NewThinger() Thinger { return &thinger{} }

// Correct: producer returns concrete type
package producer
type Thinger struct{}
func (t *Thinger) Thing() bool { return true }
func NewThinger() *Thinger { return &Thinger{} }

// Consumer defines interface it needs
package consumer
type Thinger interface { Thing() bool }
func Process(t Thinger) { }

Resource Management

runtime.AddCleanup (Go 1.24+)

Prefer runtime.AddCleanup over runtime.SetFinalizer for cleanup operations.

import "runtime"

type Resource struct {
    handle uintptr
}

func NewResource() *Resource {
    r := &Resource{handle: allocHandle()}

    // AddCleanup is more flexible than SetFinalizer:
    // - Multiple cleanups can be attached to one object
    // - Works with interior pointers
    // - Doesn't cause leaks with cycles
    // - Doesn't delay freeing the object
    runtime.AddCleanup(r, func(handle uintptr) {
        freeHandle(handle)
    }, r.handle)

    return r
}

Key advantages over SetFinalizer:
- Multiple cleanups per object
- Works with interior pointers
- No cycle-related leaks
- Object freed promptly (single GC cycle)

Weak Pointers (Go 1.24+)

The weak package provides weak references that don't prevent garbage collection.

import "weak"

// Create a weak pointer from a strong pointer
type ExpensiveResource struct {
    data []byte
}

func NewCache() *Cache {
    return &Cache{
        items: make(map[string]weak.Pointer[ExpensiveResource]),
    }
}

type Cache struct {
    mu    sync.Mutex
    items map[string]weak.Pointer[ExpensiveResource]
}

func (c *Cache) Get(key string) *ExpensiveResource {
    c.mu.Lock()
    defer c.mu.Unlock()

    if wp, ok := c.items[key]; ok {
        // Value returns the original pointer, or nil if collected
        if r := wp.Value(); r != nil {
            return r
        }
        // Resource was garbage collected, remove stale entry
        delete(c.items, key)
    }
    return nil
}

func (c *Cache) Set(key string, r *ExpensiveResource) {
    c.mu.Lock()
    defer c.mu.Unlock()
    // Make creates a weak pointer from a strong pointer
    c.items[key] = weak.Make(r)
}

Use cases for weak pointers:
- Caches that shouldn't prevent garbage collection
- Canonicalization maps (interning)
- Observer patterns where observers may be collected

Secure Directory Access with os.Root (Go 1.24+)

The os.Root type provides safe, scoped file system access that prevents path traversal attacks.

import "os"

func ServeUserFiles(userDir string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // OpenRoot opens a directory as a root for safe access
        root, err := os.OpenRoot(userDir)
        if err != nil {
            http.Error(w, "directory not found", http.StatusNotFound)
            return
        }
        defer root.Close()

        // Open is safe: paths are resolved relative to root
        // Attempts to escape (like "../etc/passwd") are rejected
        filename := r.URL.Query().Get("file")
        f, err := root.Open(filename)
        if err != nil {
            http.Error(w, "file not found", http.StatusNotFound)
            return
        }
        defer f.Close()

        io.Copy(w, f)
    }
}

// Available methods on os.Root:
// - Open(name) - open file for reading
// - Create(name) - create or truncate file
// - OpenFile(name, flag, perm) - open with flags
// - Mkdir(name, perm) - create directory
// - Remove(name) - remove file or empty directory
// - Stat(name), Lstat(name) - file info
// - ReadDir(name) - list directory contents

Key benefits:
- Prevents path traversal vulnerabilities ("../" attacks)
- Symlinks cannot escape the root directory
- Race-condition safe (uses openat2 on Linux)
- Drop-in replacement for typical file operations


Patterns

Functional Options

Use functional options for configurable constructors with many optional parameters.

type Server struct {
    addr    string
    timeout time.Duration
    logger  *slog.Logger
}

type Option func(*Server)

func WithTimeout(d time.Duration) Option {
    return func(s *Server) { s.timeout = d }
}

func WithLogger(l *slog.Logger) Option {
    return func(s *Server) { s.logger = l }
}

func NewServer(addr string, opts ...Option) *Server {
    s := &Server{
        addr:    addr,
        timeout: 30 * time.Second,
        logger:  slog.Default(),
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Usage
srv := NewServer("localhost:8080",
    WithTimeout(60*time.Second),
    WithLogger(logger),
)

Verify Interface Compliance

Use compile time checks to verify interface implementations.

type Handler struct{}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}

Defer for Cleanup

Use defer to clean up resources. The small overhead is worth the readability and safety.

p.Lock()
defer p.Unlock()

if p.count < 10 {
    return p.count
}
p.count++
return p.count

Graceful Shutdown Pattern

Production servers need graceful shutdown to drain connections.

func main() {
    srv := &http.Server{Addr: ":8080", Handler: handler}

    // Start server in goroutine
    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("server error", "err", err)
        }
    }()

    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        slog.Error("shutdown error", "err", err)
    }
    slog.Info("server stopped")
}

Start Enums at One

Zero values should represent invalid or unset state.

type Operation int

const (
    OperationUnknown Operation = iota // 0 = invalid
    OperationAdd                      // 1
    OperationSubtract                 // 2
)

Use time Package for Time

Do not use integers for time. Use time.Time for instants and time.Duration for periods.

// Wrong
func poll(delay int) {
    time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10) // is this seconds or milliseconds?

// Correct
func poll(delay time.Duration) {
    time.Sleep(delay)
}
poll(10 * time.Second)

Handle Type Assertions

Always use the two value form to avoid panics.

// Wrong: panics on wrong type
t := i.(string)

// Correct
t, ok := i.(string)
if !ok {
    // handle error
}

Context as First Parameter

Context should be the first parameter, named ctx. Do not store context in structs.

func (s *Service) Process(ctx context.Context, req *Request) (*Response, error) {
    // ...
}

Avoid Mutable Globals

Use dependency injection instead of modifying global state.

// Wrong
var db *sql.DB

func init() {
    db, _ = sql.Open("postgres", os.Getenv("DSN"))
}

// Correct
type Server struct {
    db *sql.DB
}

func NewServer(db *sql.DB) *Server {
    return &Server{db: db}
}

Avoid init()

Prefer explicit initialization in main. init() makes code harder to reason about and test.

Embed Static Files (Go 1.16+)

Use //go:embed for static assets.

import "embed"

//go:embed templates/*
var templates embed.FS

//go:embed config.json
var configData []byte

Use Field Tags in Marshaled Structs

Explicit field names protect against accidental contract changes from refactoring.

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

Container and Runtime Considerations

Container-Aware GOMAXPROCS (Go 1.25+)

Go 1.25 automatically adjusts GOMAXPROCS based on container CPU limits.

// On Linux with cgroups, GOMAXPROCS now considers:
// - CPU bandwidth limits (CPU limit in Kubernetes)
// - Changes dynamically if limits change

// The runtime periodically updates GOMAXPROCS if:
// - Number of logical CPUs changes
// - cgroup CPU bandwidth limit changes

// Automatic behavior is disabled if you set GOMAXPROCS explicitly:
// - Via GOMAXPROCS environment variable
// - Via runtime.GOMAXPROCS() call

This means Go programs in containers should now perform better out-of-the-box without manual GOMAXPROCS tuning.


Common Gotchas

Loop Variable Capture (Fixed in Go 1.22+)

Prior to Go 1.22, loop variables were reused. This is no longer an issue.

// Pre-Go 1.22: All goroutines see last value
for _, v := range values {
    go func() {
        process(v) // Wrong: captures loop variable
    }()
}

// Fix for pre-Go 1.22
for _, v := range values {
    v := v // shadow the loop variable
    go func() {
        process(v)
    }()
}

// Go 1.22+: Loop variables are per-iteration (no fix needed)
for _, v := range values {
    go func() {
        process(v) // Safe: v is unique per iteration
    }()
}

Defer Argument Evaluation

Defer evaluates arguments immediately, not when deferred function runs.

// Wrong: always prints 0
for i := 0; i < 5; i++ {
    defer fmt.Println(i) // i evaluated when defer is called
}
// Prints: 4 3 2 1 0

// Gotcha with file handles
for _, f := range files {
    defer f.Close() // All defer the same f!
}

// Fix: capture in closure
for _, f := range files {
    f := f
    defer f.Close()
}

Nil Interface vs Nil Pointer

An interface containing a nil pointer is not nil.

type MyError struct{}
func (e *MyError) Error() string { return "error" }

func returnsError() error {
    var e *MyError = nil
    return e // Returns non-nil interface containing nil pointer!
}

if err := returnsError(); err != nil {
    fmt.Println("error is not nil!") // This prints
}

// Fix: return nil explicitly
func returnsError() error {
    var e *MyError = nil
    if e == nil {
        return nil
    }
    return e
}

Use Result Before Checking Error (Go 1.25 Fix)

Go 1.25 fixed a compiler bug where using a result before checking for error sometimes didn't panic. Your code should always check errors first.

// Wrong: uses f before checking err
f, err := os.Open("file.txt")
fmt.Println(f.Name()) // May panic if f is nil
if err != nil {
    return err
}

// Correct: always check error first
f, err := os.Open("file.txt")
if err != nil {
    return err
}
fmt.Println(f.Name()) // Safe: err was nil, so f is valid

In Go 1.21-1.24, a compiler bug sometimes suppressed the panic. Go 1.25 correctly panics, so ensure your code follows the proper pattern.

Map Iteration Order

Map iteration order is randomized. Do not depend on it.

// Wrong: results vary between runs
for k, v := range m {
    results = append(results, v)
}

// Correct: sort keys first if order matters
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
    results = append(results, m[k])
}

Slice Append Gotcha

Append may or may not allocate new backing array.

a := []int{1, 2, 3}
b := a[:2]
b = append(b, 4)
// a is now [1, 2, 4]! They share backing array

// Fix: use full slice expression to limit capacity
b := a[:2:2] // len=2, cap=2
b = append(b, 4) // forces new allocation
// a is still [1, 2, 3]

Experimental Features

encoding/json/v2 (Go 1.25, Experimental)

A new JSON engine is available with improved performance and streaming support.

// Enable with: GOEXPERIMENT=jsonv2

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
)

// The v2 API offers:
// - Better performance
// - Streaming-friendly jsontext package  
// - Custom marshalers/unmarshalers per call
// - Existing encoding/json can use v2 engine internally

This is experimental and subject to change.


Documentation

Comment Sentences

Comments documenting declarations should be full sentences starting with the name being described.

// Request represents a request to run a command.
type Request struct{}

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) error {}

Package Comments

Package comments appear before the package declaration with no blank line.

// Package math provides basic constants and mathematical functions.
package math

References

  1. Google Go Style Guide
  2. Uber Go Style Guide
  3. Effective Go
  4. Go Code Review Comments
  5. Go 1.23 Release Notes
  6. Go 1.24 Release Notes
  7. Go 1.25 Release Notes

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.