MidТеория10 min

Go 1.22 – 1.23

Улучшенный HTTP-роутер, исправление захвата переменных цикла, range over int, итераторы и пакет unique

Go 1.22 (Февраль 2024)

Go 1.22 принёс два долгожданных изменения: значительно улучшенный стандартный HTTP-роутер и исправление одного из самых распространённых багов Go -- захвата переменных цикла.

Enhanced ServeMux -- полноценный HTTP-роутер в стандартной библиотеке

До Go 1.22 стандартный http.ServeMux был примитивным: он поддерживал только точные пути и префиксы, без методов и параметров. Это заставляло использовать сторонние роутеры (gorilla/mux, chi, gin).

Go 1.22 полностью переработал ServeMux, добавив:

Паттерны с HTTP-методами

mux := http.NewServeMux()

// Method-specific patterns
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

// Without method: matches any method
mux.HandleFunc("/health", healthCheck)

Path parameters

func getUser(w http.ResponseWriter, r *http.Request) {
    // Extract path parameter
    id := r.PathValue("id")

    user, err := db.FindUser(r.Context(), id)
    if err != nil {
        http.Error(w, "user not found", http.StatusNotFound)
        return
    }

    json.NewEncoder(w).Encode(user)
}

Wildcard (catch-all)

// {path...} matches everything including slashes
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
    filePath := r.PathValue("path")
    // filePath = "docs/2024/report.pdf" for /files/docs/2024/report.pdf
    http.ServeFile(w, r, filepath.Join(staticDir, filePath))
})

Правила приоритетов

mux.HandleFunc("GET /posts/{id}", getPost)      // Specific parameter
mux.HandleFunc("GET /posts/latest", getLatest)   // Exact match wins!
mux.HandleFunc("GET /posts/{id}/comments", getComments)
mux.HandleFunc("GET /{path...}", catchAll)       // Lowest priority

// Request: GET /posts/latest  -> getLatest (exact > parameter)
// Request: GET /posts/42      -> getPost
// Request: GET /posts/42/comments -> getComments
// Request: GET /anything/else -> catchAll

Правила приоритетов:

  1. Exact match побеждает wildcard
  2. Более длинный путь побеждает короткий
  3. Более специфичный паттерн побеждает менее специфичный
  4. Если два паттерна конфликтуют -- паника при регистрации

Host-based routing

mux.HandleFunc("GET api.example.com/v1/users", apiListUsers)
mux.HandleFunc("GET www.example.com/", webIndex)

Полный пример: REST API на стандартном роутере

package main

import (
    "encoding/json"
    "log/slog"
    "net/http"
    "sync"
    "time"
)

type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

type UserStore struct {
    mu    sync.RWMutex
    users map[string]*User
}

func NewUserStore() *UserStore {
    return &UserStore{users: make(map[string]*User)}
}

// Middleware: logging
func withLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("request",
            "method", r.Method,
            "path", r.URL.Path,
            "duration", time.Since(start),
        )
    })
}

// Middleware: JSON content type
func withJSON(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        next(w, r)
    }
}

func main() {
    store := NewUserStore()
    mux := http.NewServeMux()

    // RESTful routes with the new enhanced patterns
    mux.HandleFunc("GET /api/v1/users", withJSON(store.List))
    mux.HandleFunc("POST /api/v1/users", withJSON(store.Create))
    mux.HandleFunc("GET /api/v1/users/{id}", withJSON(store.Get))
    mux.HandleFunc("PUT /api/v1/users/{id}", withJSON(store.Update))
    mux.HandleFunc("DELETE /api/v1/users/{id}", withJSON(store.Delete))

    // Health check
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    // Apply global middleware
    handler := withLogging(mux)

    slog.Info("server starting", "addr", ":8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        slog.Error("server error", "error", err)
    }
}

func (s *UserStore) List(w http.ResponseWriter, r *http.Request) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    users := make([]*User, 0, len(s.users))
    for _, u := range s.users {
        users = append(users, u)
    }
    json.NewEncoder(w).Encode(users)
}

func (s *UserStore) Get(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    s.mu.RLock()
    user, ok := s.users[id]
    s.mu.RUnlock()

    if !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }
    json.NewEncoder(w).Encode(user)
}

