HardТеория8 min

Go 1.24 – 1.25

Weak pointers, Swiss tables для map, testing/synctest, os.Root, b.Loop() для бенчмарков, generic type aliases и улучшения runtime

Go 1.24 (Февраль 2025)

Go 1.24 принёс значительные улучшения производительности (Swiss tables), новые возможности для низкоуровневого программирования (weak pointers) и долгожданные улучшения в тестировании.

Weak Pointers -- слабые ссылки

Пакет weak добавляет слабые указатели -- ссылки, которые не препятствуют сборке мусора объекта. Если объект собран GC, слабый указатель возвращает nil.

Базовое использование

package main

import (
    "fmt"
    "runtime"
    "weak"
)

func main() {
    // Create a strong pointer
    val := new(string)
    *val = "hello"

    // Create a weak pointer from strong pointer
    wp := weak.Make(val)

    // Get strong pointer back (may return nil if GC collected)
    if strong := wp.Value(); strong != nil {
        fmt.Println(*strong) // "hello"
    }

    // Drop the strong reference
    val = nil
    runtime.GC() // Force GC

    // Weak pointer now returns nil
    if strong := wp.Value(); strong == nil {
        fmt.Println("Value was garbage collected")
    }
}

Use case: кеш с автоматическим освобождением памяти

package cache

import (
    "sync"
    "weak"
)

// WeakCache stores values that can be garbage collected when memory is needed
type WeakCache[K comparable, V any] struct {
    mu    sync.RWMutex
    items map[K]weak.Pointer[V]
}

func NewWeakCache[K comparable, V any]() *WeakCache[K, V] {
    return &WeakCache[K, V]{
        items: make(map[K]weak.Pointer[V]),
    }
}

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

func (c *WeakCache[K, V]) Get(key K) (*V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()

    wp, exists := c.items[key]
    if !exists {
        return nil, false
    }

    // Value may have been garbage collected
    val := wp.Value()
    if val == nil {
        // Clean up stale entry
        c.mu.RUnlock()
        c.mu.Lock()
        delete(c.items, key)
        c.mu.Unlock()
        c.mu.RLock()
        return nil, false
    }

    return val, true
}

// Cleanup removes stale entries whose values were garbage collected
func (c *WeakCache[K, V]) Cleanup() int {
    c.mu.Lock()
    defer c.mu.Unlock()

    removed := 0
    for key, wp := range c.items {
        if wp.Value() == nil {
            delete(c.items, key)
            removed++
        }
    }
    return removed
}

Use case: каноническое значение (interning с weak pointers)

package intern

import (
    "sync"
    "weak"
)

// Interner canonicalizes values: equal values share the same pointer
type Interner[T comparable] struct {
    mu    sync.Mutex
    cache map[T]weak.Pointer[T]
}

func NewInterner[T comparable]() *Interner[T] {
    return &Interner[T]{
        cache: make(map[T]weak.Pointer[T]),
    }
}

func (in *Interner[T]) Get(val T) *T {
    in.mu.Lock()
    defer in.mu.Unlock()

    // Check if we already have this value
    if wp, ok := in.cache[val]; ok {
        if ptr := wp.Value(); ptr != nil {
            return ptr // Return existing canonical value
        }
    }

    // Create new canonical value
    ptr := new(T)
    *ptr = val
    in.cache[val] = weak.Make(ptr)
    return ptr
}

Взаимодействие с GC

Тип ссылки GC может собрать? Когда использовать
Strong (*T) Нет, пока ссылка жива Обычные данные
Weak (weak.Pointer[T]) Да, в любой момент Кеши, канонические значения

Важно: weak.Pointer[T].Value() может вернуть nil в любой момент после GC-цикла. Всегда проверяйте результат на nil.

Swiss Tables -- новая реализация map

Go 1.24 полностью заменил внутреннюю реализацию map на Swiss tables (вдохновлённые Abseil от Google). Это прозрачное изменение -- API остался прежним, но производительность значительно выросла.

Что изменилось внутри

