Use when adding new error messages to React, or seeing "unknown error code" warnings.
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:
- Clarity: purpose and rationale are obvious to the reader
- Simplicity: accomplishes the goal in the simplest way
- Concision: high signal to noise ratio
- Maintainability: easy to modify correctly
- 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
# 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:
- Clarity: purpose and rationale are obvious to the reader
- Simplicity: accomplishes the goal in the simplest way
- Concision: high signal to noise ratio
- Maintainability: easy to modify correctly
- 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
# 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.