func (s *UserStore) Create(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
        return
    }

    user.ID = generateID()
    user.CreatedAt = time.Now()

    s.mu.Lock()
    s.users[user.ID] = &user
    s.mu.Unlock()

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func (s *UserStore) Update(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    s.mu.Lock()
    defer s.mu.Unlock()

    existing, ok := s.users[id]
    if !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }

    var update User
    if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
        http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
        return
    }

    existing.Name = update.Name
    existing.Email = update.Email
    json.NewEncoder(w).Encode(existing)
}

func (s *UserStore) Delete(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")

    s.mu.Lock()
    defer s.mu.Unlock()

    if _, ok := s.users[id]; !ok {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }

    delete(s.users, id)
    w.WriteHeader(http.StatusNoContent)
}

Сравнение: стандартный роутер vs сторонние

Возможность ServeMux (< 1.22) ServeMux (1.22+) chi / gorilla/mux
Методы Нет Да Да
Параметры Нет {id} {id}
Wildcard Только / {path...} *
Middleware Нет Обёртки Встроенные
Группы Нет Нет Да
Regex в путях Нет Нет Да (gorilla)
Зависимости 0 0 1+

Вывод: Для большинства API стандартный роутер Go 1.22+ достаточен. Сторонние роутеры нужны для middleware chaining, route groups и regex в путях.

Range over integer

// БЫЛО
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// СТАЛО (Go 1.22+)
for i := range 10 {
    fmt.Println(i) // 0, 1, 2, ..., 9
}

// Practical: repeat N times without using the variable
for range 5 {
    fmt.Println("retry...")
}

// Works with any integer expression
n := getRetryCount()
for i := range n {
    if err := tryOperation(); err == nil {
        break
    }
    time.Sleep(time.Duration(i) * time.Second)
}

Исправление захвата переменных цикла

Это исправление -- одно из самых важных за всю историю Go. Оно устраняет класс багов, который мучил программистов более 10 лет.

Проблема (до Go 1.22)

// BUG: all goroutines see the same value!
func processItems(items []string) {
    var wg sync.WaitGroup
    for _, item := range items {
        wg.Add(1)
        go func() {
            defer wg.Done()
            process(item) // BUG: captures loop variable by reference
        }()
    }
    wg.Wait()
    // All goroutines process the LAST item!
}

Обходные пути (до Go 1.22)

// Workaround 1: shadow variable
for _, item := range items {
    item := item // Create new variable for each iteration
    go func() {
        process(item)
    }()
}

// Workaround 2: pass as argument
for _, item := range items {
    go func(it string) {
        process(it)
    }(item)
}

Решение (Go 1.22+)

// JUST WORKS in Go 1.22+
// Each iteration creates a new variable
for _, item := range items {
    go func() {
        process(item) // Safe! Each goroutine has its own copy
    }()
}

// Same fix for index variable
for i := range items {
    go func() {
        fmt.Println(i) // Each goroutine sees a different i
    }()
}

// Also fixed for 3-clause loops
for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(i) // Was broken before 1.22, now correct
    }()
}

Влияние на существующий код

// Old code with "item := item" still works fine
for _, item := range items {
    item := item // Unnecessary in Go 1.22+, but harmless
    go func() {
        process(item)
    }()
}
// go vet will suggest removing the redundant reassignment

math/rand/v2

package main

import (
    "fmt"
    "math/rand/v2"
)

func main() {
    // No need to seed! Auto-seeded in v2
    fmt.Println(rand.IntN(100))     // Random int in [0, 100)
    fmt.Println(rand.Float64())     // Random float in [0.0, 1.0)

    // Generic N function
    fmt.Println(rand.N[int64](1000)) // Random int64 in [0, 1000)
    fmt.Println(rand.N[uint32](256)) // Random uint32 in [0, 256)

    // New distribution-specific functions
    fmt.Println(rand.NormFloat64()) // Standard normal distribution
    fmt.Println(rand.ExpFloat64())  // Exponential distribution

    // Shuffle
    s := []string{"a", "b", "c", "d", "e"}
    rand.Shuffle(len(s), func(i, j int) {
        s[i], s[j] = s[j], s[i]
    })
}
math/rand (v1) math/rand/v2 Изменение
rand.Seed(time.Now().UnixNano()) Не нужно Автоматический seed
rand.Intn(n) rand.IntN(n) Переименовано
rand.Int63n(n) rand.N[int64](n) Дженерик
rand.Read(b) не crypto-safe Убран Используйте crypto/rand
Глобальный мьютекс Без мьютекса (per-goroutine) Быстрее