Аспект Старая реализация Swiss Tables (1.24+)
Хеш-таблица Bucket array с overflow Open addressing с SIMD probing
Поиск (hit) ~60 ns ~40 ns (~30% быстрее)
Поиск (miss) ~80 ns ~35 ns (~55% быстрее)
Итерация Среднее До 2x быстрее
Память Больше overhead Меньше overhead на элемент
Удаление Дорогое Дешёвое (tombstone)

Практические бенчмарки

func BenchmarkMapLookup(b *testing.B) {
    m := make(map[string]int, 1000)
    for i := range 1000 {
        m[fmt.Sprintf("key_%d", i)] = i
    }

    key := "key_500"
    for b.Loop() { // Go 1.24+ benchmark syntax
        _ = m[key]
    }
}

// Go 1.23: BenchmarkMapLookup-8    ~60 ns/op
// Go 1.24: BenchmarkMapLookup-8    ~40 ns/op (Swiss tables)

Влияние на ваш код

Никакого. Swiss tables полностью обратно совместимы. Все программы работают так же, но быстрее. Единственное замечание: порядок итерации по map (и так не гарантированный) может измениться, что является ожидаемым поведением.

testing/synctest (экспериментальный) -- детерминированное тестирование конкурентности

testing/synctest позволяет тестировать конкурентный код детерминированно, контролируя расписание горутин и время.

package main

import (
    "testing"
    "testing/synctest"
    "time"
)

func TestDebounce(t *testing.T) {
    synctest.Run(func() {
        var calls int
        debounced := Debounce(100*time.Millisecond, func() {
            calls++
        })

        // Rapid fire
        debounced()
        debounced()
        debounced()

        // Advance virtual time
        synctest.Sleep(50 * time.Millisecond)
        debounced() // Reset timer

        synctest.Sleep(50 * time.Millisecond)
        // Still within debounce window, no call yet

        synctest.Sleep(100 * time.Millisecond)
        // Now the debounce fired

        if calls != 1 {
            t.Errorf("expected 1 call, got %d", calls)
        }
    })
}

func TestTimeout(t *testing.T) {
    synctest.Run(func() {
        ch := make(chan string, 1)

        go func() {
            synctest.Sleep(5 * time.Second) // Virtual time
            ch <- "result"
        }()

        // Fast-forward virtual clock
        synctest.Sleep(5 * time.Second)

        select {
        case result := <-ch:
            if result != "result" {
                t.Errorf("unexpected: %s", result)
            }
        default:
            t.Error("expected result")
        }
    })
}

Преимущества synctest

Обычные тесты synctest
time.Sleep(5*time.Second) -- ждём реально Виртуальное время -- мгновенно
Flaky из-за timing Детерминированные
Трудно тестировать timeout Простое управление временем
Порядок горутин недетерминирован Контролируемое расписание

os.Root -- ограничение файловых операций

package main

import (
    "fmt"
    "os"
)

func main() {
    // Create a root restricted to a directory
    root, err := os.OpenRoot("/var/data/uploads")
    if err != nil {
        panic(err)
    }
    defer root.Close()

    // All file operations are restricted to this directory
    f, err := root.Open("document.pdf")
    if err != nil {
        panic(err)
    }
    defer f.Close()

    // Path traversal attacks are prevented!
    _, err = root.Open("../../etc/passwd")
    // Error: path escapes root directory

    // Stat, Create, etc. also restricted
    info, err := root.Stat("report.txt")
    if err == nil {
        fmt.Printf("Size: %d\n", info.Size())
    }

    // Create file within root
    newFile, err := root.Create("output.txt")
    if err != nil {
        panic(err)
    }
    newFile.Write([]byte("Safe content"))
    newFile.Close()
}

Безопасность: предотвращение path traversal

// БЫЛО: уязвимо к path traversal
func serveFile(userPath string) (*os.File, error) {
    // User can pass "../../etc/passwd"!
    return os.Open(filepath.Join("/uploads", userPath))
}

// СТАЛО (Go 1.24+): безопасно по дизайну
func serveFile(root *os.Root, userPath string) (*os.File, error) {
    // Path traversal impossible -- restricted to root directory
    return root.Open(userPath)
}

Generic Type Aliases -- полная поддержка

// Go 1.24: generic type aliases fully supported
type Set[T comparable] = map[T]struct{}

type Pair[A, B any] = struct {
    First  A
    Second B
}

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

