> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Stop Writing Go Like It’s 2017: 15 Modern Patterns You Should Be Using
- URL: https://huizhou92.com/stop-writing-go-like-its-2017-15-modern-patterns-you-should-be-using/
- Published: 2026-03-06T12:31:13.000Z
- Updated: 2026-03-06T12:31:13.000Z
- Description: Stop Writing Go Like It’s 2017: 15 Modern Patterns You Should Be Using. · Background · The Foundation: Go 1.8 to 1.13 — Time and Error Handling ∘ time.Sinc。
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

· [Background](#5e76)  
· [The Foundation: Go 1.8 to 1.13 — Time and Error Handling](#c540)  
 ∘ [time.Since And time.Until (Go 1.0+ and 1.8+)](#4fcd)  
 ∘ [errors.Is For Wrapped Errors (Go 1.13+)](#6281)  
· [The Generics Revolution: Go 1.18](#2f36)  
 ∘ [any Instead of interface{}](#66d7)  
 ∘ [strings.Cut and bytes.Cut — String Parsing Made Simple](#b3ee)  
· [Atomic Operations Get Type-Safe: Go 1.19](#07e9)  
· [String and Byte Utilities: Go 1.20](#e71f)  
 ∘ [strings.Clone And bytes.Clone](#2c18)  
 ∘ [strings.CutPrefix And strings.CutSuffix](#83d7)  
 ∘ [errors.Join — Combining Multiple Errors](#c3ac)  
· [The Standard Library Renaissance: Go 1.21](#1933)  
 ∘ [Built-ins: min, max, and clear](#ab00)  
 ∘ [The slices Package](#d49b)  
 ∘ [The maps Package](#a1db)  
 ∘ [sync.OnceFunc And sync.OnceValue](#e714)  
· [Loop Revolution: Go 1.22](#038d)  
 ∘ [for i := range n — Integer Range](#7128)  
 ∘ [Fixed Loop Variable Capture](#96c9)  
 ∘ [cmp.Or — First Non-Zero Value](#84fb)  
 ∘ [Enhanced http.ServeMux](#d7a3)  
· [Iterator Pattern: Go 1.23](#84d6)  
 ∘ [time.Tick Is Now Safe](#9232)  
· [Testing and JSON Improvements: Go 1.24](#f54e)  
 ∘ [t.Context() In Tests](#f6be)  
 ∘ [omitzero For JSON](#ae63)  
 ∘ [b.Loop() For Benchmarks](#f2f7)  
 ∘ [strings.SplitSeq For Iteration](#3ac2)  
· [Summary](#00d5)  
· [Tools to Help You Modernize](#eb5e)  
· [A Word of Caution](#4fee)

### Background

A few weeks ago, I stumbled upon something interesting: [JetBrains’ Go Modern Guidelines](https://github.com/JetBrains/go-modern-guidelines?ref=huizhou92.com). It’s a collection of patterns and features that transform legacy Go code into modern, idiomatic Go. That got me thinking — how many of us are still writing Go the way we learned it years ago?

I was reviewing a colleague’s pull request and noticed this pattern:

```go
// The old way - still works, but... 
for i := 0; i < len(items); i++ { 
    process(items[i]) 
}
```

This is valid Go. It compiles. It runs. But it’s **not modern Go**. Since Go 1.8, the language has evolved dramatically, adding features that make code more readable, more maintainable, and often more performant. Yet many developers — especially those who learned Go years ago — continue writing code the way they always have.

This isn’t about being pedantic. It’s about **leverage**. Each new feature in Go solves real problems, reduces boilerplate, and makes intent clearer. In this article, we’ll explore the most impactful changes from Go 1.8 through Go 1.24, with before-and-after examples you can apply today.

### The Foundation: Go 1.8 to 1.13 — Time and Error Handling

Let’s start with two simple but frequently used improvements that have been available for years.

#### `time.Since` And `time.Until` (Go 1.0+ and 1.8+)

Measuring elapsed time is incredibly common. The modern approach is cleaner:

```go
// Before: Verbose and error-prone 
start := time.Now() 
doWork() 
elapsed := time.Now().Sub(start)  // Easy to get wrong! 
// After: Clean and idiomatic 
start := time.Now() 
doWork() 
elapsed := time.Since(start)
```

Similarly, `time.Until` (Go 1.8+) replaces `deadline.Sub(time.Now())`:

```go
// Before 
timeToDeadline := deadline.Sub(time.Now()) 
// After 
timeToDeadline := time.Until(deadline)
```

Isn’t it fascinating how such small changes add up to cleaner code?

#### `errors.Is` For Wrapped Errors (Go 1.13+)

Error wrapping with `fmt.Errorf("%w", err)` was a game-changer in Go 1.13\. But it introduced a problem: you can no longer use `==` to check error types. The solution is `errors.Is`:

```go
// Before: direct equality becomes brittle once errors may be wrapped 
if err == fs.ErrNotExist { 
    // This may fail if the error was wrapped 
} 
 
// After: works across the error chain 
if errors.Is(err, fs.ErrNotExist) { 
    // Correctly handles wrapped sentinel errors 
}
```

> **Updated:** The original example used `io.EOF`, which is a special-case sentinel in Go and should not be wrapped. The example now uses `fs.ErrNotExist` instead.

> Error wrapping with `fmt.Errorf("%w", err)` was a game-changer in Go 1.13\. Once errors may be wrapped, direct `==` checks become brittle for sentinel errors in general. The recommended approach is to use `errors.Is`, which walks the error chain.

> `io.EOF` is a notable exception: by convention it should be returned directly rather than wrapped, since callers often compare it using `==`.

> Related discussion: [https://github.com/golang/go/issues/39155](https://github.com/golang/go/issues/39155?utm%5Fsource=chatgpt.com)  
> thanks [Paul Hewlett](https://medium.com/u/412f1939facd?ref=huizhou92.com)

The key insight: `errors.Is` traverses the entire error chain, checking each wrapped error. This is crucial when working with libraries that wrap errors for context.

### The Generics Revolution: Go 1.18

Go 1.18 was a watershed moment. Generics arrived, and with them, a cleaner way to write type-agnostic code.

#### any Instead of `interface{}`

This is the simplest change with the biggest visual impact:

```go
// Before: Verbose and ugly 
func process(data interface{}) interface{} 
// After: Clean and expressive 
func process(data any) any
```

They’re identical under the hood — `any` is just a type alias for `interface{}`. But the intent is clearer, and your code reads better.

#### `strings.Cut` and `bytes.Cut` — String Parsing Made Simple

How many times have you written this pattern?

```go
// Before: Index + slice gymnastics 
idx := strings.Contains(s, "=") 
if idx != -1 { 
    key := s[:idx] 
    value := s[idx+1:] 
}
```

`strings.Cut` (Go 1.18) replaces this with a single, expressive call:

```go
// After: Clean and safe 
key, value, found := strings.Cut(s, "=") 
if found { 
    // Use key and value directly 
}
```

This is particularly elegant for parsing key-value pairs, URLs, or any delimited data. The same pattern applies to `bytes.Cut` for byte slices.

### Atomic Operations Get Type-Safe: Go 1.19

The `sync/atomic` package received a major upgrade in Go 1.19, introducing type-safe atomic types. No more `unsafe.Pointer` gymnastics or `int32` conversions!

```go
// Before: Verbose and error-prone 
var flag int32 
atomic.StoreInt32(&flag, 1) 
if atomic.LoadInt32(&flag) == 1 { 
    // ... 
} 
// After: Clean and type-safe 
var flag atomic.Bool 
flag.Store(true) 
if flag.Load() { 
    // ... 
}
```

The new atomic types include:

- `atomic.Bool`
- `atomic.Int32`, `atomic.Int64`, `atomic.Uint32`, `atomic.Uint64`
- `atomic.Pointer[T]` — Type-safe pointers!

The `atomic.Pointer[T]` is especially powerful for lock-free data structures:

```go
var config atomic.Pointer[Config] 
// Store a new config 
newCfg := &Config{Timeout: 30 * time.Second} 
config.Store(newCfg) 
// Load atomically 
current := config.Load() 
fmt.Println(current.Timeout)
```

### String and Byte Utilities: Go 1.20

Go 1.20 added several utilities that eliminate common boilerplate.

#### `strings.Clone` And `bytes.Clone`

Sometimes you need to ensure a string or byte slice doesn’t share underlying memory. Previously, this required manual copying:

```go
// Before: Manual copy 
cloned := make([]byte, len(original)) 
copy(cloned, original) 
// After: Expressive and clear 
cloned := bytes.Clone(original)
```

#### `strings.CutPrefix` And `strings.CutSuffix`

These are perfect for parsing tasks:

```go
// Before: Manual prefix check and slice 
if strings.HasPrefix(s, "prefix:") { 
    rest := s[len("prefix:"):] 
    // ... 
} 
// After: Single operation 
if rest, ok := strings.CutPrefix(s, "prefix:"); ok { 
    // rest contains everything after "prefix:" 
}
```

#### `errors.Join` — Combining Multiple Errors

When you have multiple errors to return, `errors.Join` (Go 1.20) provides a clean solution:

```go
// Before: Return first error only, or custom error type 
var errs []error 
if err1 != nil { 
    errs = append(errs, err1) 
} 
if err2 != nil { 
    errs = append(errs, err2) 
} 
return errors.New("multiple errors occurred") // Lost details! 
// After: Preserve all errors 
return errors.Join(err1, err2, err3)
```

The joined error implements `Unwrap() []error`, so `errors.Is` still works correctly.

### The Standard Library Renaissance: Go 1.21

Go 1.21 was a massive release, introducing built-in functions and new packages that eliminate huge amounts of boilerplate.

#### Built-ins: `min`, `max`, and `clear`

These functions work with any ordered type:

```go
// Before: Manual comparison 
if a > b { 
    result = a 
} else { 
    result = b 
} 
// After: Clean built-in 
result := max(a, b) 
// Works with multiple arguments too! 
result := max(a, b, c, d, e)
```

The `clear` built-in deletes all map entries or zeros slice elements:

```go
// Clear all map entries 
m := map[string]int{"a": 1, "b": 2} 
clear(m)  // m is now empty 
// Zero slice elements 
s := []int{1, 2, 3, 4, 5} 
clear(s)  // s is now []int{0, 0, 0, 0, 0}
```

#### The `slices` Package

This package is a game-changer for slice operations. Let’s look at the most useful functions:

```go
// Contains - no more manual loops! 
if slices.Contains(items, target) { 
    // Found it 
} 
// Index - find position (-1 if not found) 
pos := slices.Index(items, target) 
// Sort - for ordered types 
slices.Sort(numbers) 
// SortFunc - for custom types 
slices.SortFunc(users, func(a, b User) int { 
    return cmp.Compare(a.Age, b.Age) 
}) 
// Max/Min - find extremes 
oldest := slices.Max(ages) 
// Reverse - in-place reversal 
slices.Reverse(items) 
// Clone - create a copy 
backup := slices.Clone(items) 
// Compact - remove consecutive duplicates 
items = slices.Compact(items)  // [1,1,2,2,3] → [1,2,3]
```

#### The `maps` Package

Similarly, the `maps` package simplifies map operations:

```go
// Clone a map 
copy := maps.Clone(original) 
// Copy entries from one map to another 
maps.Copy(destination, source) 
// Delete entries matching a condition 
maps.DeleteFunc(m, func(k string, v int) bool { 
    return v < 0  // Remove negative values 
})
```

#### `sync.OnceFunc` And `sync.OnceValue`

These eliminate the boilerplate of `sync.Once`:

```go
// Before: Verbose initialization 
var ( 
    once sync.Once 
    config *Config 
) 
func GetConfig() *Config { 
    once.Do(func() { 
        config = loadConfig() 
    }) 
    return config 
} 
// After: Clean and direct 
var getConfig = sync.OnceValue(func() *Config { 
    return loadConfig() 
}) 
// Usage 
cfg := getConfig()
```

`sync.OnceFunc` is similar but for side effects:

```go
var initDB = sync.OnceFunc(func() { 
    db = sql.Open("postgres", dsn) 
}) 
// Call multiple times, but only executes once 
initDB()
```

### Loop Revolution: Go 1.22

Go 1.22 introduced two of the most-requested features: cleaner integer loops and fixed loop-variable semantics.

#### `for i := range n` — Integer Range

This is the change that will clean up your code the most:

```go
// Before: Verbose C-style loop 
for i := 0; i < len(items); i++ { 
    process(items[i]) 
} 
// After: Clean and expressive 
for i := range len(items) { 
    process(items[i]) 
} 
// Even simpler for just counting 
for i := range 10 { 
    fmt.Println(i)  // 0, 1, 2, ..., 9 
}
```

That’s pretty cool! No more `i++` bugs or off-by-one errors.

#### Fixed Loop Variable Capture

This was a notorious Go gotcha that’s finally fixed:

```go
// Before: Bug! All goroutines see the same value 
for _, item := range items { 
    go func() { 
        process(item)  // Wrong! All see the last item 
    }() 
} 
// Old workaround: Pass as argument 
for _, item := range items { 
    go func(item Item) { 
        process(item) 
    }(item) 
} 
// After (Go 1.22+): Just works! 
for _, item := range items { 
    go func() { 
        process(item)  // Each goroutine gets its own copy 
    }() 
}
```

In Go 1.22+, each iteration creates new variables, eliminating this entire class of bugs.

#### `cmp.Or` — First Non-Zero Value

This is perfect for configuration with defaults:

```go
// Before: Verbose fallback chain 
name := os.Getenv("NAME") 
if name == "" { 
    name = config.Name 
    if name == "" { 
        name = "default" 
    } 
} 
// After: Clean and linear 
name := cmp.Or( 
    os.Getenv("NAME"), 
    config.Name, 
    "default", 
)
```

`cmp.Or` returns the first non-zero value for any comparable type.

#### Enhanced `http.ServeMux`

The standard library’s HTTP router got a major upgrade:

```go
// Before: Manual method checking and path parsing 
mux.HandleFunc("/api/items", func(w http.ResponseWriter, r *http.Request) { 
    if r.Method != "GET" { 
        http.Error(w, "Method not allowed", 405) 
        return 
    } 
    // Extract ID from path manually... 
}) 
// After: Method and path parameters built-in 
mux.HandleFunc("GET /api/items/{id}", func(w http.ResponseWriter, r *http.Request) { 
    id := r.PathValue("id")  // Clean extraction 
    // ... 
})
```

This alone can eliminate the need for many third-party routers in simple applications.

### Iterator Pattern: Go 1.23

Go 1.23 introduced iterators, enabling a new pattern for collections. The `maps` and `slices` packages were updated to support them.

```go
// Iterate over map keys directly 
for k := range maps.Keys(m) { 
    fmt.Println(k) 
} 
// Collect keys to a slice 
keys := slices.Collect(maps.Keys(m)) 
// Collect and sort in one operation 
sortedKeys := slices.Sorted(maps.Keys(m))
```

The iterator pattern is particularly powerful for custom collection types, allowing them to work seamlessly with `for...range` loops.

#### `time.Tick` Is Now Safe

Previously, `time.Tick` leaked goroutines if the ticker wasn't stopped. As of Go 1.23, the garbage collector can reclaim unreferenced tickers:

```go
// Before: Had to use NewTicker to avoid leak 
ticker := time.NewTicker(time.Second) 
defer ticker.Stop() 
for t := range ticker.C { 
    // ... 
} 
// After (Go 1.23+): Tick is fine for simple cases 
for t := range time.Tick(time.Second) { 
    // Safe! GC handles cleanup 
}
```

### Testing and JSON Improvements: Go 1.24

Go 1.24 continues the trend of reducing boilerplate.

#### `t.Context()` In Tests

No more manual context creation in tests:

```go
// Before: Verbose setup 
func TestFeature(t *testing.T) { 
    ctx, cancel := context.WithCancel(context.Background()) 
    defer cancel() 
    result := doSomething(ctx) 
    // ... 
} 
// After: Clean and automatic 
func TestFeature(t *testing.T) { 
    result := doSomething(t.Context()) 
    // Automatically cancelled when test ends 
}
```

The test’s context is automatically cancelled when the test completes, including during timeouts or parallel execution.

#### `omitzero` For JSON

The `omitempty` tag has a long-standing issue: it doesn't work for `time.Duration` or structs. `omitzero` (Go 1.24) fixes this:

```go
// Before: Duration zero values still appear in JSON 
type Config struct { 
    Timeout time.Duration `json:"timeout,omitempty"`  // Doesn't work! 
} 
// After: Proper zero-value handling 
type Config struct { 
    Timeout time.Duration `json:"timeout,omitzero"`  // Works correctly 
}
```

Use `omitzero` for:

- `time.Duration` and `time.Time`
- Structs
- Slices and maps

#### `b.Loop()` For Benchmarks

Benchmarks get cleaner too:

```go
// Before: Manual loop counter 
func BenchmarkWork(b *testing.B) { 
    for i := 0; i < b.N; i++ { 
        doWork() 
    } 
} 
// After: Expressive loop 
func BenchmarkWork(b *testing.B) { 
    for b.Loop() { 
        doWork() 
    } 
}
```

`b.Loop()` handles the iteration count automatically and can provide more accurate measurements by managing timer state internally.

#### `strings.SplitSeq` For Iteration

When iterating over split results, use the new sequence functions:

```go
// Before: Creates intermediate slice 
for _, part := range strings.Split(s, ",") { 
    process(part) 
} 
// After: Iterates without allocation 
for part := range strings.SplitSeq(s, ",") { 
    process(part) 
}
```

This avoids creating the intermediate slice, which can be significant for large strings or frequent operations. The same pattern exists for `strings.FieldsSeq`, `bytes.SplitSeq`, and `bytes.FieldsSeq`.

### Summary

Go has evolved significantly since 1.8\. Here’s a quick reference of the changes we covered:

1. **Go 1.8+**: `time.Until` for deadline calculations
2. **Go 1.13+**: `errors.Is` for wrapped error checking
3. **Go 1.18+**: `any` alias, `strings.Cut`, `bytes.Cut`, generics
4. **Go 1.19+**: Type-safe atomics (`atomic.Bool`, `atomic.Pointer[T]`)
5. **Go 1.20+**: `strings.Clone`, `strings.CutPrefix`, `errors.Join`
6. **Go 1.21+**: `min`/`max`/`clear` built-ins, `slices` and `maps` packages, `sync.OnceFunc`
7. **Go 1.22+**: `for i := range n` loops, fixed loop variables, `cmp.Or`, enhanced `http.ServeMux`
8. **Go 1.23+**: Iterator pattern with `maps.Keys`, `slices.Collect`, safe `time.Tick`
9. **Go 1.24+**: `t.Context()`, `omitzero` JSON tag, `b.Loop()`, `strings.SplitSeq`

Each of these features solves real problems and reduces boilerplate. The cumulative effect is code that’s more readable, more maintainable, and less prone to bugs.

### Tools to Help You Modernize

If you’re wondering how to apply these patterns consistently across your codebase, check out [JetBrains’ Go Modern Guidelines](https://github.com/JetBrains/go-modern-guidelines?ref=huizhou92.com). It’s a collection of inspections and quick fixes that can automatically transform legacy Go code into modern, idiomatic Go.

The project is particularly useful for:

- **Code reviews**: Spot outdated patterns during PR reviews
- **Legacy migrations**: Systematically update older codebases
- **Team consistency**: Ensure everyone on your team writes modern Go

While tools like this are helpful, understanding *why* these patterns matter is still essential. Modern Go isn’t just about syntax — it’s about writing code that’s clearer, safer, and more maintainable.

### A Word of Caution

While modern features are powerful, consider your team’s Go version. Check your `go.mod` file:

```bash
grep "^go " go.mod
```

Only use features available in your target version. If you’re supporting older Go versions, tools like `golangci-lint` with version-aware rules can help catch incompatible code.

That said, if you’re on Go 1.21 or later (and you should be — 1.20 is already EOL), most of these features are available today. Start using them. Your future self will thank you.