slices.Concat

a := []int{1, 2, 3}
b := []int{4, 5}
c := []int{6, 7, 8, 9}

result := slices.Concat(a, b, c)
fmt.Println(result) // [1 2 3 4 5 6 7 8 9]

Go 1.23 (Август 2024)

Go 1.23 стабилизировал одну из самых мощных фич -- range-over-function (итераторы) и добавил пакет unique для интернирования значений.

Range-over-function (Итераторы) -- стабильная версия

Итераторы позволяют использовать for range с пользовательскими функциями, создавая единый паттерн обхода любых коллекций.

Типы итераторов

import "iter"

// Single value iterator
type Seq[V any] func(yield func(V) bool)

// Key-value iterator
type Seq2[K, V any] func(yield func(K, V) bool)

Создание итераторов

package main

import (
    "fmt"
    "iter"
)

// Simple iterator: generate Fibonacci numbers
func Fibonacci() iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for {
            if !yield(a) {
                return // Consumer stopped iteration
            }
            a, b = b, a+b
        }
    }
}

func main() {
    for n := range Fibonacci() {
        if n > 100 {
            break // Triggers yield returning false
        }
        fmt.Println(n) // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
    }
}

Итератор для пользовательской коллекции

// Binary search tree with iteration
type BST[T cmp.Ordered] struct {
    root *node[T]
}

type node[T cmp.Ordered] struct {
    value       T
    left, right *node[T]
}

// InOrder returns an iterator that yields values in sorted order
func (t *BST[T]) InOrder() iter.Seq[T] {
    return func(yield func(T) bool) {
        t.inorder(t.root, yield)
    }
}

func (t *BST[T]) inorder(n *node[T], yield func(T) bool) bool {
    if n == nil {
        return true
    }
    return t.inorder(n.left, yield) &&
        yield(n.value) &&
        t.inorder(n.right, yield)
}

// Usage: natural iteration syntax
func main() {
    tree := NewBST[int]()
    tree.Insert(5, 3, 7, 1, 4, 6, 8)

    for val := range tree.InOrder() {
        fmt.Println(val) // 1, 3, 4, 5, 6, 7, 8
    }
}

Seq2: итератор с ключом и значением

// Iterate over map in sorted key order
func SortedMap[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
    return func(yield func(K, V) bool) {
        keys := make([]K, 0, len(m))
        for k := range m {
            keys = append(keys, k)
        }
        slices.Sort(keys)

        for _, k := range keys {
            if !yield(k, m[k]) {
                return
            }
        }
    }
}

func main() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   87,
        "Carol": 92,
    }

    for name, score := range SortedMap(scores) {
        fmt.Printf("%s: %d\n", name, score)
    }
    // Alice: 95
    // Bob: 87
    // Carol: 92
}

Composable iterators (цепочки итераторов)