p := Pair[string, int]{First: "age", Second: 30}

До Go 1.24 type aliases не поддерживали параметры типов. Теперь это ограничение снято.

b.Loop() -- новый паттерн для бенчмарков

// БЫЛО: ручной цикл с b.N
func BenchmarkOld(b *testing.B) {
    for i := 0; i < b.N; i++ {
        processItem(testData)
    }
}

// СТАЛО (Go 1.24+): b.Loop() -- чище и точнее
func BenchmarkNew(b *testing.B) {
    for b.Loop() {
        processItem(testData)
    }
}

Преимущества b.Loop()

b.N loop b.Loop()
Setup code внутри цикла может исказить результат Setup вне цикла, только рабочий код в цикле
Компилятор может оптимизировать мёртвый код Предотвращает dead code elimination
Нужно помнить b.ResetTimer() Автоматическое управление таймером
func BenchmarkSort(b *testing.B) {
    // Setup: done once, outside the loop
    data := generateTestData(10000)

    for b.Loop() {
        // b.Loop() creates a fresh copy for each iteration
        // and manages timer correctly
        s := slices.Clone(data)
        slices.Sort(s)
    }
}

func BenchmarkWithSubBenchmarks(b *testing.B) {
    sizes := []int{10, 100, 1000, 10000}
    for _, size := range sizes {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            data := generateTestData(size)
            for b.Loop() {
                processData(data)
            }
        })
    }
}

json omitzero tag

type Response struct {
    Name      string    `json:"name"`
    Count     int       `json:"count,omitzero"`     // Omit if zero value
    CreatedAt time.Time `json:"created_at,omitzero"` // Omit zero time
    Tags      []string  `json:"tags,omitzero"`       // Omit nil/empty slice
}

// omitzero vs omitempty:
// omitempty: omits "", 0, false, nil, empty slice/map
// omitzero: omits only the zero value of the type (uses IsZero() if available)

type Event struct {
    Time time.Time `json:"time,omitzero"` // Uses time.Time.IsZero()
    // With omitempty, time.Time{} would NOT be omitted (it's a non-empty struct)
    // With omitzero, time.Time{} IS omitted (IsZero() returns true)
}
Tag "" 0 false nil time.Time{} []int{}
omitempty Omit Omit Omit Omit Keep Omit
omitzero Omit Omit Omit Omit Omit Omit

crypto/mlkem -- пост-квантовая криптография

package main

import (
    "crypto/mlkem"
    "fmt"
)

func main() {
    // ML-KEM (previously CRYSTALS-Kyber) -- post-quantum key encapsulation
    // Safe against quantum computer attacks

    // Generate key pair
    dk, err := mlkem.GenerateKey768()
    if err != nil {
        panic(err)
    }

    // Encapsulate: create shared secret + ciphertext
    ciphertext, sharedSecret1 := dk.EncapsulationKey().Encapsulate()

    // Decapsulate: recover shared secret from ciphertext
    sharedSecret2, err := dk.Decapsulate(ciphertext)
    if err != nil {
        panic(err)
    }

    fmt.Println(string(sharedSecret1) == string(sharedSecret2)) // true
    fmt.Printf("Shared secret: %x...\n", sharedSecret1[:16])
}

GOAUTH для приватных модулей

# Configure authentication for private module registries
GOAUTH="https://proxy.company.com:bearer:$(cat /path/to/token)" go mod download

# Multiple auth entries separated by semicolons
GOAUTH="https://github.com:bearer:$GH_TOKEN;https://gitlab.com:bearer:$GL_TOKEN"

Улучшения go tool

# go tool now supports running tools from modules
go tool golang.org/x/tools/cmd/goimports@latest .

# List available tools
go tool -n

# Run vet with specific analyzer
go tool vet -unusedresult ./...

Go 1.25 (Август 2025)

Go 1.25 сфокусирован на внутренних улучшениях runtime, продолжении оптимизации Swiss tables и стабилизации экспериментальных фич.

Удаление core types из спецификации

Core types были внутренним механизмом спецификации, усложнявшим понимание дженериков. Go 1.25 упростил спецификацию, убрав это понятие без изменения поведения кода.