// Filter: yield only elements matching predicate
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range seq {
            if pred(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

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

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

// Usage: pipeline composition
func main() {
    // First 5 even Fibonacci numbers
    evens := Filter(Fibonacci(), func(n int) bool { return n%2 == 0 })
    first5 := Take(evens, 5)

    for n := range first5 {
        fmt.Println(n) // 0, 2, 8, 34, 144
    }
}

Pull iterators: iter.Pull, iter.Pull2

Pull-итераторы преобразуют push-based итератор в pull-based (вызов next()):

func main() {
    // Convert push iterator to pull iterator
    next, stop := iter.Pull(Fibonacci())
    defer stop() // IMPORTANT: always stop to prevent goroutine leak

    // Pull values one at a time
    for range 10 {
        val, ok := next()
        if !ok {
            break
        }
        fmt.Println(val)
    }
}

Итераторы в стандартной библиотеке

Go 1.23 добавил итераторы во многие пакеты:

import (
    "maps"
    "slices"
    "strings"
)

// slices.All: iterate with index
for i, v := range slices.All([]string{"a", "b", "c"}) {
    fmt.Printf("%d: %s\n", i, v)
}

// slices.Values: iterate values only
for v := range slices.Values([]int{1, 2, 3}) {
    fmt.Println(v)
}

// slices.Backward: reverse iteration
for i, v := range slices.Backward([]int{1, 2, 3}) {
    fmt.Printf("%d: %d\n", i, v) // 2:3, 1:2, 0:1
}

// maps.Keys, maps.Values return iterators
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range maps.Keys(m) {
    fmt.Println(k)
}

// Collect iterator into slice
keys := slices.Sorted(maps.Keys(m))
fmt.Println(keys) // [a b c]

// strings.Lines: iterate over lines
text := "line1\nline2\nline3\n"
for line := range strings.Lines(text) {
    fmt.Print(line)
}

Пакет unique -- интернирование значений

unique позволяет создавать канонические значения, чтобы одинаковые значения занимали одну ячейку памяти.

package main

import (
    "fmt"
    "unique"
)

func main() {
    // Make creates a canonical handle for a value
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    h3 := unique.Make("world")

    // Handles with equal values are identical (pointer comparison)
    fmt.Println(h1 == h2) // true (same canonical value)
    fmt.Println(h1 == h3) // false

    // Get the value back
    fmt.Println(h1.Value()) // "hello"

    // Use case: efficient deduplication and comparison
    type Request struct {
        Method unique.Handle[string]
        Host   unique.Handle[string]
    }

    get := unique.Make("GET")
    post := unique.Make("POST")

    req := Request{Method: get, Host: unique.Make("example.com")}
    // Comparing handles is pointer comparison -- O(1), not O(n)
    if req.Method == get {
        fmt.Println("GET request")
    }
}

Когда использовать unique

Сценарий Выгода
Много одинаковых строк (HTTP headers, status codes) Экономия памяти
Частые сравнения O(1) вместо O(n)
Map ключи с длинными строками Меньше потребление памяти
Кеширование Автоматическая дедупликация

Изменения Timer/Ticker

В Go 1.23 неиспользуемые Timer и Ticker собираются сборщиком мусора. Раньше для предотвращения утечки нужно было вызывать Stop().

// БЫЛО (до Go 1.23): утечка если не вызвать Stop
timer := time.NewTimer(time.Second)
// Forgot timer.Stop() -> goroutine leak!

// СТАЛО (Go 1.23+): GC collected automatically
timer := time.NewTimer(time.Second)
// If timer goes out of scope, it's garbage collected
// No leak even without Stop()

// But Stop() is still recommended for deterministic behavior

os.CopyFS

import (
    "embed"
    "os"
)

//go:embed templates
var templateFS embed.FS

func extractTemplates(destDir string) error {
    // Copy entire embedded filesystem to disk
    return os.CopyFS(destDir, templateFS)
}

Сводная таблица Go 1.22 -- 1.23

Фича Версия Влияние Действие
Enhanced ServeMux 1.22 Критическое Рассмотреть отказ от chi/gorilla для новых проектов
Range-over-int 1.22 Среднее Упростить циклы for i := 0; i < n; i++
Loop variable fix 1.22 Критическое Убрать v := v из циклов
math/rand/v2 1.22 Среднее Перейти на v2, убрать Seed()
Range-over-func 1.23 Высокое Реализовать итераторы для коллекций
iter.Pull 1.23 Среднее Для pull-based потребления
unique package 1.23 Среднее Интернирование частых строк
Timer/Ticker GC 1.23 Низкое Меньше забот об утечках таймеров
os.CopyFS 1.23 Низкое Извлечение embed.FS на диск

Проверь себя

Для чего нужна функция iter.Pull в Go 1.23?

Какую проблему решило исправление переменных цикла в Go 1.22?

Какое преимущество даёт unique.Handle при сравнении строк?

Какой тип имеет iter.Seq[V] в Go 1.23?

Как получить path-параметр из URL в новом ServeMux Go 1.22?