// Before: the spec talked about "core type" of a constraint
// After: simpler rules about how operations work with type parameters

// Practical impact: NONE for users
// The same code compiles and works the same way
// Only the specification text changed

Продолжение оптимизации Swiss Tables

Go 1.25 продолжил оптимизацию реализации map:

  • Улучшена производительность для маленьких map (< 8 элементов)
  • Оптимизация итерации (for k, v := range m)
  • Улучшение memory layout для кеш-дружественности
  • Меньший overhead памяти для пустых map
// Benchmark comparison
// Go 1.24 Swiss tables -> Go 1.25 optimized Swiss tables:
// Small map (8 elements) lookup: ~35ns -> ~28ns
// Map iteration (1000 elements): ~2.5us -> ~1.8us
// Empty map memory: 48 bytes -> 0 bytes (nil map optimization)

Улучшения testing/synctest

Go 1.25 расширил возможности synctest:

func TestRateLimiter(t *testing.T) {
    synctest.Run(func() {
        limiter := NewRateLimiter(10, time.Second) // 10 requests per second

        // Fire 10 requests instantly
        for range 10 {
            if !limiter.Allow() {
                t.Error("should allow first 10 requests")
            }
        }

        // 11th request should be denied
        if limiter.Allow() {
            t.Error("should deny 11th request")
        }

        // Advance time by 1 second
        synctest.Sleep(time.Second)

        // Should allow again
        if !limiter.Allow() {
            t.Error("should allow after rate limit window")
        }
    })
}

Исследование Green Threads (Preview)

Go 1.25 включил экспериментальную поддержку "green threads" -- механизма, позволяющего горутинам переключаться без участия runtime scheduler в определённых случаях:

# Enable experimental green threads
GOEXPERIMENT=greenthreads go build ./...

Что это даёт:

  • Снижение latency переключения горутин
  • Лучшая производительность для I/O-bound нагрузок
  • Уменьшение overhead при большом количестве горутин (>100k)

Статус: Экспериментальная фича. Не для production. API и поведение могут измениться.

Улучшения crypto

  • Обновлённые реализации SHA-256/SHA-512 с использованием SIMD-инструкций
  • Улучшенная производительность TLS 1.3 handshake
  • Расширенная поддержка ML-KEM (пост-квантовая криптография)

Производительность стандартной библиотеки

encoding/json Marshal:    ~15% faster (reduced allocations)
net/http server:          ~10% lower latency (connection reuse)
regexp matching:          ~8% faster (optimized DFA)
fmt.Sprintf:              ~12% faster (reduced allocations for common formats)

Оптимизации компилятора

  • Улучшенный escape analysis: больше переменных остаётся на стеке
  • Лучшее инлайнирование для generic-функций
  • Оптимизация bounds check elimination для слайсов
  • Уменьшение размера бинарников на ~3-5%

Сводная таблица Go 1.24 -- 1.25

Фича Версия Влияние Действие
weak.Pointer 1.24 Среднее Использовать для кешей, canonical values
Swiss tables 1.24 Высокое Автоматическое ускорение map
testing/synctest 1.24 Высокое Детерминированные тесты конкурентности
os.Root 1.24 Среднее Безопасная работа с файлами
Generic type aliases 1.24 Среднее Упрощение generic-кода
b.Loop() 1.24 Среднее Заменить b.N циклы в бенчмарках
json omitzero 1.24 Среднее Использовать вместо omitempty для time/struct
crypto/mlkem 1.24 Низкое Пост-квантовая безопасность
GOAUTH 1.24 Среднее Упрощение работы с приватными модулями
Core types removal 1.25 Нет Никаких действий, только упрощение спеки
Swiss table improvements 1.25 Среднее Автоматическое ускорение
Green threads preview 1.25 Нет (эксперимент) Только для тестирования
Runtime performance 1.25 Среднее Автоматическое ускорение

Проверь себя

Что возвращает weak.Pointer[T].Value() после сборки мусора объекта?

Какое главное отличие b.Loop() от цикла for i := 0; i < b.N; i++ в бенчмарках?

Чем omitzero отличается от omitempty для поля time.Time в JSON?

Какую проблему безопасности решает os.Root в Go 1.24?