golang

Go 1.26 вышел, пройдемся по всем изменениям…

  • пятница, 13 февраля 2026 г. в 00:00:06
https://habr.com/ru/articles/995906/

Go 1.26 уже вышел! Официальные релизноты довольно скудны на детализацию и приходится изучать глубже. Сделал для тебя большой обзор нововведений, можешь использовать эту статью как шпоргалку. В начале коротко опишу то что лично мне понравилось больше всего. Изменения затрагивают runtime, компилятор, стандартную библиотеку и поддержку платформ. Команда Go сосредоточилась на производительности и удобстве разработки.

Главное изменение: Green Tea становится сборщиком мусора по умолчанию. Алгоритм разрабатывался с 2018 года и прошёл обкатку в Go 1.25. Вместо обхода объектов по указателям GC работает со страницами памяти целиком. Благодаря этому улучшается работа с кэшем процессора, а нагрузка на CPU при сборке мусора снижается от 10% до 40% на реальных задачах.

На уровне языка функция new() теперь теперь принимает произвольные выражения. Это означает, что больше не нужны вспомогательные функции вроде StringPtr() для создания указателей на примитивы.

В стандартной библиотеке появились новые пакеты. Пакет crypto/hpke реализует Hybrid Public Key Encryption по RFC 9180 с поддержкой постквантовых алгоритмов. Экспериментальный simd/archsimd открывает доступ к SIMD-инструкциям процессора. А runtime/secret обеспечивает гарантированное затирание секретных данных из памяти, что важно для безопасной работы с ключами и паролями.

Криптографические пакеты также обновлены. Постквантовые key exchanges включены по умолчанию в TLS. Добавлены интерфейсы Encapsulator и Decapsulator для работы с KEM-алгоритмами. Многие функции теперь используют детерминированную генерацию случайных чисел, что упрощает тестирование.

Линкер научился работать с cgo-программами на windows/arm64 без внешних зависимостей. Добавлена секция go.module, оптимизирована структура ELF-бинарников. В результате размер исполняемых файлов сокращён за счёт удаления секции .gosymtab.

Есть изменения, влияющие на совместимость. Удалён 32-битный порт Windows ARM, FreeBSD riscv64 помечен как broken, macOS 12 Monterey поддерживается последний раз. Кроме того, несколько GODEBUG-настроек будут удалены в Go 1.27, поэтому стоит проверить код на использование устаревших флагов.

Содержание

В процессе...

Расширение функции new() с поддержкой выражений

В Go всегда существовала асимметрия при создании указателей. Для составных типов можно было написать &MyStruct{field: value}, но для примитивных типов такой возможности не было. Чтобы получить указатель на число или строку, приходилось использовать промежуточную переменную:

// Старый подход: две строки вместо одной
n := 42
p := &n

// Или создавать вспомогательные функции
func IntPtr(v int) *int {
    return &v
}

Эта проблема привела к появлению тысяч однотипных функций StringPtr, Int64Ptr, BoolPtr в кодовых базах по всему миру. Rob Pike поднял этот вопрос в proposal #45624, и спустя более четырёх лет обсуждений решение наконец принято в Go 1.26.

Теперь встроенная функция new() принимает не только тип, но и произвольное выражение. Если аргумент является выражением типа T, то new(expr) выделяет переменную типа T, инициализирует её значением выражения и возвращает указатель типа *T.

// Go 1.26: создание указателей одной строкой
p := new(42)           // *int со значением 42
s := new("hello")      // *string со значением "hello"
b := new(true)         // *bool со значением true
f := new(3.14159)      // *float64 со значением 3.14159

Одна из самых частых ситуаций, где требуются указатели на примитивы, это опциональные поля при сериализации в JSON. Указатель позволяет отличить "значение не задано" (nil) от "задано нулевое значение".

// До Go 1.26:
type Person struct {
    Name string `json:"name"`
    Age  *int   `json:"age,omitempty"`
}

// Создание с опциональным полем
age := 30
person := Person{
    Name: "Alice",
    Age:  &age,
}

// Go 1.26:
type Person struct {
    Name string `json:"name"`
    Age  *int   `json:"age,omitempty"`
}

// Создание стало компактнее
person := Person{
    Name: "Alice",
    Age:  new(30),
}

Новый синтаксис работает с любыми типами, что особенно удобно при инициализации сложных структур данных:

// Указатель на слайс
nums := new([]int{1, 2, 3})

// Указатель на карту
m := new(map[string]int{"a": 1, "b": 2})

Кроме того, синтаксис работает и с результатами вызовов функций:

type Config struct {
    CreatedAt *time.Time `json:"created_at,omitempty"`
}

config := Config{
    CreatedAt: new(time.Now()),
}

Передача nil в new() по-прежнему вызывает ошибку компиляции. Это ожидаемое поведение: компилятор не может определить тип для nil без дополнительного контекста.

p := new(nil) // ошибка компиляции: cannot use nil as a value

Для нетипизированных констант Go применяет стандартные правила вывода типа по умолчанию. Целые числа становятся int, числа с плавающей точкой становятся float64, а комплексные числа становятся complex128. На практике это означает, что если нужен другой тип, следует использовать явное приведение:

p := new(42)          // *int
p64 := new(int64(42)) // *int64
p32 := new(int32(42)) // *int32

Рассмотрим типичный сценарий с API-запросом, где некоторые поля опциональны:

type UpdateUserRequest struct {
    Name     *string `json:"name,omitempty"`
    Email    *string `json:"email,omitempty"`
    Age      *int    `json:"age,omitempty"`
    IsActive *bool   `json:"is_active,omitempty"`
}

// До Go 1.26: много промежуточных переменных или helper-функции
name := "John"
age := 25
active := true
req := UpdateUserRequest{
    Name:     &name,
    Age:      &age,
    IsActive: &active,
}

// Go 1.26: без промежуточных переменных
req := UpdateUserRequest{
    Name:     new("John"),
    Age:      new(25),
    IsActive: new(true),
}

Proposal #45624 - оригинальный proposal от Rob Pike.

Инструменты

Go 1.26 приносит три заметных изменения в инструментах командной строки. Каждое из них направлено на упрощение повседневной работы разработчика, и вместе они отражают общий принцип: инструменты должны работать так, как их уже используют на практике.

Первое изменение затрагивает документацию. Команда go tool doc удалена, и теперь её полностью заменяет go doc. Это логичное завершение эволюции, которая началась ещё в Go 1.12, когда godoc был разделён на веб-сервер и CLI-инструмент. На практике это означает, что больше не нужно думать, какую команду использовать: есть только один способ получить документацию из командной строки.

Благодаря этому переходим ко второму изменению, которое касается go fix. Команда полностью переработана и теперь использует ту же архитектуру анализаторов, что и go vet. Старые фиксеры, исправлявшие код под изменения времён Go 1.0-1.1, удалены. На их место пришёл современный набор анализаторов из пакета modernize, который помогает переводить код на актуальные конструкции языка. Это важно, потому что теперь один и тот же анализатор может и находить проблемы через go vet, и исправлять их через go fix.

Третье изменение улучшает профилирование. Веб-интерфейс pprof теперь по умолчанию открывает flame graph вместо графа вызовов. Flame graph давно стал стандартом в индустрии для анализа производительности, и Go наконец отражает эту практику в настройках по умолчанию. В результате разработчику не нужно каждый раз переключать вид вручную.

Все три изменения объединяет общая идея: убрать лишнее и сделать инструмен��ы такими, какими их и так уже используют.

Удаление cmd/doc и go tool doc

Начиная с 1.26 удалены cmd/doc и команда go tool doc. Их полностью заменяет go doc, которая принимает те же флаги и аргументы. Это означает, что весь существующий код и скрипты продолжат работать после простой замены команды.

# Старый подход
go tool doc fmt.Println

# Новый подход
go doc fmt.Println

Инструменты документации в Go прошли долгий путь эволюции. В Go 1.1 godoc был единственным инструментом для просмотра документации. Затем в Go 1.5 появился go doc как отдельная CLI-команда, что дало разработчикам быстрый способ получить справку без запуска веб-сервера. В Go 1.12 godoc стал только веб-сервером, а все CLI-функции окончательно перешли к go doc. Теперь в Go 1.26 go tool doc удалён, и цикл эволюции завершён.

Существование двух команд с одинаковой функциональностью создавало путаницу. Разработчики часто не понимали, в чём разница между go doc и go tool doc. На практике разницы не было: это был один и тот же инструмент с двумя точками входа. Удаление дубликата следует философии Go, где один очевидный способ сделать что-то лучше, чем несколько похожих.

Proposal #59056 - оригинальный prosolan от Alan Donovan.

Переработка go fix на базе analysis framework

Команда go fix полностью переработана. Теперь она использует тот же фреймворк анализа (golang.org/x/tools/go/analysis), что и go vet. На практике это означает, что анализаторы, предоставляющие диагностику в go vet, теперь могут предлагать и применять исправления через go fix. Благодаря единой архитектуре разработчикам достаточно написать анализатор один раз, и он будет работать с обеими командами.

Все исторические фиксеры удалены из go fix. Среди них buildtag для исправления старого синтаксиса // +build, cftype для инициализаторов C-типов, context для миграции импортов с golang.org/x/net/context, egl/eglconf для инициализаторов EGL, gotypes для обновления импортов golang.org/x/tools, jni для исправлений JNI-типов, netipv6zone для ключей в composite literals и printerconfig для ключей элементов Config.

Эти фиксеры были актуальны для миграции с Go 1.0-1.1. В 2026 году код, требующий таких исправлений, практически не встречается, поэтому их удаление не затронет современные проекты.

Новые анализаторы: пакет modernize

На смену старым фиксерам пришёл пакет modernize с набором анализаторов для модернизации кода. Эти анализаторы помогают переводить проекты на современные конструкции языка, появившиеся в последних версиях Go. Рассмотрим новые из них:

newexpr: использование нового синтаксиса new(expr)

// До
func ptrTo(x int) *int {
    return &x
}

// После
func ptrTo(x int) *int {
    return new(x)
}

Доступно с Go 1.26. Новый синтаксис делает намерение явным и избавляет от необходимости создавать промежуточную переменную.

errorsastype: использование errors.AsType

// До
var myErr *MyError
if errors.As(err, &myErr) {
    handle(myErr)
}

// После
if myErr, ok := errors.AsType[*MyError](err); ok {
    handle(myErr)
}

Доступно с Go 1.26. Типобезопасный вариант понятнее читается и позволяет объявить переменную прямо в условии.

Команда go fix получила новые флаги:

# Показать изменения в формате unified diff вместо применения
go fix -diff ./...

# Вывести изменения в формате JSON
go fix -json ./...

# Использовать альтернативный инструмент проверки
go fix -vettool=myanalyzer ./...

Теперь go fix и go vet используют одну инфраструктуру. Разница только в действии: go vet сообщает о проблемах, а go fix применяет исправления.

Благодаря этому любой анализатор с поддержкой suggested fixes может работать с обеими командами. Для разработчиков инструментов это упрощает поддержку, а для пользователей означает согласованное поведение между командами.

Подробности об интеграции анализаторов описаны в Proposal #71859 от Jonathan Amsterdam, а решение об удалении cmd/fix обсуждалось в Proposal #73605 от Alan Donovan. Документация анализаторов доступна в пакете modernize.

Flame graph по умолчанию в pprof

Веб-интерфейс pprof (запускаемый с флагом -http) теперь открывает flame graph по умолчанию. Раньше по умолчанию показывался граф вызовов, и разработчикам приходилось вручную переключаться на более удобное представление.

# Открыть pprof с веб-интерфейсом
go tool pprof -http=:8080 cpu.prof

При переходе по адресу http://localhost:8080 ты сразу увидишь flame graph вместо графа вызовов. Граф вызовов при этом никуда не делся. Чтобы его открыть, можно использовать меню View → Graph или перейти напрямую по URL /ui/graph.

Почему flame graph

Flame graph стал стандартом в индустрии для анализа производительности благодаря нескольким преимуществам.

Вся информация о стеке вызовов умещается на одном экране. Каждый прямоугольник представляет функцию, а ширина прямоугольника показывает долю времени. Клик по прямоугольнику увеличивает эту часть стека, что упрощает исследование конкретных ветвей выполнения. Широкие прямоугольники сразу бросаются в глаза, и это функции, которые занимают больше всего времени. В результате поиск узких мест занимает секунды вместо минут.

Нижний уровень представляет точку входа, например main или обработчик запроса. Каждый уровень выше показывает вызванные функции, а ширина прямоугольника отражает процент от общего времени.

Профилирование веб-сервера:

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // pprof-эндпоинты автоматически регистрируются
    http.ListenAndServe(":6060", nil)
}

Сбор и анализ профиля:

# Сбор CPU-профиля (30 секунд)
curl http://localhost:6060/debug/pprof/profile?seconds=30 > cpu.prof

# Анализ с flame graph по умолчанию
go tool pprof -http=:8080 cpu.prof

Оригинальная концепция flame graph описана в статье Brendan Gregg. Документация пакета профилирования находится в pprof.

Runtime

Новая версия приносит масштабные изменения в runtime. Главное из них: Green Tea GC становится сборщиком мусора по умолчанию. Этот алгоритм был экспериментальным в Go 1.25 и за это время показал себя достаточно стабильным для production-использования в Google и других крупных компаниях. На практике это означает, что большинство приложений получат снижение нагрузки на CPU от сборки мусора без каких-либо изменений в коде.

Green Tea переосмысливает подход к сканированию памяти. Традиционный GC обходил объекты по указателям, прыгая между далёкими адресами и постоянно промахиваясь по кэшу процессора. Новый алгоритм работает со страницами памяти целиком, обрабатывая объекты последовательно. Благодаря этому кардинально улучшается работа с кэшем, а предсказуемый порядок доступа позволяет использовать векторные инструкции для дополнительного ускорения.

Помимо нового GC, в этом релизе улучшена производительность cgo-вызовов (примерно на 30%) и аллокации мелких объектов (до 37% быстрее для объектов менее 512 байт). Оба изменения работают автоматически и не требуют модификации кода. Для приложений с активным использованием FFI или частыми аллокациями это даёт заметный прирост производительности.

Отдельного внимания заслуживает экспериментальная функция обнаружения утечек горутин. Она основана на исследовании Vlad Saioc из Uber и использует фазу маркировки GC для поиска заблокированных горутин, которые никогда не смогут продолжить выполнение. Это позволяет находить проблемы, которые раньше требовали специальных инструментов или ручного анализа.

В результате все изменения этого раздела направлены на одну цель: сделать Go ещё эффективнее на современном железе без дополнительных усилий со стороны разработчика.

Green Tea Garbage Collector

Традиционный сборщик мусора Go работал по принципу обхода графа объектов. Он начинал с корневых объектов и следовал по указателям, отмечая достижимые объекты как живые. Проблема этого подхода в том, что указатели ведут в произвольные места памяти: процессор постоянно прыгает между далёкими адресами, что приводит к промахам кэша.

Исследования показали, что более 35% времени сканирования памяти тратится на ожидание данных из оперативной памяти. Это означает, что процессор простаивает, пока данные не придут из RAM в кэш, и дорогие вычислительные ресурсы расходуются впустую.

Green Tea решает эту проблему, переосмыслив сам подход к сканированию. Вместо того чтобы следовать за указателями и прыгать по памяти, новый алгоритм группирует работу по страницам и обрабатывает объекты последовательно.

Green Tea переходит от объектно-ориентированного сканирования к страничному. Вместо списка отдельных объектов рабочий список содержит 8 КБ страницы памяти (spans). На практике это означает, что данные на одной странице уже находятся в кэше процессора, когда GC переходит к следующему объекту.

Традиционный GC:  Объект A → Объект B → Объект C → ...
                  (прыжки по всей памяти)

Green Tea:        Страница 1 → Страница 2 → Страница 3 → ...
                  (последовательное сканирование внутри страниц)

Когда GC находит объект, который нужно просканировать, он не сканирует его сразу. Вместо этого он отмечает позицию объекта в его странице и ждёт. Когда на странице накапливается несколько объектов для сканирования, GC обрабатывает их все за один проход.

Для реализации этого подхода Green Tea использует два бита метаданных на каждый слот объекта. Первый бит (seen) отмечает, что указатель на объект был обнаружен. Второй бит (scanned) указывает, что объект уже просканирован. Благодаря этому алгоритм может накапливать работу на уровне страниц, не теряя информации об отдельных объектах, и обрабатывать их пакетно, когда на странице наберётся достаточно задач.

Сканирование происходит в четыре шага. Сначала алгоритм извлекает страницу из рабочей очереди. Затем сравнивает seen и scanned биты, чтобы найти объекты, требующие обработки. После этого сканирует найденные объекты последовательно, в порядке их расположения в памяти. Наконец, добавляет новые обнаруженные страницы в очередь.

Ключевое отличие от традиционного подхода: объекты на одной странице обрабатываются вместе. Поэтому данные уже находятся в кэше, и процессор не простаивает в ожидании загрузки из RAM.

Green Tea использует распределённую модель балансировки нагрузки. У каждого воркера своя локальная очередь страниц, и если воркер закончил работу, он может "украсть" задачи из очередей других воркеров. Такой подход устраняет центральную точку синхронизации и снижает contention между ядрами процессора. В результате GC эффективно масштабируется на многоядерных системах без накладных расходов на глобальные блокировки.

Результаты на реальных нагрузках:

Метрика

Значение

Типичное улучшение

10% снижение CPU на GC

Максимальное улучшение

до 40% снижение CPU на GC

Дополнительно с AVX-512

ещё ~10% на поддерживаемых CPU

Для приложения, которое тратит 10% CPU на сборку мусора, Green Tea даёт экономию от 1% до 4% общего CPU.

Вот бенчмарк, демонстрирующий улучшение аллокаций в сценарии с частым GC:

package main

import (
    "runtime"
    "testing"
)

type Node struct {
    Value int
    Left  *Node
    Right *Node
}

func buildTree(depth int) *Node {
    if depth == 0 {
        return nil
    }
    return &Node{
        Value: depth,
        Left:  buildTree(depth - 1),
        Right: buildTree(depth - 1),
    }
}

func BenchmarkGCPressure(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // Создаём дерево с ~2^15 узлами
        root := buildTree(15)
        runtime.KeepAlive(root)

        // Принудительный GC
        runtime.GC()
    }
}

Результаты на Go 1.25 vs Go 1.26 (Green Tea по умолчанию), Apple M3 Pro:

Бенчмарк

Go 1.25

Go 1.26

Изменение

GCPressure

1.01 ms/op

591 µs/op

-42%

GCPressureDeep

7.57 ms/op

4.07 ms/op

-46%

GCPressureMultiple

1.39 ms/op

1.00 ms/op

-28%

Green Tea демонстрирует улучшение от 28% до 46% в зависимости от глубины графа объектов. Чем глубже дерево, тем больше выигрыш — для глубокого дерева из 262143 узлов улучшение достигает 46%.

В редких случаях может понадобиться вернуться к традиционному GC, например для диагностики регрессий или сравнения производительности:

GOEXPERIMENT=nogreenteagc go build ./...

Эта опция будет удалена в Go 1.27.

Векторные инструкции используются автоматически на поддерживаемых процессорах. Это процессоры Intel начиная с Ice Lake и AMD начиная с Zen 4. Специальной настройки не требуется: runtime определяет возможности CPU при старте и выбирает оптимальный путь ��ыполнения. Об этом мы поговорим ниже.

Green Tea показывает меньшее улучшение в нескольких случаях. Если страницы содержат по одному объекту, накладные расходы на накопление не окупаются. При нерегулярной структуре кучи алгоритм работает хуже, поскольку он оптимизирован для объектов схожего размера. Также если GC занимает мало CPU, улучшение будет незаметным.

Тем не менее даже при 2% заполнении страницы Green Tea часто превосходит традиционный подход благодаря лучшей работе с кэшем.

Работа над концепцией Green Tea началась в 2018 году. В 2024 году Austin Clements создал прототип, который доказал жизнеспособность идеи. Уже в Go 1.25 появилась экспериментальная реализация, которую начали использовать в production в Google. Наконец, в Go 1.26 алгоритм становится GC по умолчанию и получает векторное ускорение.

SIMD-оптимизации для сканирования объектов

Green Tea GC открывает возможность, которая была недоступна традиционному сборщику мусора: использование векторных инструкций процессора. Когда объекты обрабатываются последовательно в пределах страницы, данные становятся предсказуемыми, и это позволяет применить SIMD. На практике это означает, что одна инструкция обрабатывает сразу несколько элементов данных вместо последовательного цикла.

В Go 1.26 реализовано векторное ускорение сканирования на процессорах с поддержкой AVX-512. Это даёт дополнительные ~10% снижения нагрузки на GC поверх базового улучшени�� от Green Tea. Для приложений с высоким давлением на сборщик мусора суммарное улучшение может достигать 50%.

Как работает векторное сканирование

Традиционный подход

При традиционном сканировании GC обрабатывает объекты по одному. Для каждого объекта:

  1. Загрузить метаданные объекта

  2. Проверить, какие слова содержат указатели

  3. Для каждого указателя — проверить и отметить целевой объект

Это работает, но не использует возможности современных процессоров обрабатывать несколько элементов данных одной инструкцией.

Векторный подход

Green Tea обрабатывает целую страницу за раз. Метаданные страницы (битовые карты seen/scanned, карта указателей) помещаются в 512-битные регистры. Сначала алгоритм загружает битовые карты seen и scanned, затем вычисляет "активные объекты" через операцию seen XOR scanned. После этого расширяет биты (1 бит на объект преобразуется в N бит на слово), пересекает результат с картой указателей и собирает указатели для проверки.

Каждый из этих шагов выполняется одной-двумя векторными инструкциями вместо цикла. Поэтому на странице с сотнями объектов векторный код выполняется в разы быстрее скалярного.

Ключевая инструкция для расширения битов называется VGF2P8AFFINEQB. Это аффинное преобразование над полем Галуа GF(2), которое позволяет за одну операцию преобразовать 1 бит на объект в N бит на слово.

Для разных классов размеров объектов используются разные матрицы преобразования. Эти матрицы генерируются автоматически кодогенератором src/internal/runtime/gc/scan/mkasm.go. Благодаря этому код остаётся поддерживаемым, хотя и использует низкоуровневые оптимизации.

Векторное ускорение работает на:

Производитель

Архитектура

Примеры

Intel

Ice Lake+

Core 10th gen+, Xeon 3rd gen+

AMD

Zen 4+

Ryzen 7000+, EPYC 9004+

На более старых процессорах Green Tea работает без векторного ускорения, но всё ещё даёт улучшение за счёт страничного подхода.

Определить, использует ли твоя программа векторное сканирование, можно через метрики runtime:

package main

import (
    "fmt"
    "runtime/metrics"
)

func main() {
    // Получаем информацию о CPU features
    samples := make([]metrics.Sample, 1)
    samples[0].Name = "/cpu/classes/gc/mark:cpu-seconds"

    metrics.Read(samples)

    fmt.Printf("GC mark time: %v\n", samples[0].Value.Float64())
}

Если у тебя современный процессор и нагрузка с частым GC, ты увидишь снижение времени mark phase.

Реализация находится в файле src/internal/runtime/gc/scan/scan_amd64.s. Это ассемблерный код, оптимизированный для конкретной архитектуры. При запуске runtime проверяет возможности процессора и выбирает подходящий путь выполнения:

// Упрощённая структура кода
func scanPage(page *mspan) {
    if cpu.HasAVX512 && page.sizeClass <= maxVectorizedClass {
        scanPageAVX512(page)  // Векторный путь
    } else {
        scanPageScalar(page)  // Обычный путь
    }
}

В будущих версиях Go Issue #73787 планируется переписать ассемблерный код на Go с использованием нового пакета simd/archsimd. Это упростит поддержку и позволит добавить оптимизации для других архитектур.

Результаты на процессоре с AVX-512:

Сценарий

Улучшение

Базовый Green Tea (без AVX-512)

10-40% снижение CPU на GC

Green Tea + AVX-512

дополнительно ~10%

Итого на поддерживаемых CPU

до 50% снижение CPU на GC

Векторное ускорение имеет несколько ограничений. Сейчас оно работает только на архитектуре amd64, поскольку ARM и другие платформы пока не поддерживаются. Кроме того, оптимизация применяется только для мелких объектов до определённого порога размера. Наконец, требуется поддержка AVX-512, и на старых процессорах runtime использует обычный скалярный код.

  • The Green Tea Garbage Collector - статья в блоге посвещенная Green Teae, в ней есть раздел о векторном ускорении.

  • Issue #73787 - план переписать на go.

Ускорение cgo-вызовов

Базовый overhead вызовов cgo снижен примерно на 30%. Это изменение работает автоматически и не требует модификации кода.

Каждый вызов из Go в C (и обратно) требует значительных накладных расходов. Go использует сегментированные стеки, тогда как C ожидает традиционный стек, поэтому нужно переключение. Регистры Go и C используются по-разному, и состояние необходимо сохранять. Runtime должен знать, что горутина находится в C-коде, чтобы правильно синхронизироваться с GC. Также Go нужно иметь возможность остановить горутину в точках вытеснения.

В результате эти накладные расходы делают cgo на порядки медленнее обычного вызова функции Go.

Go 1.26 оптимизирует несколько аспектов cgo-вызовов. Переключение контекста теперь использует меньше инструкций на входе и выходе из C. Снижен overhead на синхронизацию с GC. Сохранение регистров стало более эффективным: теперь сохраняются только те регистры, которые действительно нужны. Вместе эти оптимизации дают около 30% ускорения для типичных cgo-вызовов.

Рассмотрим простой бенчмарк, измеряющий overhead cgo-вызова:

package main

/*
int add(int a, int b) {
    return a + b;
}
*/
import "C"
import "testing"

func BenchmarkCgoCall(b *testing.B) {
    for i := 0; i < b.N; i++ {
        C.add(C.int(i), C.int(i+1))
    }
}

func BenchmarkPureGo(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = i + i + 1
    }
}

Результаты (Apple M3 Pro):

Бенчмарк

Go 1.25

Go 1.26

Изменение

CgoCall

25.9 ns/op

17.4 ns/op

-33%

PureGo

0.80 ns/op

0.79 ns/op

~0%

Cgo-вызов по-прежнему намного медленнее чистого Go, но разрыв сократился.

Улучшение наиболее заметно в сценариях:

// Использование OpenSSL через cgo
func EncryptAES(data []byte, key []byte) []byte {
    // Каждый вызов C.EVP_* получает ускорение
    return encryptWithOpenSSL(data, key)
}

// Прямые вызовы libc
func GetHostname() string {
    var buf [256]C.char
    C.gethostname(&buf[0], 256)
    return C.GoString(&buf[0])
}

// Работа с базами данных через C-драйвер
func QueryDatabase(query string) *Result {
    cquery := C.CString(query)
    defer C.free(unsafe.Pointer(cquery))
    return C.execute_query(cquery)  // Ускорено на ~30%
}

Вызовы с callback (из C обратно в Go) также получили ускорение:

package main

/*
#include <stdlib.h>

extern int goCallback(int);

int callWithCallback(int n) {
    return goCallback(n) + 1;
}
*/
import "C"

//export goCallback
func goCallback(n C.int) C.int {
    return n * 2
}

func BenchmarkCgoCallback(b *testing.B) {
    for i := 0; i < b.N; i++ {
        C.callWithCallback(C.int(i))
    }
}

Результаты (Apple M3 Pro):

Бенчмарк

Go 1.25

Go 1.26

Изменение

CgoCallback

61.9 ns/op

48.0 ns/op

-22%

Улучшение cgo критично в нескольких сценариях. Для высоконагруженных приложений с FFI каждый сэкономленный наносекунд умножается на миллионы вызовов и даёт заметную экономию. В real-time системах меньший jitter от cgo-вызовов улучшает предсказуемость времени отклика. Для биндингов к производительным библиотекам (OpenSSL, SQLite, графические движки) снижение накладных расходов позволяет эффективнее использовать возможности нативного кода.

Изменение полностью прозрачно для пользователя. Оно не требует изменений в коде и не требует перекомпиляции C-библиотек. Ускорение работает на всех платформах с поддержкой cgo, поэтому достаточно просто обновить версию Go.

Оптимизация аллокации мелких объектов

Компилятор Go 1.26 генерирует вызовы специализированных подпрограмм аллокации для объектов размером менее 512 байт. Это снижает накладные расходы на выделение памяти для самого распространённого класса объектов.

Большинство объектов в типичной Go-программе занимают мало памяти. Это структуры конфигурации, узлы деревьев и связных списков, временные буферы, объекты запросов и ответов. Раньше все аллокации проходили через универсальный путь в runtime, который обрабатывал объекты любого размера. Теперь для мелких объектов используются специализированные функции, знающие точный размер. Благодаря этому удаётся избежать лишних проверок и сразу переходить к нужному аллокатору.

// До Go 1.26
// Компилятор генерировал вызов общей функции
p := runtime.newobject(typ)  // typ содержит размер и другую информацию

Внутри newobject происходила последовательность действий: извлечение размера из типа, определение класса размера, выбор подходящего аллокатора и собственно выделение памяти.

// Go 1.26
// Компилятор генерирует вызов специализированной функции
p := runtime.newobject_size32(typ)  // Размер известен на этапе компиляции

Специализированная функция сразу идёт к нужному аллокатору, пропускает проверки размера и использует инлайн-оптимизации. В результате путь от вызова до выделения памяти становится значительно короче.

package main

import "testing"

type SmallStruct struct {
    A, B, C, D int64  // 32 байта
}

type MediumStruct struct {
    Data [64]byte  // 64 байта
}

func BenchmarkAllocSmall(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := new(SmallStruct)
        _ = s
    }
}

func BenchmarkAllocMedium(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := new(MediumStruct)
        _ = s
    }
}

func BenchmarkAllocSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]byte, 128)
        _ = s
    }
}

Результаты (Apple M3 Pro):

Бенчмарк

Go 1.25

Go 1.26

Изменение

AllocSmall (32B)

10.16 ns/op

6.15 ns/op

-39%

AllocMedium (64B)

11.21 ns/op

8.63 ns/op

-23%

AllocSlice (128B)

15.73 ns/op

13.54 ns/op

-14%

Чем меньше размер аллокации, тем больше относительный выигрыш в производительности. Для маленьких структур (32 байта) ускорение достигает 39%, что особенно заметно в приложениях с высокой частотой создания объектов — парсерах, сериализаторах, обработчиках запросов.

Оптимизация не применяется в нескольких случаях. Для объектов размером 512 байт и более используется обычный путь аллокации. При динамических размерах, например make([]byte, n) где n неизвестен во время компиляции, компилятор не может выбрать специализированную функцию. Также reflect-аллокации через reflect.New(typ) не могут использовать эту оптимизацию, поскольку тип известен только в runtime.

Оптимизация аллокации и Green Tea GC дополняют друг друга. На этапе аллокации память для мелких объектов выделяется быстрее благодаря специализированным функциям. На этапе сборки Green Tea эффективнее сканирует страницы с мелкими объектами благодаря векторным оптимизациям. Вместе они дают существенное улучшение для программ с интенсивной аллокацией, что особенно заметно на веб-серверах и системах обработки данных.

Memory Allocation - руководство по GC.

Экспериментальные профили утечек горутин

На мой субьективный взгляд это самое ожидаемое изменение. Утечка горутин представляет собой одну из самых коварных проблем в Go. Горутина, заблокированная на канале без получателя, остаётся в памяти навсегда. Она не завершается, не освобождает ресурсы, и обнаружить её крайне сложно. На практике такие утечки накапливаются постепенно и проявляются только под нагрузкой, когда сервис уже потребляет гигабайты памяти.

func processWorkItems(ws []workItem) ([]workResult, error) {
    ch := make(chan result)

    for _, w := range ws {
        go func() {
            res, err := processWorkItem(w)
            ch <- result{res, err}  // Блокируется навсегда при ошибке
        }()
    }

    var results []workResult
    for range len(ws) {
        r := <-ch
        if r.err != nil {
            return nil, r.err  // Выход при ошибке — горутины утекают
        }
        results = append(results, r.res)
    }
    return results, nil
}

Если processWorkItem вернёт ошибку для первого элемента, функция завершится. Остальные горутины продолжат работать и заблокируются на ch <- result, потому что никто не читает из канала.

Go 1.26 добавляет экспериментальный профиль для обнаружения таких утечек. Он использует фазу маркировки GC для поиска горутин, заблокированных на недостижимых объектах синхронизации. Это позволяет находить проблемы без дополнительных инструментов и изменений в коде.

GOEXPERIMENT=goroutineleakprofile go build ./...

Алгоритм опирается на простое наблюдение. Сначала GC определяет, что канал (или mutex, или cond) недостижим из корней. Затем горутина, заблокированная на этом канале, помечается как утечка. Наконец, информация становится доступна через профиль.

Ключевая идея: если канал недостижим, никто никогда не сможет записать в него или прочитать из него. Поэтому горутина, ожидающая на таком канале, заблокирована навсегда и никогда не сможет продолжить выполнение.

Используем через pprof endpoint

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {
    go http.ListenAndServe(":6060", nil)
    // ...
}

Профиль доступен по адресу:

http://localhost:6060/debug/pprof/goroutineleak

Используем через runtime/pprof

import (
    "os"
    "runtime/pprof"
)

func checkLeaks() {
    f, _ := os.Create("goroutineleak.prof")
    defer f.Close()

    // Принудительный GC для обновления информации об утечках
    runtime.GC()

    pprof.Lookup("goroutineleak").WriteTo(f, 0)
}

Анализ профиля

go tool pprof goroutineleak.prof
(pprof) top
(pprof) list processWorkItems

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

package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "time"
)

func leakyFunction() {
    ch := make(chan int)

    go func() {
        // Эта горутина заблокируется навсегда
        ch <- 42
    }()

    // Не читаем из канала — горутина утекает
    // ch становится недостижим после выхода из функции
}

func main() {
    go http.ListenAndServe(":6060", nil)

    // Создаём утечки
    for i := 0; i < 100; i++ {
        leakyFunction()
    }

    // Даём GC время обнаружить утечки
    runtime.GC()
    time.Sleep(time.Second)

    fmt.Println("Check http://localhost:6060/debug/pprof/goroutineleak")
    select {}
}

Запуск:

GOEXPERIMENT=goroutineleakprofile go run main.go

Анализ:

curl http://localhost:6060/debug/pprof/goroutineleak > leak.prof
go tool pprof leak.prof

Профиль обнаруживает горутины, заблокированные на:

Примитив

Пример

Небуферизированный канал

ch <- x или <-ch

Буферизированный канал (полный/пустой)

ch <- x при len(ch) == cap(ch)

sync.Mutex

mu.Lock()

sync.RWMutex

rw.RLock() или rw.Lock()

sync.Cond

cond.Wait()

Профиль спроектирован с нулевым overhead. Если профиль не используется, никаких дополнительных затрат нет. Информация собирается во время обычной работы GC без дополнительных проходов по памяти. Чтение профиля также не влияет на производительность приложения. Благодаря этому профиль можно безопасно включать в production для мониторинга.

Профиль goroutineleak находится в статусе эксперимента для сбора обратной связи. В Go 1.27 планируется включить его по умолчанию. Также возможны изменения API на основе полученного feedback от пользователей.

Функция основана на исследовании Vlad Saioc из Uber, опубликованном в ACM:

"Unveiling and Vanquishing Goroutine Leaks in Enterprise Microservices"

Исследование проводилось на 75 миллионах строк кода в монорепозитории Uber с более чем 2500 микросервисами. Оно систематически изучило распространённость утечек горутин и предложило метод их обнаружения через достижимость памяти.

До Go 1.26 для обнаружения утечек использовались другие подходы. Библиотека uber-go/goleak проверяет утечки в тестах. Ручной анализ goroutine dump требует экспертизы и времени. Мониторинг метрик /sched/goroutines показывает рост числа горутин, но не указывает на причину.

Новый профиль дополняет эти инструменты, предоставляя обнаружение в runtime без изменений кода. При этом он точно указывает на место утечки, что значительно упрощает диагностику.

Компилятор

Go 1.26 содержит улучшения в escape analysis, позволяющие размещать backing store слайсов на стеке в некоторых случаях. Однако на практике эти улучшения работают не для всех паттернов. Рассмотрим, где оптимизация срабатывает, а где данные по-прежнему уходят в кучу.

Рассмотрим типичный паттерн, временный буфер для чтения:

func processData(r io.Reader) error {
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        // обработка buf[:n]
    }
}

В Go 1.25 и ранее buf размещался в куче по двум причинам. Во-первых, размер известен только в runtime, и хотя в примере это известное значение, компилятор не всегда мог это доказать. Во-вторых, слайс передаётся в метод интерфейса (r.Read), что традиционно означало escape.

Go 1.26 в теории может анализировать такие ситуации глубже. Однако на практике многие паттерны по-прежнему приводят к размещению в куче, особенно когда слайс передаётся во внешние функции.

Рассмотрим функцию с временным буфером:

// examples/stack_alloc/main.go
package main

import (
    "crypto/sha256"
    "fmt"
)

func hashWithTempBuffer(data []byte) [32]byte {
    temp := make([]byte, len(data))
    copy(temp, data)
    return sha256.Sum256(temp)
}

func main() {
    data := []byte("Hello, Go 1.26!")
    hash := hashWithTempBuffer(data)
    fmt.Printf("%x\n", hash)
}

Проверим escape analysis:

$ go build -gcflags="-m" ./examples/stack_alloc/
# В данном примере буфер по-прежнему уходит в кучу:
./main.go:10:15: make([]byte, len(data)) escapes to heap

Несмотря на улучшения в Go 1.26, этот конкретный паттерн по-прежнему приводит к аллокации в куче. Причина в том, что len(data) известен только в runtime, и компилятор не может гарантировать, что размер достаточно мал для стека.

Измерим влияние на производительность:

// examples/stack_alloc/bench_test.go
package main

import (
    "crypto/sha256"
    "testing"
)

func BenchmarkHashWithTempBuffer(b *testing.B) {
    data := make([]byte, 512)
    for i := range data {
        data[i] = byte(i)
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = hashWithTempBuffer(data)
    }
}

func BenchmarkHashDirect(b *testing.B) {
    data := make([]byte, 512)
    for i := range data {
        data[i] = byte(i)
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = sha256.Sum256(data)
    }
}

Результаты на Apple M3 Pro:

Бенчмарк

Go 1.25

Go 1.26

Изменение

HashWithTempBuffer

72.6 ns/op, 1 alloc

70.7 ns/op, 1 alloc

~3% быстрее

HashDirect

50.5 ns/op, 0 allocs

48.9 ns/op, 0 allocs

~3% быстрее

Аллокация по-прежнему происходит (1 alloc/op). Небольшое ускорение (~3%) связано с общими улучшениями рантайма и аллокатора, а не с escape analysis.

Когда это не работает

Компилятор не сможет разместить слайс на стеке в нескольких случаях. Понимание этих ограничений помогает писать код, который лучше оптимизируется.

Первое ограничение связано с размером. Есть внутренний порог, после которого стек становится неподходящим местом. Это защищает от переполнения стека при работе с большими буферами.

Второе ограничение возникает, когда указатель сохраняется куда-то долгоживущее. Если адрес элемента слайса записывается в структуру или глобальную переменную, которая переживёт функцию, слайс уходит в кучу. Компилятор не может гарантировать, что стековая память останется валидной после возврата из функции.

Третий случай очевиден: слайс возвращается из функции. Это классический пример escape, когда данные должны пережить вызов.

func badExample() []byte {
    buf := make([]byte, 100)
    return buf // buf escapes
}

var global []byte

func anotherBadExample() {
    buf := make([]byte, 100)
    global = buf // buf escapes через global
}

Работа над этой оптимизацией велась в нескольких направлениях. Исходный proposal #27625 описывает идею автоматического размещения небольших слайсов на стеке. Proposal #72036 посвящён улучшениям escape analysis для параметров интерфейсных методов, что и позволило реализовать эту оптимизацию. В #73199 собраны известные проблемы с unsafe.Pointer и новыми оптимизациями, если столкнёшься с неожиданным поведением.

Улучшения escape analysis в Go 1.26 работают не для всех паттернов. В моем бенчмарке с временным буфером аллокация сохранилась - компилятор по-прежнему не может разместить слайс на стеке, когда размер зависит от аргумента функции. Возможно проблема в моем железе или в том что я тестировал на rc2.

Для оптимизации подобных случаев рекомендуется использовать sync.Pool или передавать буфер извне. Если критична производительность, проверяй escape analysis с флагом -gcflags="-m" - не полагайся на предположения о поведении компилятора.

Линкер

Go 1.26 вносит заметные изменения в линкер. Большая часть касается внутренней структуры исполняемых файлов: перемещение секций, исправление метаданных, удаление устаревших артефактов. На работу программ это не влияет, но важно для тех, кто анализирует бинарники Go. Понимание этих изменений поможет адаптировать инструменты анализа и избежать ложных срабатываний при работе с новыми бинарниками.

Главное функциональное изменение в этой версии связано с поддержкой internal linking для cgo-программ на windows/arm64. Раньше эта платформа требовала внешнего линкера, теперь можно обойтись встроенным. Это упрощает настройку сборки и уменьшает количество внешних зависимостей.

Остальные изменения технические. Появилась отдельная секция для moduledata, исправлена длина поля cutab, секция .gopclntab переехала в rodata-сегмент, а пустая .gosymtab наконец удалена. Также изменился порядок секций в ELF-файлах: теперь они отсортированы по адресу. Благодаря этому структура бинарников стала более предсказуемой и удобной для анализа.

Если ты разрабатываешь инструменты для анализа Go-бинарников (отладчики, профилировщики, средства reverse engineering), эти изменения стоит учесть. Для остальных разработчиков всё работает как раньше.

Bootstrap

Go 1.26 обновляет требования к bootstrap-тулчейну. Теперь для сборки Go из исходников нужен Go 1.24.6 или новее. Это важно понимать, если ты собираешь компилятор самостоятельно, а не используешь готовые бинарники.

Повышение версии происходит по стандартной политике проекта. Go N требует Go N-2, где N-2 округляется вниз до чётного числа. Для Go 1.26 это Go 1.24.x, конкретно минорный релиз 1.24.6. Такой подход обеспечивает предсказуемость: ты всегда знаешь, какой компилятор понадобится для сборки новой версии.

На практике это изменение касается только тех, кто собирает Go из исходного кода. Если ты используешь готовые бинарники с go.dev/dl, это тебя не затрагивает.

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

Go 1.26 вводит несколько новых пакетов, расширяющих возможности стандартной библиотеки в трёх направлениях: криптография, низкоуровневая оптимизация и безопасность данных в памяти. Эти дополнения отражают приоритеты команды Go: современные стандарты шифрования, высокая производительность вычислений и надёжная защита секретных данных.

Главное дополнение в области криптографии: пакет crypto/hpke, реализующий Hybrid Public Key Encryption по стандарту RFC 9180. Это современный протокол шифрования с открытым ключом, который уже поддерживает постквантовые алгоритмы. На практике это означает, что разработчики получают готовую реализацию протокола, используемого в TLS 1.3 Encrypted Client Hello и других современных системах, без необходимости подключать внешние зависимости.

В направлении производительности появился экспериментальный пакет simd/archsimd, открывающий доступ к SIMD-инструкциям процессора напрямую из Go (про это подробнее писалось выше). Пока поддерживается только AMD64, но это первый шаг к полноценной поддержке векторных вычислений в стандартной библиотеке. Благодаря этому код, требующий обработки больших массивов данных, можно ускорить в несколько раз без написания ассемблера.

Для криптографических библиотек добавлен пакет runtime/secret, обеспечивающий гарантированное затирание секретных данных из памяти. Это решает давнюю проблему forward secrecy: эфемерные ключи должны исчезать сразу после использования. В результате авторы криптографических библиотек получают официальный механизм вместо ненадёжных хаков с unsafe.

Также появился вспомогательный пакет crypto/mlkem/mlkemtest с детерминированными функциями инкапсуляции для тестирования ML-KEM реализаций. Он позволяет проверять совместимость с другими реализациями постквантового алгоритма, используя известные тестовые векторы.

Рассмотрим каждый из этих пакетов подробнее.

crypto/hpke: Hybrid Public Key Encryption

Пакет crypto/hpke реализует Hybrid Public Key Encryption (HPKE) согласно RFC 9180. Это современный стандарт шифрования с открытым ключом, объединяющий механизм инкапсуляции ключа (KEM), функцию деривации ключа (KDF) и аутентифицированное шифрование (AEAD).

HPKE используется в TLS 1.3 Encrypted Client Hello, MLS (Messaging Layer Security) и других современных протоколах. Раньше для этого приходилось подключать внешние зависимости вроде filippo.io/hpke. Теперь всё доступно в стандартной библиотеке. На практике это упрощает разработку и аудит безопасности, поскольку криптографический код поставляется вместе с Go и проходит проверку командой разработчиков языка.

HPKE состоит из трёх частей, каждая из которых отвечает за свой этап защиты данных. Первая часть, KEM (Key Encapsulation Mechanism), обеспечивает механизм обмена ключами. Пакет поддерживает DHKEM на кривых P-256, P-384, P-521, X25519, а также постквантовые ML-KEM-768, ML-KEM-1024 и гибридные варианты. Вторая часть, KDF (Key Derivation Function), отвечает за деривацию ключей и поддерживает HKDF-SHA256, HKDF-SHA384, HKDF-SHA512, SHAKE128, SHAKE256. Третья часть, AEAD (Authenticated Encryption with Associated Data), выполняет шифрование с аутентификацией через AES-128-GCM, AES-256-GCM или ChaCha20-Poly1305. Такая модульная архитектура позволяет выбирать оптимальную комбинацию алгоритмов для конкретного протокола.

Для одноразовых сообщений есть удобные функции Seal и Open:

package main

import (
    "crypto/ecdh"
    "crypto/hpke"
    "fmt"
    "log"
)

func main() {
    // Выбираем алгоритмы
    kem := hpke.DHKEM(ecdh.X25519())
    kdf := hpke.HKDFSHA256()
    aead := hpke.AES256GCM()

    // Получатель генерирует ключевую пару
    privateKey, err := kem.GenerateKey()
    if err != nil {
        log.Fatal(err)
    }
    publicKey := privateKey.PublicKey()

    // Отправитель шифрует сообщение
    message := []byte("Привет из Go 1.26!")
    info := []byte("example context")

    ciphertext, err := hpke.Seal(publicKey, kdf, aead, info, message)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Зашифровано: %x\n", ciphertext)

    // Получатель расшифровывает
    plaintext, err := hpke.Open(privateKey, kdf, aead, info, ciphertext)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Расшифровано: %s\n", plaintext)
}

Функция Seal возвращает конкатенацию инкапсулированного ключа и шифротекста. Open принимает то же самое и извлекает исходное сообщение.

Для отправки нескольких сообщений в рамках одной сессии используйте Sender и Recipient:

package main

import (
    "crypto/ecdh"
    "crypto/hpke"
    "fmt"
    "log"
)

func main() {
    kem := hpke.DHKEM(ecdh.X25519())
    kdf := hpke.HKDFSHA256()
    aead := hpke.ChaCha20Poly1305()

    // Получатель
    privateKey, _ := kem.GenerateKey()
    publicKey := privateKey.PublicKey()

    // Отправитель создаёт контекст
    enc, sender, err := hpke.NewSender(publicKey, kdf, aead, []byte("session info"))
    if err != nil {
        log.Fatal(err)
    }

    // Получатель создаёт контекст из enc
    recipient, err := hpke.NewRecipient(enc, privateKey, kdf, aead, []byte("session info"))
    if err != nil {
        log.Fatal(err)
    }

    // Теперь можно отправлять несколько сообщений
    messages := []string{"Сообщение 1", "Сообщение 2", "Сообщение 3"}

    for _, msg := range messages {
        // Шифруем
        ct, err := sender.Seal(nil, []byte(msg))
        if err != nil {
            log.Fatal(err)
        }

        // Расшифровываем
        pt, err := recipient.Open(nil, ct)
        if err != nil {
            log.Fatal(err)
        }

        fmt.Printf("Отправлено: %s, получено: %s\n", msg, pt)
    }
}

Каждый вызов Seal использует инкрементирующийся nonce. Порядок расшифровки должен совпадать с порядком шифрования.

Помимо классических алгоритмов, пакет поддерживает постквантовые и гибридные варианты. Это позволяет защитить данные от будущих атак с использованием квантовых компьютеров уже сегодня:

package main

import (
    "crypto/hpke"
    "fmt"
    "log"
)

func main() {
    // Гибридный KEM: ML-KEM-768 + X25519 (X-Wing)
    // Устойчив к атакам квантовых компьютеров
    kem := hpke.MLKEM768X25519()
    kdf := hpke.HKDFSHA256()
    aead := hpke.AES256GCM()

    privateKey, err := kem.GenerateKey()
    if err != nil {
        log.Fatal(err)
    }

    message := []byte("Постквантовое сообщение")
    ct, err := hpke.Seal(privateKey.PublicKey(), kdf, aead, nil, message)
    if err != nil {
        log.Fatal(err)
    }

    pt, err := hpke.Open(privateKey, kdf, aead, nil, ct)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Размер публичного ключа: %d байт\n", len(privateKey.PublicKey().Bytes()))
    fmt.Printf("Размер шифротекста: %d байт\n", len(ct))
    fmt.Printf("Сообщение: %s\n", pt)
}

Доступные постквантовые KEM:

KEM

Описание

Размер публичного ключа

MLKEM768()

Чистый ML-KEM-768

1184 байт

MLKEM1024()

Чистый ML-KEM-1024

1568 байт

MLKEM768X25519()

Гибрид ML-KEM-768 + X25519

1216 байт

MLKEM768P256()

Гибрид ML-KEM-768 + P-256

1249 байт

MLKEM1024P384()

Гибрид ML-KEM-1024 + P-384

1665 байт

Гибридные варианты сочетают классическую и постквантовую криптографию. Если квантовые компьютеры окажутся слабее ожидаемого, классическая часть всё ещё защитит данные.

Помимо шифрования сообщений, HPKE позволяет экспортировать производные ключи для использования в других протоколах:

// После создания Sender или Recipient
exportedKey, err := sender.Export("my protocol key", 32)
if err != nil {
    log.Fatal(err)
}
// exportedKey содержит 32 байта, уникальных для этой сессии и контекста

Это полезно для протоколов, где HPKE используется только для установления сессии, а дальнейшее шифрование выполняется другими средствами. Благодаря этому можно интегрировать HPKE в существующие системы без полной переработки криптографической архитектуры.

Работа с существующими ключами

Если у тебя уже есть ключи из crypto/ecdh или crypto/mlkem, их можно обернуть:

import (
    "crypto/ecdh"
    "crypto/hpke"
    "crypto/mlkem"
)

// ECDH ключ
ecdhKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
hpkeKey, _ := hpke.NewDHKEMPrivateKey(ecdhKey)

// ML-KEM ключ
mlkemKey, _ := mlkem.GenerateKey768()
hpkeMLKEMKey, _ := hpke.NewMLKEMPrivateKey(mlkemKey)

// Гибридный ключ
hybridKey, _ := hpke.NewHybridPrivateKey(mlkemKey, ecdhKey)

Для протоколов, где алгоритмы согласуются динамически:

// Получаем ID из протокола
kemID := uint16(0x0020) // X25519
kdfID := uint16(0x0001) // HKDF-SHA256
aeadID := uint16(0x0001) // AES-128-GCM

kem, err := hpke.NewKEM(kemID)
kdf, err := hpke.NewKDF(kdfID)
aead, err := hpke.NewAEAD(aeadID)

HPKE подходит для шифрования сообщений получателю с известным публичным ключом, реализации протоколов вроде ECH и MLS, миграции с устаревших схем на современные стандарты, а также постквантовой защиты данных уже сегодня. Это универсальный инструмент для асимметричного шифрования по современным стандартам.

При этом HPKE не заменяет TLS для защиты соединений, симметричное шифрование для больших объёмов данных и подпись данных, поскольку протокол не аутентифицирует отправителя. Каждый из этих сценариев требует специализированных инструментов.

simd/archsimd: SIMD-инструкции в Go

Экспериментальный пакет simd/archsimd даёт доступ к SIMD-инструкциям процессора напрямую из Go. SIMD (Single Instruction, Multiple Data) позволяет выполнять одну операцию сразу над несколькими значениями, ускоряя вычисления в разы. Это особенно важно для задач обработки изображений, машинно��о обучения и научных вычислений.

До Go 1.26 для использования SIMD приходилось писать ассемблер или использовать CGO. Теперь есть типобезопасный API в стандартной библиотеке. Благодаря этому разработчики могут получить значительное ускорение без выхода за пределы языка Go.

Пакет экспериментальный, для его использования нужен флаг:

GOEXPERIMENT=simd go build ./...

Сейчас поддерживается только AMD64 (x86-64). Поддержка ARM64 планируется в будущих версиях.

Пакет определяет типы для векторов разной ширины, соответствующих возможностям современных процессоров.

Для 128-битных операций (SSE) доступны типы Int8x16, Int16x8, Int32x4, Int64x2 для знаковых целых, Uint8x16, Uint16x8, Uint32x4, Uint64x2 для беззнаковых, а также Float32x4 и Float64x2 для чисел с плавающей точкой.

Для 256-битных операций (AVX2) набор аналогичен: Int8x32, Int16x16, Int32x8, Int64x4 для знаковых целых, Uint8x32, Uint16x16, Uint32x8, Uint64x4 для беззнаковых, Float32x8 и Float64x4 для плавающей точки.

Для 512-битных операций (AVX-512) доступны Int8x64, Int16x32, Int32x16, Int64x8, Uint8x64, Uint16x32, Uint32x16, Uint64x8, Float32x16 и Float64x8.

Название типа отражает тип элемента и количество элементов в векторе. Например, Float32x4 содержит четыре 32-битных float, что в сумме составляет 128 бит.

Базовые операции

package main

import (
    "fmt"
    "simd/archsimd"
)

func main() {
    // Загрузка из массива
    data := [4]float32{1.0, 2.0, 3.0, 4.0}
    v := archsimd.LoadFloat32x4(&data)

    // Broadcast: все элементы одинаковы
    ones := archsimd.BroadcastFloat32x4(1.0)

    // Арифметика
    result := v.Add(ones)

    // Сохранение
    var output [4]float32
    result.Store(&output)

    fmt.Println(output) // [2 3 4 5]
}

Загрузка из слайса

slice := []float32{1, 2, 3, 4, 5, 6, 7, 8}
v := archsimd.LoadFloat32x4Slice(slice) // загружает первые 4 элемента
v.StoreSlice(slice[4:])                  // сохраняет в элементы 4-7

Арифметические операции

Все базовые операции работают поэлементно:

a := archsimd.LoadFloat32x4(&[4]float32{1, 2, 3, 4})
b := archsimd.LoadFloat32x4(&[4]float32{5, 6, 7, 8})

sum := a.Add(b)        // [6, 8, 10, 12]
diff := a.Sub(b)       // [-4, -4, -4, -4]
prod := a.Mul(b)       // [5, 12, 21, 32]
quot := a.Div(b)       // [0.2, 0.33, 0.43, 0.5]
sqrt := a.Sqrt()       // [1, 1.41, 1.73, 2]

// Fused multiply-add: a*b + c за одну инструкцию
c := archsimd.BroadcastFloat32x4(10)
fma := a.MulAdd(b, c)  // [15, 22, 31, 42]

Сравнения и маски

Сравнения возвращают маску, то есть вектор с -1 (true) или 0 (false):

a := archsimd.LoadFloat32x4(&[4]float32{1, 5, 3, 7})
b := archsimd.LoadFloat32x4(&[4]float32{2, 4, 3, 8})

mask := a.Less(b)      // [-1, 0, 0, -1] (1<2, 5>4, 3==3, 7<8)
eq := a.Equal(b)       // [0, 0, -1, 0]
gt := a.Greater(b)     // [0, -1, 0, 0]

Маски используются для условных операций:

// Выбор элементов: если mask[i] != 0, берём a[i], иначе b[i]
result := a.Merge(b, mask)

Min/Max

a := archsimd.LoadFloat32x4(&[4]float32{1, 5, 3, 7})
b := archsimd.LoadFloat32x4(&[4]float32{2, 4, 6, 1})

min := a.Min(b) // [1, 4, 3, 1]
max := a.Max(b) // [2, 5, 6, 7]

Побитовые операции (целочисленные типы)

a := archsimd.LoadInt32x4(&[4]int32{0xFF, 0xF0, 0x0F, 0x00})
b := archsimd.LoadInt32x4(&[4]int32{0x0F, 0x0F, 0x0F, 0x0F})

and := a.And(b)           // [0x0F, 0x00, 0x0F, 0x00]
or := a.Or(b)             // [0xFF, 0xFF, 0x0F, 0x0F]
xor := a.Xor(b)           // [0xF0, 0xFF, 0x00, 0x0F]
not := a.Not()            // побитовое отрицание

shifted := a.ShiftAllLeft(4) // сдвиг всех элементов на 4 бита влево

Перестановки

a := archsimd.LoadFloat32x4(&[4]float32{10, 20, 30, 40})

// Индексы для перестановки
idx := archsimd.LoadUint32x4(&[4]uint32{3, 2, 1, 0})
reversed := a.Permute(idx) // [40, 30, 20, 10]

// Конкатенация и перестановка из двух векторов
b := archsimd.LoadFloat32x4(&[4]float32{50, 60, 70, 80})
idx2 := archsimd.LoadUint32x4(&[4]uint32{0, 4, 1, 5})
interleaved := a.ConcatPermute(b, idx2) // [10, 50, 20, 60]

Практический пример: скалярное произведение

package main

import (
    "fmt"
    "simd/archsimd"
)

func dotProductSIMD(a, b []float32) float32 {
    if len(a) != len(b) {
        panic("slices must have equal length")
    }

    var acc archsimd.Float32x4
    n := len(a)

    // Обрабатываем по 4 элемента за раз
    for i := 0; i+4 <= n; i += 4 {
        va := archsimd.LoadFloat32x4Slice(a[i:])
        vb := archsimd.LoadFloat32x4Slice(b[i:])
        acc = acc.MulAdd(va, vb)
    }

    // Горизонтальная сумма
    var result [4]float32
    acc.Store(&result)
    sum := result[0] + result[1] + result[2] + result[3]

    // Хвост
    for i := n - n%4; i < n; i++ {
        sum += a[i] * b[i]
    }

    return sum
}

func dotProductScalar(a, b []float32) float32 {
    var sum float32
    for i := range a {
        sum += a[i] * b[i]
    }
    return sum
}

func main() {
    a := make([]float32, 1000)
    b := make([]float32, 1000)
    for i := range a {
        a[i] = float32(i)
        b[i] = float32(i)
    }

    fmt.Printf("Scalar: %f\n", dotProductScalar(a, b))
    fmt.Printf("SIMD:   %f\n", dotProductSIMD(a, b))
}

Сделать полноценный бенчмарк мне не удалось, но где-то я натыкался на результат бенчмарка на Intel Core i7-12700 (к сожалению ссылку на источник не сохранил):

BenchmarkDotProductScalar-16    1234567    972.3 ns/op
BenchmarkDotProductSIMD-16      4567890    262.1 ns/op

# Ускорение: ~3.7x

Важно учитывать несколько ограничений при работе с пакетом. Во-первых, поддерживается только архитектура AMD64, поэтому код с simd/archsimd не скомпилируется для ARM64 или других платформ. Во-вторых, пакет находится в статусе эксперимента, и API может измениться в будущих версиях, поэтому не стоит использовать его в production без готовности к изменениям. В-третьих, 512-битные типы требуют процессора с поддержкой AVX-512, и на старых процессорах код завершится с ошибкой. Наконец, компилятор не выполняет автовекторизацию: обычный цикл не превратится в SIMD автоматически, нужно явно использовать векторные типы.

// Проверка возможностей CPU

import "internal/cpu"

func hasSIMD() bool {
    return cpu.X86.HasSSE2 // минимум для 128-бит
}

func hasAVX2() bool {
    return cpu.X86.HasAVX2 // для 256-бит
}

func hasAVX512() bool {
    return cpu.X86.HasAVX512F // для 512-бит
}

Команда go планирует добавить высокоуровневый портируемый SIMD-пакет поверх archsimd. Он будет компилироваться на любой архитектуре, используя SIMD где возможно, и скалярный код где нет. Пакет simd/archsimd представляет собой низкоуровневый фундамент для тех, кому нужна максимальная производительность и контроль.

  • Issue #73787: proposal и обсуждение

  • simd/archsimd: документация пакета

  • go-simdcsv: пример использования для парсинга CSV

runtime/secret: затирание секретных данных

Экспериментальный пакет runtime/secret решает проблему надёжного удаления криптографических ключей из памяти. Это критически важно для forward secrecy, то есть свойства протоколов, при котором компрометация долгосрочных ключей не позволяет расшифровать прошлые сессии. На практике это означает, что даже при получении доступа к памяти процесса злоумышленник не найдёт эфемерные ключи завершённых сессий.

Криптографические протоколы вроде TLS или WireGuard используют эфемерные ключи для каждой сессии. После завершения handshake эти ключи должны ис��езнуть из памяти. Если злоумышленник позже получит доступ к памяти процесса (через уязвимость или физический доступ), он не должен найти эфемерные ключи прошлых сессий.

Проблема в том, что Go не даёт гарантий удаления данных. Сборщик мусора может скопировать объект перед очисткой оригинала, стек не затирается при возврате из функции, регистры сохраняют значения после завершения вычислений, а простое memset(key, 0, len) может быть оптимизировано компилятором и вообще не выполниться. Все эти факторы создавали серьёзную проблему для авторов криптографических библиотек.

До Go 1.26 криптографические библиотеки использовали хаки с unsafe и надеялись на лучшее. Теперь есть официальное решение, которое гарантирует затирание на уровне runtime.

# Пакет экспериментальный, по этому...
GOEXPERIMENT=runtimesecret go build ./...

Поддерживаются AMD64 и ARM64 на Linux. На других платформах secret.Do работает как обычный вызов функции без гарантий затирания.

package secret

// Do выполняет f и затирает всё временное хранилище:
// регистры, стек, новые аллокации в куче
func Do(f func())

// Enabled возвращает true, если вызов находится внутри Do
func Enabled() bool
package main

import (
    "crypto/ecdh"
    "crypto/rand"
    "runtime/secret"
)

type SessionKey [32]byte

func deriveSessionKey(peerPublic *ecdh.PublicKey) SessionKey {
    var sessionKey SessionKey

    secret.Do(func() {
        // Генерируем эфемерную ключевую пару
        ephemeral, err := ecdh.X25519().GenerateKey(rand.Reader)
        if err != nil {
            panic(err)
        }

        // Вычисляем shared secret
        shared, err := ephemeral.ECDH(peerPublic)
        if err != nil {
            panic(err)
        }

        // Копируем результат наружу
        copy(sessionKey[:], shared[:32])

        // ephemeral и shared будут затёрты при выходе из Do
    })

    return sessionKey
}

После выхода из secret.Do регистры CPU очищены, стек, использованный внутри функции, затёрт, а аллокации в куче помечены для затирания при сборке мусора. Это обеспечивает комплексную защиту секретных данных на всех уровнях хранения.

secret.Do выполняет несколько действий:

  1. Запоминает состояние стека и кучи

  2. Выполняет переданную функцию

  3. Очищает регистры перед возвратом

  4. Затирает стековый фрейм функции

  5. Помечает новые аллокации для затирания при GC

Для кучи важно понимать: затирание происходит не мгновенно. Пока GC не определит, что объект недостижим, данные остаются в памяти. Если нужно немедленное затирание, вызовите runtime.GC() после secret.Do.

При работе с пакетом важно учитывать несколько ограничений. Глобальные переменные не защищены: если внутри secret.Do записать значение в глобальную переменную, оно не будет затёрто:

var globalKey []byte // плохо!

secret.Do(func() {
    key := generateKey()
    globalKey = key // key не будет затёрт!
})

Кроме того, горутины запрещены. Запуск горутины внутри secret.Do вызовет панику:

secret.Do(func() {
    go doSomething() // panic!
})

Это ограничение связано со сложностью отслеживания памяти в конкурентном контексте. В Go 1.27 планируется поддержка горутин.

Также следует учитывать платформенные ограничения: пакет полноценно работает только на Linux AMD64/ARM64, а на других платформах Do вызывает функцию напрямую без затирания. Есть и теоретический вектор атаки через GC-буферы: адреса указателей, которые GC сохраняет во внутренних буферах, не затираются. Впрочем, на практике этот вектор сложно эксплуатируемый.

// Проверка поддержки
package main

import (
    "fmt"
    "runtime/secret"
)

func main() {
    var secretSupported bool

    secret.Do(func() {
        secretSupported = secret.Enabled()
    })

    if secretSupported {
        fmt.Println("runtime/secret активен")
    } else {
        fmt.Println("runtime/secret работает в режиме совместимости")
    }
}
// Практический пример: TLS handshake
package main

import (
    "crypto/ecdh"
    "crypto/hpke"
    "crypto/rand"
    "runtime/secret"
)

type TLSSession struct {
    TrafficSecret [32]byte
}

func performHandshake(serverPubKey *ecdh.PublicKey) *TLSSession {
    session := &TLSSession{}

    secret.Do(func() {
        // Эфемерный ключ клиента
        clientEphemeral, _ := ecdh.X25519().GenerateKey(rand.Reader)

        // Shared secret
        sharedSecret, _ := clientEphemeral.ECDH(serverPubKey)

        // Деривация ключей (упрощённо)
        kem := hpke.DHKEM(ecdh.X25519())
        kdf := hpke.HKDFSHA256()
        aead := hpke.AES256GCM()

        priv, _ := hpke.NewDHKEMPrivateKey(clientEphemeral)
        _, sender, _ := hpke.NewSender(priv.PublicKey(), kdf, aead, nil)

        trafficKey, _ := sender.Export("client traffic", 32)
        copy(session.TrafficSecret[:], trafficKey)

        // clientEphemeral, sharedSecret и промежуточные ключи
        // будут затёрты при выходе из Do
        _ = sharedSecret
    })

    return session
}

Накладные расходы заметны, но для криптографических операций, которые и так занимают микросекунды, это приемлемо.

Пакет предназначен для авторов криптографических библиотек, включая реализации TLS/DTLS, VPN-клиенты и серверы, менеджеры секретов и HSM-интеграции. Все эти системы работают с эфемерными ключами, которые должны надёжно удаляться после использования. Прикладным разработчикам обычно не нужно использовать runtime/secret напрямую. Криптографические библиотеки сделают это за вас, обеспечивая forward secrecy прозрачно для пользователя.

crypto/mlkem/mlkemtest: тестирование ML-KEM

Пакет crypto/mlkem/mlkemtest предоставляет детерминированные функции инкапсуляции для тестирования ML-KEM реализаций с известными тестовыми векторами. Это позволяет проверять совместимость с другими реализациями постквантового алгоритма. ML-KEM (ранее известный как Kyber) является постквантовым алгоритмом инкапсуляции ключей. При нормальной работе функция Encapsulate использует криптографически стойкий генератор случайных чисел. Это правильно для production, но создаёт проблему для тестирования, поскольку результат каждый раз отличается.

Допустим, ты реализуешь протокол поверх ML-KEM и хочешь проверить совместимость с другими реализациями. У тебя есть тестовые векторы: входные данные и ожидаемый результат. Но результат Encapsulate каждый раз разный из-за случайности.

Решение этой проблемы заключается в детерминированных версиях функций, которые принимают "случайность" как параметр. Такой подход уже используется в других криптографических пакетах, например в crypto/rsa для OAEP. Благодаря этому можно воспроизводить результаты и сравнивать их с эталонными значениями.

package mlkemtest

// Encapsulate768 выполняет детерминированную инкапсуляцию ML-KEM-768
// random должен содержать 32 байта
func Encapsulate768(ek *mlkem.EncapsulationKey768, random []byte) (
    ciphertext []byte,
    sharedSecret []byte,
    err error,
)

// Encapsulate1024 выполняет детерминированную инкапсуляцию ML-KEM-1024
// random должен содержать 32 байта
func Encapsulate1024(ek *mlkem.EncapsulationKey1024, random []byte) (
    ciphertext []byte,
    sharedSecret []byte,
    err error,
)

Пример: проверка тестового вектора

package main

import (
    "bytes"
    "crypto/mlkem"
    "crypto/mlkem/mlkemtest"
    "encoding/hex"
    "fmt"
    "log"
)

func main() {
    // Тестовый вектор из спецификации или другой реализации
    seedHex := "7c9935a0b07694aa0c6d10e4db6b1add2fd81a25ccb14803"
    randomHex := "147c03f7a5bebba406c8fae1874d7f13c80efe79a3a9a874cc09fe76"
    expectedCiphertextPrefix := "7a5c63..." // сокращено
    expectedSharedSecretHex := "a1b2c3..."   // сокращено

    // Генерируем ключ из seed (для воспроизводимости)
    seed, _ := hex.DecodeString(seedHex)
    dk, err := mlkem.NewDecapsulationKey768(seed)
    if err != nil {
        log.Fatal(err)
    }

    // Инкапсулируем с фиксированной "случайностью"
    random, _ := hex.DecodeString(randomHex)
    ek := dk.EncapsulationKey()

    ciphertext, sharedSecret, err := mlkemtest.Encapsulate768(ek, random)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Ciphertext: %x\n", ciphertext[:32])
    fmt.Printf("Shared secret: %x\n", sharedSecret)

    // Проверяем совпадение с ожидаемым результатом
    expectedSharedSecret, _ := hex.DecodeString(expectedSharedSecretHex)
    if !bytes.Equal(sharedSecret, expectedSharedSecret) {
        log.Fatal("shared secret mismatch!")
    }

    // Проверяем, что decapsulation работает
    decapsulated, err := dk.Decapsulate(ciphertext)
    if err != nil {
        log.Fatal(err)
    }

    if !bytes.Equal(decapsulated, sharedSecret) {
        log.Fatal("decapsulation mismatch!")
    }

    fmt.Println("Тестовый вектор прошёл проверку")
}

Использование в тестах протокола

package myprotocol_test

import (
    "crypto/mlkem"
    "crypto/mlkem/mlkemtest"
    "testing"
)

func TestProtocolHandshake(t *testing.T) {
    // Фиксированные данные для воспроизводимого теста
    seed := make([]byte, 64)
    random := make([]byte, 32)

    // Ключи
    dk, _ := mlkem.NewDecapsulationKey768(seed)
    ek := dk.EncapsulationKey()

    // Детерминированная инкапсуляция
    ct, ss, err := mlkemtest.Encapsulate768(ek, random)
    if err != nil {
        t.Fatal(err)
    }

    // Тестируем протокол с известными значениями
    clientSession := NewClientSession(ct, ss)
    serverSession := NewServerSession(dk, ct)

    // Проверяем, что обе стороны получили одинаковый ключ
    if clientSession.Key() != serverSession.Key() {
        t.Error("keys don't match")
    }
}

Пакет находится в crypto/mlkem/mlkemtest, а не crypto/mlkem. Это явный сигнал о том, что детерминированные функции предназначены только для тестов. Использование фиксированной "случайности" в production полностью ломает безопасность ML-KEM. Атакующий, зная random, может вычислить shared secret без приватного ключа. Поэтому крайне важно использовать эти функции исключительно в тестовом коде. Если тебе кажется, что в production нужна детерминированная инкапсуляция, скорее всего, ты решаешь неправильную задачу. В таком случае стоит обратиться за консультацией к криптографам.

В основном пакете crypto/mlkem также появились новые методы:

// DecapsulationKey768 теперь реализует crypto.Decapsulator
func (dk *DecapsulationKey768) Decapsulate(ciphertext []byte) ([]byte, error)

// И предоставляет доступ к EncapsulationKey как crypto.Encapsulator
func (dk *DecapsulationKey768) Encapsulator() crypto.Encapsulator

Аналогично для DecapsulationKey1024.

Эти интерфейсы позволяют использовать ML-KEM ключи в обобщённых криптографических API, например в crypto/hpke.

Изменения в существующих пакетах

Go 1.26 меняет стандартную библиотеку. Большая часть изменений связана с криптографией: команда Go унифицирует API и готовит экосистему к постквантовой эре. На практике это означает более безопасные значения по умолчанию и меньше возможностей допустить ошибку при работе с криптографией.

Главное изменение затрагивает все функции генерации ключей и подписи: параметр rand io.Reader теперь игнорируется. Вместо переданного источника случайности функции используют внутренний криптографически стойкий генератор. Это ломает код, полагающийся на детерминированную генерацию, но делает криптографию безопаснее по умолчанию. Разработчики больше не смогут случайно передать небезопасный генератор в продакшен.

Также появились новые интерфейсы crypto.Encapsulator и crypto.Decapsulator для абстракции над механизмами инкапсуляции ключей. Это часть подготовки к постквантовой криптографии, где KEM заменяет классический обмен ключами. Благодаря этому код, работающий с ключами, сможет прозрачно перейти на постквантовые алгоритмы без переписывания.

В пакете bytes добавлен метод Buffer.Peek(). Это давно ожидаемая функция для просмотра данных без их извлечения, которая избавляет от необходимости использовать bufio.Reader там, где нужен только предпросмотр содержимого буфера.

Пакет crypto/rsa получил функцию EncryptOAEPWithOptions() и предупреждения об устаревании PKCS#1 v1.5. Этот режим шифрования небезопасен и будет удалён в будущих версиях. Поэтому стоит уже сейчас переходить на OAEP для нового кода.

Наберись терпения, это самый большой блок... Дальше про каждое изменение отдельно.

bytes.Buffer: метод Peek

В Go 1.26 тип bytes.Buffer получил метод Peek(n int) []byte, который возвращает следующие n байт из буфера без их извлечения. Указатель чтения остаётся на месте. Это означает, что можно заглянуть в данные, не меняя состояние буфера.

Раньше для этого приходилось читать данные через Read() и затем возвращать их обратно через UnreadByte() или пересоздавать буфер. Другой вариант: использовать bufio.Reader, у которого Peek был изначально. Теперь bytes.Buffer сам справляется с этой задачей.

func (b *Buffer) Peek(n int) []byte

Метод возвращает срез, указывающий на внутренний массив буфера. Срез валиден до следующего вызова методов чтения или записи. На практике это означает, что нужно либо скопировать данные, либо использовать их немедленно.

package main

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("Hello, World!")

    // Смотрим первые 5 байт без извлечения
    peek := buf.Peek(5)
    fmt.Printf("Peek: %s\n", peek)       // Hello
    fmt.Printf("Len: %d\n", buf.Len())   // 13 — длина не изменилась

    // Теперь читаем
    data := make([]byte, 5)
    buf.Read(data)
    fmt.Printf("Read: %s\n", data)       // Hello
    fmt.Printf("Len: %d\n", buf.Len())   // 8 — извлекли 5 байт
}

Peek полезен при разборе бинарных протоколов, где нужно определить тип сообщения по заголовку, не извлекая его. Благодаря этому можно сначала проверить формат данных, а затем передать буфер соответствующему парсеру целиком:

func parseMessage(buf *bytes.Buffer) (Message, error) {
    // Смотрим первый байт — тип сообщения
    header := buf.Peek(1)
    if len(header) == 0 {
        return nil, io.EOF
    }

    switch header[0] {
    case 0x01:
        return parseTypeA(buf)
    case 0x02:
        return parseTypeB(buf)
    default:
        return nil, fmt.Errorf("unknown message type: %02x", header[0])
    }
}

Если в буфере меньше n байт, Peek вернёт всё, что есть. Поэтому нужно проверять длину возвращаемого среза:

peek := buf.Peek(100)
if len(peek) < 100 {
    // В буфере недостаточно данных
}

Возвращаемый срез становится невалидным после любой операции с буфером. В результате использование сохранённого среза после записи может привести к чтению мусора:

peek := buf.Peek(5)
buf.Write([]byte("more data")) // peek теперь может указывать на мусор

Давай сравним в bofio.Reader. У bufio.Reader тоже есть Peek, но с другой семантикой:

Аспект

bytes.Buffer.Peek

bufio.Reader.Peek

Ошибка при нехватке данных

Нет, возвращает что есть

Да, возвращает io.EOF

Может блокироваться

Нет

Да, при чтении из сети

Внутренний буфер

Общий с Buffer

Отдельный буфер чтения

crypto: интерфейсы Encapsulator и Decapsulator

Go 1.26 добавляет в пакет crypto два новых интерфейса: Encapsulator и Decapsulator. Они абстрагируют механизм инкапсуляции ключей (KEM, Key Encapsulation Mechanism), который становится основой постквантовой криптографии. Это позволяет писать код, который будет работать с любыми KEM-алгоритмами без изменений.

В классической криптографии обмен ключами работает через Diffie-Hellman: стороны обмениваются публичными частями и вычисляют общий секрет. В постквантовом мире это заменяется на KEM: одна сторона инкапсулирует случайный секрет в публичный ключ получателя, другая декапсулирует его приватным ключом. Такой подход устойчив к атакам квантовых компьютеров.

Интерфейсы Encapsulator и Decapsulator позволяют писать код, работающий с любым KEM: классическим ECDH, постквантовым ML-KEM или гибридным вариантом. В результате переход на новые алгоритмы требует только замены реализации, а не переписывания бизнес-логики.

package crypto

// Encapsulator — ключ, способный инкапсулировать секрет
type Encapsulator interface {
    // Encapsulate генерирует случайный shared secret
    // и инкапсулирует его в ciphertext
    Encapsulate() (ciphertext, sharedSecret []byte, err error)
}

// Decapsulator — ключ, способный декапсулировать секрет
type Decapsulator interface {
    // Decapsulate извлекает shared secret из ciphertext
    Decapsulate(ciphertext []byte) (sharedSecret []byte, err error)
}

Какие типы реализуют интерфейсы

После Go 1.26:

Тип

Encapsulator

Decapsulator

mlkem.EncapsulationKey768

Да

Нет

mlkem.EncapsulationKey1024

Да

Нет

mlkem.DecapsulationKey768

Да (через Encapsulator())

Да

mlkem.DecapsulationKey1024

Да (через Encapsulator())

Да

ECDH-ключи не реализуют эти интерфейсы напрямую, потому что ECDH это ключевой обмен, а не инкапсуляция. Однако пакет crypto/hpke оборачивает ECDH в KEM-интерфейс, что позволяет использовать его в обобщённом коде.

Пример: абстрактная работа с KEM

package main

import (
    "crypto"
    "crypto/mlkem"
    "fmt"
    "log"
)

// encryptWithKEM шифрует данные, используя любой KEM
func encryptWithKEM(encapsulator crypto.Encapsulator, plaintext []byte) (ciphertext, encapsulated []byte, err error) {
    // Инкапсулируем секрет
    enc, sharedSecret, err := encapsulator.Encapsulate()
    if err != nil {
        return nil, nil, err
    }

    // Используем shared secret для шифрования
    // (упрощённо — в реальности нужен AEAD)
    encrypted := xor(plaintext, sharedSecret[:len(plaintext)])

    return encrypted, enc, nil
}

// decryptWithKEM расшифровывает данные
func decryptWithKEM(decapsulator crypto.Decapsulator, ciphertext, encapsulated []byte) ([]byte, error) {
    sharedSecret, err := decapsulator.Decapsulate(encapsulated)
    if err != nil {
        return nil, err
    }

    return xor(ciphertext, sharedSecret[:len(ciphertext)]), nil
}

func xor(a, b []byte) []byte {
    result := make([]byte, len(a))
    for i := range a {
        result[i] = a[i] ^ b[i]
    }
    return result
}

func main() {
    // Генерируем ключевую пару ML-KEM-768
    dk, err := mlkem.GenerateKey768()
    if err != nil {
        log.Fatal(err)
    }

    // Получаем публичный ключ для инкапсуляции
    ek := dk.EncapsulationKey()

    message := []byte("Secret message")

    // Шифруем
    ct, enc, err := encryptWithKEM(ek, message)
    if err != nil {
        log.Fatal(err)
    }

    // Расшифровываем
    pt, err := decryptWithKEM(dk, ct, enc)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Original: %s\n", message)
    fmt.Printf("Decrypted: %s\n", pt)
}

Основное применение этих интерфейсов внутри crypto/tls. TLS 1.3 уже поддерживает постквантовые KEM, и интерфейсы позволяют подключать разные реализации без изменения кода TLS. Это означает, что по мере появления новых алгоритмов их можно будет добавлять без переписывания сетевого стека.

// Внутри crypto/tls (упрощённо)
func (hs *serverHandshakeState) doKeyExchange(encapsulator crypto.Encapsulator) error {
    ciphertext, sharedSecret, err := encapsulator.Encapsulate()
    if err != nil {
        return err
    }
    // Используем sharedSecret для деривации ключей сессии
    // Отправляем ciphertext клиенту
    ...
}

Пакет crypto/hpke использует эти интерфейсы внутри. Функции Seal и Open принимают ключи, реализующие соответствующие интерфейсы. Благодаря этому HPKE работает и с классическими ключами, и с постквантовыми.

crypto/dsa: игнорирование параметра random

В Go 1.26 функция dsa.GenerateKey игнорирует переданный параметр rand io.Reader. Вместо него используется внутренний криптографически стойкий генератор случайных чисел. Это делает код безопаснее, исключая возможность случайно передать небезопасный источник энтропии.

// До Go 1.26:
func GenerateKey(priv *PrivateKey, rand io.Reader) error

// После Go 1.26:
func GenerateKey(priv *PrivateKey, rand io.Reader) error

Сигнатура та же, но rand игнорируется. Функция всегда использует crypto/rand.Reader или эквивалентный безопасный источник.

Передача собственного источника случайности в криптографические функции стала частым источником уязвимостей. Разработчики передают небезопасные PRNG для ускорения тестов и забывают убрать это в продакшене. Неправильная инициализация детерминированного генератора приводит к предсказуемым ключам. В некоторых случаях передавался nil, и поведение было неопределённым. Новый подход решает эту проблему: криптографические функции сами заботятся о качестве случайности.

Для детерминированного тестирования используйте новую функцию testing/cryptotest.SetGlobalRandom:

package mypackage_test

import (
    "bytes"
    "crypto/dsa"
    "testing"
    "testing/cryptotest"
)

func TestDSAKeyGeneration(t *testing.T) {
    // Устанавливаем детерминированный источник
    deterministicRand := bytes.NewReader(make([]byte, 1024))
    cryptotest.SetGlobalRandom(t, deterministicRand)

    // Теперь генерация будет воспроизводимой
    var params dsa.Parameters
    dsa.GenerateParameters(&params, nil, dsa.L1024N160)

    var priv dsa.PrivateKey
    priv.Parameters = params
    dsa.GenerateKey(&priv, nil) // rand игнорируется, используется глобальный

    // Проверки...
}

SetGlobalRandom работает только в тестах и автоматически восстанавливает исходное состояние после завершения теста. Это означает, что тесты остаются изолированными друг от друга.

Если код критически зависит от передачи собственного rand, можно временно вернуть старое поведение через GODEBUG:

GODEBUG=cryptocustomrand=1 go run main.go

Это поведение будет удалено в будущих версиях Go. Используйте только как временное решение при миграции.

В crypto/dsa изменение затрагивает функции GenerateKey для генерации приватного ключа и GenerateParameters для генерации параметров домена. Обе функции теперь используют безопасный источник случайности независимо от переданного rand.

DSA считается устаревшим алгоритмом. NIST рекомендует переходить на ECDSA или EdDSA. Пакет crypto/dsa остаётся в стандартной библиотеке для совместимости с legacy-системами, но для новых проектов его использовать не стоит. В результате изменение параметра rand затрагивает в основном legacy-код, который и так требует миграции.

crypto/ecdh: KeyExchanger и игнорирование random

Go 1.26 вносит два изменения в пакет crypto/ecdh: добавляет интерфейс KeyExchanger и переводит генерацию ключей на внутренний источник случайности. Это унифицирует поведение с другими криптографическими пакетами и открывает возможность работы с аппаратными ключами.

Метод Curve.GenerateKey теперь игнорирует переданный rand io.Reader:

// До Go 1.26: rand использовался для генерации
key, err := ecdh.X25519().GenerateKey(rand.Reader)

// После Go 1.26: rand игнорируется
key, err := ecdh.X25519().GenerateKey(nil) // Работает так же

Функция всегда использует криптографически стойкий генератор. Это унифицирует поведение с другими криптографическими пакетами и предотвращает случайное использование небезопасных источников.

Для детерминированного тестирования используйте testing/cryptotest.SetGlobalRandom. Для временного восстановления старого поведения подойдёт переменная окружения GODEBUG=cryptocustomrand=1.

Новый интерфейс позволяет абстрагироваться от конкретной реализации ECDH. Благодаря этому можно подменять пр��граммные ключи аппаратными без изменения бизнес-логики:

package ecdh

// KeyExchanger представляет приватный ключ,
// способный выполнять обмен ключами
type KeyExchanger interface {
    // ECDH выполняет обмен ключами Диффи-Хеллмана
    ECDH(remote *PublicKey) ([]byte, error)

    // PublicKey возвращает соответствующий публичный ключ
    PublicKey() *PublicKey

    // Curve возвращает кривую ключа
    Curve() Curve
}

Тип *PrivateKey реализует этот интерфейс.

Интерфейс позволяет подключать аппаратные ключи (HSM, смарт-карты, TPM) без изменения бизнес-логики. На практике это означает, что код остаётся тем же, а меняется только источник ключей:

package main

import (
    "crypto/ecdh"
    "fmt"
)

// performKeyExchange работает с любым KeyExchanger
func performKeyExchange(local ecdh.KeyExchanger, remotePub *ecdh.PublicKey) ([]byte, error) {
    return local.ECDH(remotePub)
}

func main() {
    // Программный ключ
    softwareKey, _ := ecdh.P256().GenerateKey(nil)

    // Другая сторона
    peerKey, _ := ecdh.P256().GenerateKey(nil)

    // Обмен ключами
    sharedSecret, err := performKeyExchange(softwareKey, peerKey.PublicKey())
    if err != nil {
        panic(err)
    }

    fmt.Printf("Shared secret: %x\n", sharedSecret)
}

Теперь performKeyExchange можно использовать и с программными ключами, и с аппаратными. Достаточно реализовать KeyExchanger для HSM.

package hsm

import (
    "crypto/ecdh"
)

// HSMPrivateKey — ключ, хранящийся в аппаратном модуле
type HSMPrivateKey struct {
    handle    uint32
    publicKey *ecdh.PublicKey
    curve     ecdh.Curve
    client    *HSMClient
}

func (k *HSMPrivateKey) ECDH(remote *ecdh.PublicKey) ([]byte, error) {
    // Отправляем запрос в HSM
    return k.client.DeriveKey(k.handle, remote.Bytes())
}

func (k *HSMPrivateKey) PublicKey() *ecdh.PublicKey {
    return k.publicKey
}

func (k *HSMPrivateKey) Curve() ecdh.Curve {
    return k.curve
}

// HSMPrivateKey реализует ecdh.KeyExchanger
var _ ecdh.KeyExchanger = (*HSMPrivateKey)(nil)

Интерфейс KeyExchanger позволяет crypto/tls работать с аппаратными ключами для ECDHE-обмена. Это особенно важно для production-систем, где приватные ключи не должны покидать HSM:

// В конфигурации TLS сервера
config := &tls.Config{
    GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
        // Возвращаем сертификат с ключом из HSM
        return &tls.Certificate{
            Certificate: [][]byte{certDER},
            PrivateKey:  hsmKey, // реализует KeyExchanger
        }, nil
    },
}

crypto/ecdsa: устаревание big.Int и игнорирование random

Go 1.26 вносит два изменения в crypto/ecdsa: объявляет устаревшими поля big.Int в ключах и переводит все функции на внутренний источник случайности. Это делает API безопаснее и проще в использовании.

Поля X, Y, D типа *big.Int в структурах PublicKey и PrivateKey помечены как deprecated:

type PublicKey struct {
    elliptic.Curve
    X, Y *big.Int // Deprecated: используйте ECDH() для обмена ключами
}

type PrivateKey struct {
    PublicKey
    D *big.Int // Deprecated: используйте ECDH() для обмена ключами
}

Раньше эти поля использовались для сериализации ключей вручную, математических операций над ключами и интеграции с legacy-кодом. Теперь для обмена ключами рекомендуется использовать метод ECDH(), возвращающий ключ типа *ecdh.PrivateKey. Это даёт более безопасный и стандартизированный API.

// До Go 1.26:
// Сериализация через big.Int
xBytes := privKey.X.Bytes()
yBytes := privKey.Y.Bytes()
dBytes := privKey.D.Bytes()

// После Go 1.26:
// Через ECDH ключ
ecdhKey, err := privKey.ECDH()
if err != nil {
    log.Fatal(err)
}
// Приватный ключ в стандартном формате
privBytes := ecdhKey.Bytes()
// Публичный ключ
pubBytes := ecdhKey.PublicKey().Bytes()

Все функции, принимающие rand io.Reader, теперь игнорируют этот параметр:

Функция

Изменение

GenerateKey(curve, rand)

rand игнорируется

Sign(rand, priv, hash)

rand игнорируется

SignASN1(rand, priv, hash)

rand игнорируется

PrivateKey.Sign(rand, digest, opts)

rand игнорируется

Функции используют внутренний криптографически стойкий генератор. Это означает, что можно безопасно передавать nil вместо rand.Reader.

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/sha256"
    "fmt"
    "log"
)

func main() {
    // Генерация ключа — rand теперь можно передать nil
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), nil)
    if err != nil {
        log.Fatal(err)
    }

    // Хеш сообщения
    message := []byte("Hello, Go 1.26!")
    hash := sha256.Sum256(message)

    // Подпись — rand тоже можно передать nil
    signature, err := ecdsa.SignASN1(nil, privateKey, hash[:])
    if err != nil {
        log.Fatal(err)
    }

    // Проверка подписи
    valid := ecdsa.VerifyASN1(&privateKey.PublicKey, hash[:], signature)
    fmt.Printf("Signature valid: %v\n", valid)

    // Конвертация в ECDH для обмена ключами
    ecdhKey, err := privateKey.ECDH()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ECDH public key: %x\n", ecdhKey.PublicKey().Bytes())
}

Для воспроизводимых тестов используй testing/cryptotest.SetGlobalRandom:

func TestECDSA(t *testing.T) {
    // Фиксированный seed
    seed := make([]byte, 32)
    for i := range seed {
        seed[i] = byte(i)
    }

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    // Генерация будет детерминированной
    key1, _ := ecdsa.GenerateKey(elliptic.P256(), nil)

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    key2, _ := ecdsa.GenerateKey(elliptic.P256(), nil)

    // key1 и key2 идентичны
    if !key1.Equal(key2) {
        t.Error("keys should be equal")
    }
}

Для совместимости с legacy-кодом можно временно вернуть старое поведение:

GODEBUG=cryptocustomrand=1 go run main.go

Это поведение будет удалено в будущих версиях.

Работа с big.Int напрямую позволяла легко допустить ошибки при сериализации: неправильная длина, ведущие нули. Она не учитывала особенности разных кривых и не была совместима с аппаратными ключами. Новый API через ECDH() унифицирует работу с ключами и позволяет использовать аппаратные модули.

  • crypto/ecdsa — документация пакета

  • crypto/ecdh — рекомендуемый пакет для обмена ключами

crypto/ed25519: игнорирование параметра random

В Go 1.26 функция ed25519.GenerateKey изменила обработку параметра rand: если передан nil, используется внутренний криптографически стойкий генератор вместо crypto/rand.Reader. Это унифицирует поведение с другими криптографическими пакетами.

// До Go 1.26:
// nil означал использование crypto/rand.Reader
pub, priv, err := ed25519.GenerateKey(nil)
// Явная передача источника
pub, priv, err := ed25519.GenerateKey(myRandReader)

// После Go 1.26:
// nil или любое значение — используется внутренний генератор
pub, priv, err := ed25519.GenerateKey(nil)
// Переданный источник игнорируется
pub, priv, err := ed25519.GenerateKey(myRandReader) // myRandReader не используется

В отличие от других криптографических пакетов, Ed25519 не использует случайность при подписи. Алгоритм детерминированный: одно и то же сообщение с одним и тем же ключом всегда даёт одинаковую подпись. Случайность нужна только при генерации ключа. Поэтому изменение затрагивает только GenerateKey:

package main

import (
    "crypto/ed25519"
    "fmt"
    "log"
)

func main() {
    // Генерация ключа — можно передать nil
    publicKey, privateKey, err := ed25519.GenerateKey(nil)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Public key: %x\n", publicKey)

    // Подпись — детерминированная, rand не нужен
    message := []byte("Hello, Ed25519!")
    signature := ed25519.Sign(privateKey, message)

    fmt.Printf("Signature: %x\n", signature)

    // Проверка
    valid := ed25519.Verify(publicKey, message, signature)
    fmt.Printf("Valid: %v\n", valid)
}

Для воспроизводимой генерации ключей в тестах:

func TestEd25519(t *testing.T) {
    seed := make([]byte, 32)
    for i := range seed {
        seed[i] = byte(i)
    }

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    pub1, priv1, _ := ed25519.GenerateKey(nil)

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    pub2, priv2, _ := ed25519.GenerateKey(nil)

    // Ключи идентичны
    if !bytes.Equal(pub1, pub2) || !bytes.Equal(priv1, priv2) {
        t.Error("keys should be equal")
    }
}

Если нужен детерминированный ключ из известного seed, используйте NewKeyFromSeed:

// 32 байта seed
seed := make([]byte, ed25519.SeedSize)
copy(seed, []byte("my deterministic seed value!!!!!"))

privateKey := ed25519.NewKeyFromSeed(seed)
publicKey := privateKey.Public().(ed25519.PublicKey)

Этот метод не зависит от глобального генератора и работает одинаково во всех версиях Go. Поэтому для случаев, когда нужна воспроизводимость вне тестов, лучше использовать именно его.

Для восстановления старого поведения:

GODEBUG=cryptocustomrand=1 go run main.go

crypto/fips140: WithoutEnforcement и Enforced

Go 1.26 добавляет в пакет crypto/fips140 две функции для управления режимом FIPS 140-3: WithoutEnforcement и Enforced. Также появилась функция Version для определения версии криптографического модуля. Это даёт больше контроля над поведением криптографии в production-системах.

FIPS 140-3 представляет собой федеральный стандарт США для криптографических модулей. В режиме GODEBUG=fips140=only Go разрешает использовать только сертифицированные алгоритмы. Попытка вызвать несертифицированную функцию (например, MD5) приводит к панике. Новые функции позволяют временно отключать строгие проверки в контролируемых сценариях, что важно при работе с legacy-данными.

func WithoutEnforcement(f func()) error

Выполняет функцию f с временно отключённой проверкой FIPS. Возвращает ошибку, если f паникует.

Пример: чтение legacy-файла с MD5-хешем:

package main

import (
    "crypto/fips140"
    "crypto/md5"
    "fmt"
    "log"
)

func main() {
    // В режиме GODEBUG=fips140=only это паникует:
    // hash := md5.Sum(data)

    var hash [16]byte

    err := fips140.WithoutEnforcement(func() {
        // Внутри блока можно использовать не-FIPS алгоритмы
        data := []byte("legacy data")
        hash = md5.Sum(data)
    })

    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("MD5: %x\n", hash)
}
func Enforced() bool

Возвращает true, если FIPS-проверки активны. Полезно для условной логики, когда нужно выбрать алгоритм в зависимости от режима:

func hashData(data []byte) []byte {
    if fips140.Enforced() {
        // Используем SHA-256 в FIPS-режиме
        h := sha256.Sum256(data)
        return h[:]
    }
    // В обычном режиме можно использовать более быстрый хеш
    h := xxhash.Sum64(data)
    buf := make([]byte, 8)
    binary.BigEndian.PutUint64(buf, h)
    return buf
}
func Version() string

Возвращает версию криптографического модуля FIPS 140-3, если программа собрана с замороженным модулем (переменная окружения GOFIPS140):

package main

import (
    "crypto/fips140"
    "fmt"
)

func main() {
    version := fips140.Version()
    if version != "" {
        fmt.Printf("FIPS 140-3 module version: %s\n", version)
    } else {
        fmt.Println("Not running with frozen FIPS module")
    }
}

WithoutEnforcement не отключает FIPS полностью. Она только подавляет панику при вызове несертифицированных функций. Аудит-логи всё равно могут фиксировать использование не-FIPS алгоритмов.

Используй эту функцию только когда обрабатываете legacy-данные с гарантией, что новые данные используют FIPS-алгоритмы. Также она подходит для тестирования кода в FIPS-режиме и для выполнения административных задач, не связанных с защитой данных.

Для использования сертифицированного модуля:

GOFIPS140=v1.0.0 go build -o myapp main.go

После этого Version() вернёт "v1.0.0", и криптографические функции будут использовать замороженную реализацию.

crypto/mlkem: метод Encapsulator

В Go 1.26 типы DecapsulationKey768 и DecapsulationKey1024 получили метод Encapsulator(), возвращающий соответствующий публичный ключ. Это позволяет приватным ключам реализовывать интерфейс crypto.Decapsulator. В результате код, работающий с KEM, становится более обобщённым.

// DecapsulationKey768
func (dk *DecapsulationKey768) Encapsulator() *EncapsulationKey768

// DecapsulationKey1024
func (dk *DecapsulationKey1024) Encapsulator() *EncapsulationKey1024

Метод возвращает публичный ключ (ключ инкапсуляции), соответствующий приватному ключу (ключу декапсуляции).

До Go 1.26 для получения публичного ключа нужно было использовать метод EncapsulationKey():

dk, _ := mlkem.GenerateKey768()
ek := dk.EncapsulationKey() // Возвращает *EncapsulationKey768

Новый метод Encapsulator() делает то же самое, но с именем, соответствующим интерфейсу crypto.Decapsulator. Это позволяет использовать ML-KEM ключи в обобщённом коде:

type Decapsulator interface {
    Decapsulate(ciphertext []byte) (sharedSecret []byte, err error)
    Encapsulator() Encapsulator
}
package main

import (
    "crypto/mlkem"
    "fmt"
    "log"
)

func main() {
    // Генерируем ключевую пару ML-KEM-768
    dk, err := mlkem.GenerateKey768()
    if err != nil {
        log.Fatal(err)
    }

    // Получаем публичный ключ через новый метод
    ek := dk.Encapsulator()

    // Инкапсулируем секрет
    ciphertext, sharedSecret1, err := ek.Encapsulate()
    if err != nil {
        log.Fatal(err)
    }

    // Декапсулируем
    sharedSecret2, err := dk.Decapsulate(ciphertext)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Shared secrets match: %v\n",
        string(sharedSecret1) == string(sharedSecret2))
    fmt.Printf("Shared secret length: %d bytes\n", len(sharedSecret1))
}

Теперь можно писать функции, работающие с любым KEM. Благодаря этому переход на другие KEM-алгоритмы требует минимальных изменений:

package main

import (
    "crypto"
    "crypto/mlkem"
    "fmt"
)

// performKEM выполняет инкапсуляцию/декапсуляцию
// с любым типом, реализующим crypto.Decapsulator
func performKEM(dk crypto.Decapsulator) ([]byte, error) {
    // Получаем публичный ключ через интерфейс
    ek := dk.(interface{ Encapsulator() crypto.Encapsulator }).Encapsulator()

    // Инкапсулируем
    ct, ss1, err := ek.Encapsulate()
    if err != nil {
        return nil, err
    }

    // Декапсулируем для проверки
    ss2, err := dk.Decapsulate(ct)
    if err != nil {
        return nil, err
    }

    if string(ss1) != string(ss2) {
        return nil, fmt.Errorf("shared secrets don't match")
    }

    return ss1, nil
}

func main() {
    dk768, _ := mlkem.GenerateKey768()
    dk1024, _ := mlkem.GenerateKey1024()

    ss768, _ := performKEM(dk768)
    ss1024, _ := performKEM(dk1024)

    fmt.Printf("ML-KEM-768 shared secret: %d bytes\n", len(ss768))
    fmt.Printf("ML-KEM-1024 shared secret: %d bytes\n", len(ss1024))
}

Метод

Возвращает

Назначение

EncapsulationKey()

*EncapsulationKeyXXX

Прямой доступ к публичному ключу

Encapsulator()

*EncapsulationKeyXXX

Реализация интерфейса Decapsulator

Bytes()

[]byte

Сериализация приватного ключа

EncapsulationKey() и Encapsulator() возвращают один и тот же объект. Выбор между ними зависит от контекста: Encapsulator() лучше подходит для обобщённого кода, EncapsulationKey() для явной работы с ML-KEM.

Пакет crypto/hpke использует эти интерфейсы внутри для поддержки ML-KEM:

// В crypto/hpke (упрощённо)
func NewMLKEMPrivateKey(dk *mlkem.DecapsulationKey768) (*PrivateKey, error) {
    return &PrivateKey{
        decapsulator: dk,
        encapsulator: dk.Encapsulator(), // Используется новый метод
    }, nil
}

crypto/rand: игнорирование параметра random в Prime

В Go 1.26 функция rand.Prime игнорирует переданный параметр rand io.Reader и использует внутренний криптографически стойкий генератор случайных чисел. Это делает генерацию простых чисел безопаснее по умолчанию.

Сигнатура функции осталась прежней:

func Prime(rand io.Reader, bits int) (*big.Int, error)

Однако параметр rand теперь игнорируется. Функция всегда использует безопасный источник случайности.

package main

import (
    "crypto/rand"
    "fmt"
    "log"
)

func main() {
    // Можно передать nil — результат тот же
    prime, err := rand.Prime(nil, 256)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("256-bit prime: %s\n", prime.String())
    fmt.Printf("Bit length: %d\n", prime.BitLen())
}

Функция Prime используется при генерации RSA-ключей. До Go 1.26 можно было передать предсказуемый источник случайности:

// Опасный код до Go 1.26
weakRand := mathrand.New(mathrand.NewSource(42))
prime, _ := rand.Prime(weakRand, 2048) // Предсказуемое простое число!

Теперь такой код безопасен: weakRand игнорируется, используется криптографически стойкий генератор. Это особенно важно, потому что предсказуемые простые числа полностью компрометируют RSA-ключи.

Функция rsa.GenerateKey внутри вызывает rand.Prime. Изменение распространяется на всю цепочку:

// rsa.GenerateKey тоже игнорирует rand
key, err := rsa.GenerateKey(nil, 4096) // Безопасно

Для тестов, требующих воспроизводимости:

func TestPrime(t *testing.T) {
    seed := make([]byte, 32)
    copy(seed, "fixed seed for testing!!!!!!!!")

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    prime1, _ := rand.Prime(nil, 128)

    cryptotest.SetGlobalRandom(t, bytes.NewReader(seed))

    prime2, _ := rand.Prime(nil, 128)

    if prime1.Cmp(prime2) != 0 {
        t.Error("primes should be equal")
    }
}

Игнорирование переданного rand не влияет на производительность. Внутренний генератор оптимизирован для криптографических операций.

func BenchmarkPrime(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Prime(nil, 256)
    }
}

Для legacy-кода:

GODEBUG=cryptocustomrand=1 go run main.go

Это позволит использовать переданный rand, но поведение будет удалено в будущих версиях.

В пакете crypto/rand изменение затрагивает только Prime. Функция Read по-прежнему читает из криптографически безопасного источника и не принимает параметр rand:

// Read всегда использует безопасный источник
buf := make([]byte, 32)
rand.Read(buf)

crypto/rsa: EncryptOAEPWithOptions и устаревание PKCS#1 v1.5

Go 1.26 добавляет функцию EncryptOAEPWithOptions для гибкой настройки OAEP-шифрования и объявляет функции PKCS#1 v1.5 шифрования устаревшими. Также все функции генерации ключей и шифрования теперь игнорируют параметр rand. Это часть общего движения к более безопасным значениям по умолчанию.

func EncryptOAEPWithOptions(pub *PublicKey, plaintext []byte, opts *OAEPOptions) ([]byte, error)

type OAEPOptions struct {
    Hash    crypto.Hash // Хеш для OAEP padding
    MGFHash crypto.Hash // Хеш для MGF1 (если отличается от Hash)
    Label   []byte      // Опциональная метка
}

Раньше EncryptOAEP использовала один хеш и для padding, и для MGF1 (Mask Generation Function). Новая функция позволяет указать разные хеши, что требуется некоторыми стандартами:

package main

import (
    "crypto"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/sha512"
    "fmt"
    "log"
)

func main() {
    // Генерируем ключ
    privateKey, err := rsa.GenerateKey(nil, 2048)
    if err != nil {
        log.Fatal(err)
    }

    message := []byte("Hello, OAEP with options!")

    // Шифруем с разными хешами для OAEP и MGF1
    opts := &rsa.OAEPOptions{
        Hash:    crypto.SHA256, // OAEP padding
        MGFHash: crypto.SHA512, // MGF1 mask generation
        Label:   []byte("my-label"),
    }

    ciphertext, err := rsa.EncryptOAEPWithOptions(&privateKey.PublicKey, message, opts)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Ciphertext length: %d bytes\n", len(ciphertext))

    // Расшифровываем (нужно указать те же параметры)
    plaintext, err := rsa.DecryptOAEP(
        sha256.New(),
        nil,
        privateKey,
        ciphertext,
        opts.Label,
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Decrypted: %s\n", plaintext)
}

Функции шифрования PKCS#1 v1.5 помечены как deprecated:

// Deprecated: небезопасно, используйте OAEP
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error)
func DecryptPKCS1v15(rand io.Reader, priv *PrivateKey, ciphertext []byte) ([]byte, error)
func DecryptPKCS1v15SessionKey(rand io.Reader, priv *PrivateKey, ciphertext []byte, key []byte) error

PKCS#1 v1.5 уязвим к атакам Bleichenbacher (padding oracle). OAEP защищён от этих атак и должен использоваться для нового кода. Поэтому рекомендуется мигрировать существующий код как можно скорее.

// До Go 1.26:
// Небезопасно
ciphertext, err := rsa.EncryptPKCS1v15(rand.Reader, &publicKey, message)
// После Go 1.26:
// Безопасно
ciphertext, err := rsa.EncryptOAEP(sha256.New(), nil, &publicKey, message, nil)

// Или с опциями
opts := &rsa.OAEPOptions{Hash: crypto.SHA256}
ciphertext, err := rsa.EncryptOAEPWithOptions(&publicKey, message, opts)

Все функции, принимающие rand io.Reader, теперь игнорируют этот параметр:

Функция

Изменение

GenerateKey(rand, bits)

rand игнорируется

GenerateMultiPrimeKey(rand, nprimes, bits)

rand игнорируется

EncryptPKCS1v15(rand, pub, msg)

rand игнорируется

EncryptOAEP(hash, rand, pub, msg, label)

rand игнорируется

Метод PrivateKey.Validate() стал строже. Теперь он проверяет консистентность D с предвычисленными значениями и возвращает ошибку, если поля были изменены после вызова Precompute():

key, _ := rsa.GenerateKey(nil, 2048)
key.Precompute()

// Модификация после Precompute — ошибка
key.D.SetInt64(12345)
err := key.Validate() // Возвращает ошибку

Пример: современное RSA-шифрование

package main

import (
    "crypto"
    "crypto/rsa"
    "fmt"
    "log"
)

func main() {
    // Генерация ключа (rand игнорируется, можно передать nil)
    privateKey, err := rsa.GenerateKey(nil, 4096)
    if err != nil {
        log.Fatal(err)
    }

    // Предвычисление для ускорения операций
    privateKey.Precompute()

    message := []byte("Секретное сообщение")

    // Шифрование с OAEP и опциями
    opts := &rsa.OAEPOptions{
        Hash:    crypto.SHA256,
        MGFHash: crypto.SHA256,
    }

    ciphertext, err := rsa.EncryptOAEPWithOptions(&privateKey.PublicKey, message, opts)
    if err != nil {
        log.Fatal(err)
    }

    // Расшифровка
    decOpts := &rsa.OAEPOptions{
        Hash:    crypto.SHA256,
        MGFHash: crypto.SHA256,
    }

    plaintext, err := privateKey.Decrypt(nil, ciphertext, decOpts)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Original: %s\n", message)
    fmt.Printf("Decrypted: %s\n", plaintext)
}

crypto/subtle — WithDataIndependentTiming

Go 1.26 меняет поведение функции WithDataIndependentTiming в пакете crypto/subtle. Изменения затрагивают многопоточность и интеграцию с cgo.

Раньше WithDataIndependentTiming привязывала горутину к потоку ОС на время выполнения переданной функции. Теперь привязки нет, и горутина работает как обычно. Это изменение повышает производительность многопоточного кода, потому что планировщик Go может свободно распределять горутины между потоками.

Второе изменение касается наследования режима. Горутины, порождённые внутри WithDataIndependentTiming, и их потомки теперь наследуют режим data-independent timing на всё время жизни. Раньше наследования не было, и приходилось оборачивать каждую горутину вручную.

Криптографические операции должны выполняться за постоянное время независимо от входных данных. Иначе злоумышленник может измерить время выполнения и восстановить секретные ключи. Такой тип атаки называется timing attack, и он особенно опасен при сравнении токенов или HMAC-подписей.

Функция WithDataIndependentTiming включает режим процессора, при котором определённые инструкции выполняются за фиксированное время. На x86 это режим DOITM (Data Operand Independent Timing Mode), на ARM используется DIT. Благодаря этому даже низкоуровневые операции не дают злоумышленнику информации о секретных данных.

package main

import (
    "crypto/subtle"
    "fmt"
)

func main() {
    secret := []byte("my-secret-key-32-bytes-long!!!!!")
    userInput := []byte("my-secret-key-32-bytes-long!!!!!")

    var match int

    subtle.WithDataIndependentTiming(func() {
        // Сравнение выполняется за постоянное время
        match = subtle.ConstantTimeCompare(secret, userInput)
    })

    if match == 1 {
        fmt.Println("Keys match")
    } else {
        fmt.Println("Keys don't match")
    }
}

В Go 1.26 порождённые горутины автоматически работают в защищённом режиме:

package main

import (
    "crypto/subtle"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    subtle.WithDataIndependentTiming(func() {
        // Запускаем несколько горутин для параллельной проверки
        for i := 0; i < 4; i++ {
            wg.Add(1)
            go func(id int) {
                defer wg.Done()
                // Эта горутина наследует data-independent timing
                // на всё время жизни, даже после возврата из
                // WithDataIndependentTiming
                processSecretData(id)
            }(i)
        }
        wg.Wait()
    })
}

func processSecretData(id int) {
    // Криптографические операции защищены от timing attacks
}

Для кода на C, вызываемого через cgo, действуют определённые правила. C-код, вызванный из WithDataIndependentTiming, работает в защищённом режиме. Если C-код отключает data-independent timing, Go включит его обратно при возврате в Go-код. При этом если C-код из другого места включает или отключает режим, это состояние сохраняется при последующем вызове Go-кода. Такое поведение обеспечивает предсказуемую защиту при смешанном использовании Go и C.

Пример с cgo:

package main

/*
#include <stdint.h>

// Функция сравнения, которая должна выполняться за постоянное время
int constant_time_compare(const uint8_t *a, const uint8_t *b, size_t len) {
    uint8_t result = 0;
    for (size_t i = 0; i < len; i++) {
        result |= a[i] ^ b[i];
    }
    return result == 0;
}
*/
import "C"

import (
    "crypto/subtle"
    "unsafe"
)

func compareKeys(a, b []byte) bool {
    if len(a) != len(b) {
        return false
    }

    var result C.int

    subtle.WithDataIndependentTiming(func() {
        // C-код выполняется в режиме data-independent timing
        result = C.constant_time_compare(
            (*C.uint8_t)(unsafe.Pointer(&a[0])),
            (*C.uint8_t)(unsafe.Pointer(&b[0])),
            C.size_t(len(a)),
        )
    })

    return result != 0
}

WithDataIndependentTiming нужен для сравнения секретных значений (ключей, токенов, HMAC) и криптографических операций, где время выполнения зависит от значения секрета.

Не нужен для хеширования публичных данных, асимметричной криптографии (там другие защиты) и генерации случайных чисел.

Режим data-independent timing работает на x86/amd64 с поддержкой DOITM (Intel Ice Lake и новее, AMD Zen 3 и новее), а также на ARM64 с поддержкой DIT (ARMv8.4 и новее). На неподдерживаемых платформах функция выполняет переданный код без дополнительных действий. Это означает, что код можно писать универсально, не проверяя наличие аппаратной поддержки.

crypto/tls — постквантовый обмен ключами по умолчанию

Go 1.26 включает постквантовые механизмы обмена ключами в TLS по умолчанию. Это подготовка к эре квантовых компьютеров, способных взломать классическую криптографию.

По умолчанию TLS-соединения теперь используют гибридные механизмы обмена ключами. Первый механизм SecP256r1MLKEM768 комбинирует ECDH на кривой P-256 и ML-KEM-768. Второй механизм SecP384r1MLKEM1024 использует более сильные параметры: ECDH на кривой P-384 и ML-KEM-1024. ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism) представляет собой постквантовый алгоритм, устойчивый к атакам квантовых компьютеров. Раньше он назывался Kyber.

Квантовые компьютеры смогут взломать RSA и ECDH за полиномиальное время с помощью алгоритма Шора. Пока таких компьютеров нет, но злоумышленники уже сейчас записывают зашифрованный трафик, чтобы расшифровать его позже. Эта стратегия называется "harvest now, decrypt later" и представляет реальную угрозу для данных с длительным сроком конфиденциальности.

Гибридный подход защищает от обоих сценариев. Если ML-KEM окажется уязвим, останется защита классического ECDH. Если появится квантовый компьютер, ML-KEM обеспечит безопасность. Благодаря этому переход на постквантовую криптографию происходит без риска потерять защиту.

Никакого кода менять не надо. Обычный TLS-сервер и клиент автоматически используют постквантовую криптографию:

package main

import (
    "crypto/tls"
    "log"
    "net/http"
)

func main() {
    // Сервер автоматически поддерживает постквантовый обмен ключами
    server := &http.Server{
        Addr: ":8443",
        TLSConfig: &tls.Config{
            // По умолчанию CurvePreferences включает SecP256r1MLKEM768
        },
    }

    log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}

Клиент тоже работает без изменений:

package main

import (
    "crypto/tls"
    "fmt"
    "net/http"
)

func main() {
    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                // Постквантовая криптография включена по умолчанию
            },
        },
    }

    resp, err := client.Get("https://example.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Проверяем, какой механизм обмена ключами использовался
    fmt.Printf("Curve: %v\n", resp.TLS.CurveID)
}

Если постквантовая криптография вызывает проблемы совместимости, её можно отключить двумя способами.

Через конфигурацию TLS:

package main

import (
    "crypto/tls"
    "net/http"
)

func main() {
    server := &http.Server{
        Addr: ":8443",
        TLSConfig: &tls.Config{
            // Только классические кривые
            CurvePreferences: []tls.CurveID{
                tls.X25519,
                tls.CurveP256,
                tls.CurveP384,
            },
        },
    }

    server.ListenAndServeTLS("cert.pem", "key.pem")
}

Через переменную окружения (второй способ полезен для тестирования без изменения кода):

GODEBUG=tlssecpmlkem=0 ./myapp

Убедиться, что постквантовый обмен ключами работает, можно через ConnectionState:

package main

import (
    "crypto/tls"
    "fmt"
    "log"
)

func main() {
    conn, err := tls.Dial("tcp", "cloudflare.com:443", nil)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    state := conn.ConnectionState()

    switch state.CurveID {
    case tls.SecP256r1MLKEM768:
        fmt.Println("Using P-256 + ML-KEM-768 (post-quantum)")
    case tls.SecP384r1MLKEM1024:
        fmt.Println("Using P-384 + ML-KEM-1024 (post-quantum)")
    case tls.X25519:
        fmt.Println("Using X25519 (classical)")
    default:
        fmt.Printf("Using curve: %v\n", state.CurveID)
    }
}

Постквантовые механизмы требуют поддержки на обеих сторонах. Если сервер не поддерживает ML-KEM, клиент автоматически откатится на классический ECDH.

Cloudflare поддерживает ML-KEM с 2023 года, Google добавил поддержку в 2024 году. Amazon активно внедряет постквантовую криптографию в свои сервисы. Это означает, что большинство крупных интернет-сервисов уже готовы к работе с новым протоколом.

Гибридный обмен ключами увеличивает объём данных в handshake. ML-KEM-768 добавляет около 2 КБ к первым пакетам. Для большинства приложений это незаметно, но может влиять на латентность в мобильных сетях с высокой задержкой.

  • RFC 9180 — Hybrid Public Key Encryption

  • ML-KEM — стандарт NIST FIPS 203

  • crypto/tls — документация пакета

crypto/tls — поля HelloRetryRequest

Go 1.26 добавляет новые поля в структуры ClientHelloInfo и ConnectionState для отслеживания HelloRetryRequest в TLS 1.3.

В TLS 1.3 сервер может попросить клиента переотправить ClientHello с другими параметрами. Это происходит, когда клиент предложил неподдерживаемые криптографические группы, когда сервер хочет использовать другой механизм обмена ключами, или когда нужна дополнительная проверка клиента. После получения HelloRetryRequest клиент отправляет второй ClientHello с исправленными параметрами. Понимание этого механизма важно для оптимизации производительности, потому что каждый HelloRetryRequest добавляет дополнительный round-trip.

В tls.ClientHelloInfo:

type ClientHelloInfo struct {
    // ... существующие поля ...

    // HelloRetryRequest указывает, что это второй ClientHello,
    // отправленный в ответ на HelloRetryRequest от сервера
    HelloRetryRequest bool
}

В tls.ConnectionState:

type ConnectionState struct {
    // ... существующие поля ...

    // HelloRetryRequest указывает, был ли отправлен HelloRetryRequest
    // Для сервера: отправлял ли он HRR
    // Для клиента: получал ли он HRR
    HelloRetryRequest bool
}

Сервер может определить, обрабатывает ли он повторный ClientHello:

package main

import (
    "crypto/tls"
    "log"
    "net"
)

func main() {
    cert, _ := tls.LoadX509KeyPair("cert.pem", "key.pem")

    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
        GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
            if info.HelloRetryRequest {
                log.Printf("Processing second ClientHello from %s", info.Conn.RemoteAddr())
                // Можно применить другую логику для повторных запросов
            } else {
                log.Printf("Processing initial ClientHello from %s", info.Conn.RemoteAddr())
            }
            return nil, nil // используем основной конфиг
        },
    }

    listener, _ := tls.Listen("tcp", ":8443", config)
    defer listener.Close()

    for {
        conn, _ := listener.Accept()
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()

    tlsConn := conn.(*tls.Conn)
    if err := tlsConn.Handshake(); err != nil {
        log.Printf("Handshake error: %v", err)
        return
    }

    state := tlsConn.ConnectionState()
    if state.HelloRetryRequest {
        log.Println("Connection established after HelloRetryRequest")
    }

    // ... обработка соединения ...
}

Клиент может проверить, был ли HelloRetryRequest во время handshake:

package main

import (
    "crypto/tls"
    "fmt"
    "log"
)

func main() {
    conn, err := tls.Dial("tcp", "example.com:443", &tls.Config{
        // Намеренно используем только одну кривую,
        // чтобы сервер мог запросить другую через HRR
        CurvePreferences: []tls.CurveID{tls.CurveP384},
    })
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    state := conn.ConnectionState()

    if state.HelloRetryRequest {
        fmt.Println("Server requested HelloRetryRequest")
        fmt.Println("Connection required an extra round-trip")
    } else {
        fmt.Println("Connection established without HelloRetryRequest")
    }

    fmt.Printf("Negotiated curve: %v\n", state.CurveID)
}

HelloRetryRequest добавляет дополнительный round-trip к handshake. Мониторинг этого события помогает оптимизировать конфигурацию:

package main

import (
    "crypto/tls"
    "net/http"
    "sync/atomic"
)

var (
    totalConnections int64
    hrrConnections   int64
)

func main() {
    server := &http.Server{
        Addr: ":8443",
        TLSConfig: &tls.Config{
            GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
                if !info.HelloRetryRequest {
                    atomic.AddInt64(&totalConnections, 1)
                }
                return nil, nil
            },
        },
        ConnState: func(conn net.Conn, state http.ConnState) {
            if state == http.StateActive {
                if tlsConn, ok := conn.(*tls.Conn); ok {
                    if tlsConn.ConnectionState().HelloRetryRequest {
                        atomic.AddInt64(&hrrConnections, 1)
                    }
                }
            }
        },
    }

    // Endpoint для метрик
    http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
        total := atomic.LoadInt64(&totalConnections)
        hrr := atomic.LoadInt64(&hrrConnections)
        fmt.Fprintf(w, "tls_connections_total %d\n", total)
        fmt.Fprintf(w, "tls_hrr_connections_total %d\n", hrr)
    })

    server.ListenAndServeTLS("cert.pem", "key.pem")
}

HelloRetryRequest происходит в нескольких случаях. Чаще всего клиент не предложил ключевые доли для выбранной сервером группы. Иногда сервер предпочитает другую криптографическую группу. В других случаях сервер требует cookie для защиты от DoS-атак. Понимание этих сценариев помогает правильно настроить CurvePreferences и минимизировать лишние round-trip.

С включением постквантовой криптографии HRR случается чаще. Старые клиенты не предлагают ML-KEM, и сервер может запросить классическую кривую. Поэтому мониторинг HRR особенно актуален при переходе на постквантовые алгоритмы.

HelloRetryRequest добавляет один round-trip к TLS handshake. На соединениях с высокой латентностью (мобильные сети, межконтинентальные соединения) это заметно. Правильная настройка CurvePreferences на клиенте и сервере минимизирует вероятность HRR.

crypto/tls — удаление GODEBUG в Go 1.27

Go 1.26 помечает несколько GODEBUG-настроек TLS как устаревшие. В Go 1.27 они будут удалены, и устаревшие криптографические режимы станут недоступны.

Что будет удалено

Пять настроек перестанут работать в следующей версии Go:

Настройка

Что делает

Версия добавления

tlsunsafeekm

Разрешает ExportKeyingMaterial без EMS или TLS 1.3

Go 1.22

tlsrsakex

Включает RSA-only обмен ключами

Go 1.22

tls10server

Разрешает TLS 1.0/1.1 на сервере

Go 1.22

tls3des

Включает 3DES в cipher suites

Go 1.23

x509keypairleaf

Контролирует заполнение Certificate.Leaf

Go 1.23

tlsunsafeekm

ConnectionState.ExportKeyingMaterial экспортирует ключевой материал из TLS-соединения. Без Extended Master Secret (EMS) или TLS 1.3 это небезопасно, потому что возможна атака с подменой сервера.

Сейчас:

GODEBUG=tlsunsafeekm=1 ./myapp

После Go 1.27: ExportKeyingMaterial потребует TLS 1.3 или EMS. Старые соединения не смогут экспортировать ключи.

Миграция:

package main

import (
    "crypto/tls"
    "log"
)

func main() {
    config := &tls.Config{
        MinVersion: tls.VersionTLS13, // Требуем TLS 1.3
    }

    conn, err := tls.Dial("tcp", "example.com:443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    state := conn.ConnectionState()

    // Безопасный экспорт ключей в TLS 1.3
    key, err := state.ExportKeyingMaterial("my-protocol", nil, 32)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Exported key: %x", key)
}

tlsrsakex

RSA key exchange передаёт pre-master secret, зашифрованный открытым ключом сервера. Если приватный ключ сервера скомпрометирован, злоумышленник расшифрует весь записанный трафик. Это происходит из-за отсутствия forward secrecy, когда один скомпрометированный ключ раскрывает все прошлые соединения.

Сейчас:

GODEBUG=tlsrsakex=1 ./myapp

После Go 1.27 останутся только ECDHE и другие механизмы с forward secrecy. На практике это не вызовет проблем, потому что все современные TLS-реализации поддерживают ECDHE.

Миграция не требуется, если твои серверы и клиенты обновлены за последние 10 лет.

tls10server

TLS 1.0 и 1.1 уязвимы к нескольким атакам: BEAST, POODLE, Lucky13. PCI DSS запретил TLS 1.0 ещё в 2018 году.

Сейчас:

GODEBUG=tls10server=1 ./myapp

После Go 1.27 минимальной версией станет TLS 1.2 для серверов и клиентов. Это соответствует требованиям PCI DSS и рекомендациям NIST.

Проверка совместимости:

package main

import (
    "crypto/tls"
    "log"
    "net/http"
)

func main() {
    server := &http.Server{
        Addr: ":8443",
        TLSConfig: &tls.Config{
            MinVersion: tls.VersionTLS12, // Готовимся к Go 1.27
        },
    }

    log.Println("Server requires TLS 1.2 or later")
    log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}

tls3des

3DES (Triple DES) использует 64-битные блоки. При объёме данных около 32 ГБ становится возможной атака Sweet32 на восстановление открытого текста.

Сейчас:

GODEBUG=tls3des=1 ./myapp

После Go 1.27 3DES будет полностью удалён из cipher suites. Современные альтернативы, такие как AES-GCM, работают значительно быстрее и обеспечивают лучшую защиту.

x509keypairleaf

Настройка x509keypairleaf контролирует, заполняют ли X509KeyPair и LoadX509KeyPair поле Certificate.Leaf распарсенным сертификатом.

Сейчас можно отключить:

GODEBUG=x509keypairleaf=0 ./myapp

После Go 1.27 поле Certificate.Leaf будет заполняться всегда. Это ускоряет работу, потому что сертификат не нужно парсить повторно при каждом handshake. В результате приложения получат небольшой прирост производительности без каких-либо изменений в коде.

Как подготовиться

  1. Проверь, используешь ли ты эти настройки:

grep -r "GODEBUG" . | grep -E "(tlsunsafeekm|tlsrsakex|tls10server|tls3des|x509keypairleaf)"
  1. Убедитесь, что клиенты поддерживают TLS 1.2+:

package main

import (
    "crypto/tls"
    "log"
)

func main() {
    config := &tls.Config{
        MinVersion: tls.VersionTLS12,
        // Проверяем, что современные cipher suites работают
        CipherSuites: []uint16{
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        },
    }

    listener, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    log.Println("Testing TLS 1.2+ configuration")
}
  1. Обновите мониторинг для отслеживания соединений с устаревшими протоколами.

Если тебе нужна поддержка древних клиентов, рассмотри TLS-терминирующий прокси (nginx, HAProxy, Envoy) с отдельной настройкой для legacy-трафика.

crypto/x509 — методы String() и OID()

Go 1.26 добавляет методы String() и OID() для типов ExtKeyUsage и KeyUsage в пакете crypto/x509. Это упрощает логирование и отладку работы с сертификатами.

Новые методы для ExtKeyUsage:

func (e ExtKeyUsage) String() string
func (e ExtKeyUsage) OID() OID

Новый метод для KeyUsage:

func (k KeyUsage) String() string

Метод String() для ExtKeyUsage

Возвращает имя Extended Key Usage согласно RFC 5280 и другим реестрам:

package main

import (
    "crypto/x509"
    "fmt"
)

func main() {
    usages := []x509.ExtKeyUsage{
        x509.ExtKeyUsageServerAuth,
        x509.ExtKeyUsageClientAuth,
        x509.ExtKeyUsageCodeSigning,
        x509.ExtKeyUsageEmailProtection,
        x509.ExtKeyUsageTimeStamping,
        x509.ExtKeyUsageOCSPSigning,
    }

    for _, usage := range usages {
        fmt.Printf("%d: %s\n", usage, usage.String())
    }
}

Вывод:

1: serverAuth
2: clientAuth
3: codeSigning
4: emailProtection
8: timeStamping
9: OCSPSigning

Метод OID() для ExtKeyUsage

Возвращает Object Identifier для Extended Key Usage:

package main

import (
    "crypto/x509"
    "fmt"
)

func main() {
    usage := x509.ExtKeyUsageServerAuth

    oid := usage.OID()
    fmt.Printf("ExtKeyUsage: %s\n", usage.String())
    fmt.Printf("OID: %s\n", oid.String())
}

Вывод:

ExtKeyUsage: serverAuth
OID: 1.3.6.1.5.5.7.3.1

Метод String() для KeyUsage

KeyUsage представляет собой битовую маску, поэтому метод String() возвращает список всех установленных флагов. Это упрощает логирование и отладку, потому что не нужно вручную проверять каждый бит:

package main

import (
    "crypto/x509"
    "fmt"
)

func main() {
    // Типичный набор для TLS-сертификата
    usage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment

    fmt.Printf("KeyUsage: %s\n", usage.String())

    // Для CA-сертификата
    caUsage := x509.KeyUsageCertSign | x509.KeyUsageCRLSign
    fmt.Printf("CA KeyUsage: %s\n", caUsage.String())
}

Вывод:

KeyUsage: digitalSignature, keyEncipherment
CA KeyUsage: keyCertSign, cRLSign

Практическое применение

Логирование сертификатов

package main

import (
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "log"
    "os"
)

func main() {
    certPEM, err := os.ReadFile("cert.pem")
    if err != nil {
        log.Fatal(err)
    }

    block, _ := pem.Decode(certPEM)
    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Subject: %s\n", cert.Subject)
    fmt.Printf("KeyUsage: %s\n", cert.KeyUsage.String())

    if len(cert.ExtKeyUsage) > 0 {
        fmt.Println("ExtKeyUsage:")
        for _, eku := range cert.ExtKeyUsage {
            fmt.Printf("  - %s (OID: %s)\n", eku.String(), eku.OID())
        }
    }
}

Валидация сертификатов

package main

import (
    "crypto/x509"
    "fmt"
)

func validateServerCert(cert *x509.Certificate) error {
    // Проверяем KeyUsage
    required := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
    if cert.KeyUsage&required != required {
        return fmt.Errorf("missing KeyUsage: have %s, need digitalSignature and keyEncipherment",
            cert.KeyUsage.String())
    }

    // Проверяем ExtKeyUsage
    hasServerAuth := false
    for _, eku := range cert.ExtKeyUsage {
        if eku == x509.ExtKeyUsageServerAuth {
            hasServerAuth = true
            break
        }
    }

    if !hasServerAuth {
        return fmt.Errorf("certificate missing ExtKeyUsage serverAuth, has: %v",
            formatExtKeyUsages(cert.ExtKeyUsage))
    }

    return nil
}

func formatExtKeyUsages(usages []x509.ExtKeyUsage) string {
    var result string
    for i, u := range usages {
        if i > 0 {
            result += ", "
        }
        result += u.String()
    }
    return result
}

Аудит сертификатов

package main

import (
    "crypto/x509"
    "encoding/json"
    "fmt"
)

type CertAudit struct {
    Subject      string   `json:"subject"`
    KeyUsage     string   `json:"key_usage"`
    ExtKeyUsage  []string `json:"ext_key_usage"`
    ExtKeyOIDs   []string `json:"ext_key_oids"`
}

func auditCertificate(cert *x509.Certificate) CertAudit {
    audit := CertAudit{
        Subject:  cert.Subject.String(),
        KeyUsage: cert.KeyUsage.String(),
    }

    for _, eku := range cert.ExtKeyUsage {
        audit.ExtKeyUsage = append(audit.ExtKeyUsage, eku.String())
        audit.ExtKeyOIDs = append(audit.ExtKeyOIDs, eku.OID().String())
    }

    return audit
}

func main() {
    // Пример использования
    audit := CertAudit{
        Subject:     "CN=example.com",
        KeyUsage:    "digitalSignature, keyEncipherment",
        ExtKeyUsage: []string{"serverAuth", "clientAuth"},
        ExtKeyOIDs:  []string{"1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.2"},
    }

    data, _ := json.MarshalIndent(audit, "", "  ")
    fmt.Println(string(data))
}

Основные Extended Key Usage и их OID:

Константа

String()

OID

ExtKeyUsageServerAuth

serverAuth

1.3.6.1.5.5.7.3.1

ExtKeyUsageClientAuth

clientAuth

1.3.6.1.5.5.7.3.2

ExtKeyUsageCodeSigning

codeSigning

1.3.6.1.5.5.7.3.3

ExtKeyUsageEmailProtection

emailProtection

1.3.6.1.5.5.7.3.4

ExtKeyUsageTimeStamping

timeStamping

1.3.6.1.5.5.7.3.8

ExtKeyUsageOCSPSigning

OCSPSigning

1.3.6.1.5.5.7.3.9

crypto/x509 — функция OIDFromASN1OID

Go 1.26 добавляет функцию OIDFromASN1OID в пакет crypto/x509 для конвертации между типами OID из разных пакетов.

В Go существуют два типа для Object Identifier. Тип encoding/asn1.ObjectIdentifier используется при парсинге ASN.1 данных, а тип crypto/x509.OID применяется в пакете x509 для работы с сертификатами. До Go 1.26 прямого способа конвертации между ними не было, и разработчикам приходилось писать вспомогательные функции вручную.

func OIDFromASN1OID(oid asn1.ObjectIdentifier) OID

Преобразует asn1.ObjectIdentifier в x509.OID.

package main

import (
    "crypto/x509"
    "encoding/asn1"
    "fmt"
)

func main() {
    // OID для serverAuth из ASN.1
    asn1OID := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 1}

    // Конвертируем в x509.OID
    x509OID := x509.OIDFromASN1OID(asn1OID)

    fmt.Printf("ASN.1 OID: %v\n", asn1OID)
    fmt.Printf("x509 OID: %s\n", x509OID.String())
    fmt.Printf("Equal: %v\n", x509OID.Equal(x509.ExtKeyUsageServerAuth.OID()))
}

Вывод:

ASN.1 OID: 1.3.6.1.5.5.7.3.1
x509 OID: 1.3.6.1.5.5.7.3.1
Equal: true

Характеристика

asn1.ObjectIdentifier

x509.OID

Определение

[]int

Непрозрачный тип

Парсинг

Прямой доступ к элементам

Через методы

Строковое представление

fmt.Sprintf("%v", oid)

oid.String()

Сравнение

reflect.DeepEqual()

oid.Equal()

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

Низкоуровневый ASN.1

Высокоуровневый x509

Для конвертации в обратную сторону используйте метод ToASN1OID:

package main

import (
    "crypto/x509"
    "fmt"
)

func main() {
    // x509.OID -> asn1.ObjectIdentifier
    x509OID := x509.ExtKeyUsageServerAuth.OID()
    asn1OID := x509OID.ToASN1OID()

    fmt.Printf("x509 OID: %s\n", x509OID.String())
    fmt.Printf("ASN.1 OID: %v\n", asn1OID)
}

debug/elf — константы R_LARCH для LoongArch

Go 1.26 добавляет константы релокаций для архитектуры LoongArch в пакет debug/elf. Новые константы соответствуют спецификации LoongArch ELF psABI v20250521 (глобальная версия v2.40).

LoongArch представляет собой процессорную архитектуру, разработанную китайской компанией Loongson. Это полностью независимая ISA, не совместимая с x86 или ARM. Архитектура используется в серверах, рабочих станциях и встраиваемых системах на внутреннем рынке Китая.

Go поддерживает LoongArch начиная с версии 1.19 (GOARCH=loong64).

Релокации ELF представляют собой инструкции для линкера о том, как модифицировать машинный код при связывании объектных файлов. Каждая архитектура определяет собственный набор типов релокаций.

Для LoongArch определены несколько групп:

Базовые релокации:

  • R_LARCH_NONE: нет действия

  • R_LARCH_32, R_LARCH_64: абсолютные адреса

  • R_LARCH_RELATIVE: относительный адрес для PIC

  • R_LARCH_COPY, R_LARCH_JUMP_SLOT: для динамической линковки

TLS-релокации:

  • R_LARCH_TLS_DTPMOD32, R_LARCH_TLS_DTPMOD64: идентификатор модуля

  • R_LARCH_TLS_DTPREL32, R_LARCH_TLS_DTPREL64: смещение в TLS-блоке

Релокации v2.00 ABI:

  • R_LARCH_B16, R_LARCH_B21, R_LARCH_B26: ветвления

  • R_LARCH_ABS_HI20, R_LARCH_ABS_LO12: абсолютная адресация

  • R_LARCH_PCALA_HI20, R_LARCH_PCALA_LO12: PC-relative адресация

  • R_LARCH_GOT_PC_HI20, R_LARCH_GOT_PC_LO12: доступ к GOT

Новые в v2.40:

  • R_LARCH_PCREL20_S2: PC-relative адресация с выравниванием 4 байта

  • R_LARCH_CALL36: вызов функции в расширенном диапазоне

Пример использования

package main

import (
    "debug/elf"
    "fmt"
    "log"
)

func main() {
    f, err := elf.Open("program.loongarch64")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    // Читаем секцию релокаций
    for _, section := range f.Sections {
        if section.Type != elf.SHT_RELA {
            continue
        }

        relas, err := section.Data()
        if err != nil {
            continue
        }

        fmt.Printf("Section %s:\n", section.Name)
        // Парсим релокации (упрощённо)
        analyzeRelocations(relas, f.Machine)
    }
}

func analyzeRelocations(data []byte, machine elf.Machine) {
    if machine != elf.EM_LOONGARCH {
        return
    }

    // Анализ релокаций LoongArch
    // R_LARCH_* константы теперь доступны в Go 1.26
    relocTypes := map[elf.R_LARCH]string{
        elf.R_LARCH_NONE:        "NONE",
        elf.R_LARCH_64:          "ABS64",
        elf.R_LARCH_PCALA_HI20:  "PC-relative high 20 bits",
        elf.R_LARCH_PCALA_LO12:  "PC-relative low 12 bits",
        elf.R_LARCH_CALL36:      "Function call (36-bit range)",
    }

    for typ, name := range relocTypes {
        fmt.Printf("  %s (%d): %s\n", typ, typ, name)
    }
}

Константы R_LARCH_* нужны разработчикам отладчиков и профилировщиков для LoongArch, авторам инструментов статического анализа бинарников, а также создателям кросс-компиляторов, линкеров и систем эмуляции.

Для обычных Go-программ под LoongArch изменение прозрачно. Компилятор и линкер используют эти константы внутренне, поэтому разработчику не нужно ничего менять в коде.

errors: generic-функция AsType

Go 1.26 добавляет функцию errors.AsType[E error](err error) (E, bool), которая представляет собой типобезопасную альтернативу errors.As. Proposal для этой функции открыли ещё в 2022 год��, сразу после выхода дженериков.

Функция errors.As требует предварительного объявления переменной и передачи указателя:

var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Failed at path:", pathErr.Path)
}

У этого подхода есть несколько недостатков. Во-первых, переменная объявляется отдельно от места использования, что добавляет избыточности в код. Во-вторых, функция As использует reflect для проверки типов, что создаёт накладные расходы. В-третьих, передача неправильного типа вызывает панику в runtime, а не ошибку компиляции.

Решение: AsType

func AsType[E error](err error) (E, bool)

Новая функция принимает тип как параметр дженерика и возвращает типизированное значение:

if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
    fmt.Println("Failed at path:", pathErr.Path)
}

Практические примеры

Обработка сетевых ошибок

package main

import (
    "errors"
    "fmt"
    "net"
    "syscall"
)

func handleNetworkError(err error) {
    // Проверяем тип ошибки в одну строку
    if opErr, ok := errors.AsType[*net.OpError](err); ok {
        fmt.Printf("Network operation failed: %s on %s\n",
            opErr.Op, opErr.Addr)

        // Проверяем вложенную ошибку
        if sysErr, ok := errors.AsType[syscall.Errno](opErr.Err); ok {
            switch sysErr {
            case syscall.ECONNREFUSED:
                fmt.Println("Connection refused")
            case syscall.ETIMEDOUT:
                fmt.Println("Connection timed out")
            }
        }
    }
}

Каскадная проверка типов

func categorizeError(err error) string {
    if _, ok := errors.AsType[*fs.PathError](err); ok {
        return "filesystem"
    }
    if _, ok := errors.AsType[*net.OpError](err); ok {
        return "network"
    }
    if _, ok := errors.AsType[*json.SyntaxError](err); ok {
        return "parsing"
    }
    return "unknown"
}

Собственные типы ошибок

type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message)
}

func handleValidation(err error) {
    if valErr, ok := errors.AsType[*ValidationError](err); ok {
        fmt.Printf("Field %s: %s\n", valErr.Field, valErr.Message)
    }
}

Сравнение производительности

AsType не использует рефлексию, что даёт выигрыш в скорости:

func BenchmarkErrorsAs(b *testing.B) {
    err := &fs.PathError{Op: "open", Path: "/test", Err: os.ErrNotExist}
    wrapped := fmt.Errorf("wrapped: %w", err)

    b.Run("As", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var pathErr *fs.PathError
            _ = errors.As(wrapped, &pathErr)
        }
    })

    b.Run("AsType", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _, _ = errors.AsType[*fs.PathError](wrapped)
        }
    })
}

Результаты:

BenchmarkErrorsAs/As-8         10000000    105 ns/op    48 B/op    1 allocs/op
BenchmarkErrorsAs/AsType-8     30000000     42 ns/op     0 B/op    0 allocs/op

AsType работает в 2.5 раза быстрее и не аллоцирует память.

С errors.As можно допустить ошибку, которая проявится только в runtime:

// Компилируется, но паникует в runtime
var s string
errors.As(err, &s) // panic: *target must be interface or implement error

С AsType ошибка обнаруживается при компиляции:

// Не компилируется
errors.AsType[string](err) // string does not implement error

errors.As по-прежнему нужен, если целевой тип не реализует error:

type Temporary interface {
    Temporary() bool
}

// AsType не работает — Temporary не реализует error
var temp Temporary
if errors.As(err, &temp) && temp.Temporary() {
    // Повторяем операцию
}

Для стандартных случаев с типами ошибок используйте AsType.

fmt.Errorf: оптимизация аллокаций

Go 1.26 оптимизирует fmt.Errorf для строк без форматирования. Теперь fmt.Errorf("message") аллоцирует столько же памяти, сколько errors.New("message"). На практике это означает, что можно использовать единый стиль создания ошибок во всём проекте без потери производительности.

Раньше даже простой вызов fmt.Errorf("static message") запускал тяжёлую машинерию. Сначала newPrinter() создавал объект принтера из sync.Pool, затем doPrintf сканировал строку в поисках %, после чего p.buf аллоцировал временный буфер, и наконец результат оборачивался через errors.New(s). В итоге получалось минимум 2 аллокации на куче для любого вызова.

Компилятор Go 1.26 распознаёт вызовы fmt.Errorf без аргументов форматирования и генерирует оптимизированный код:

// До Go 1.26: 2 аллокации
err := fmt.Errorf("connection failed")

// Go 1.26: 0-1 аллокация (как errors.New)
err := fmt.Errorf("connection failed")
package main

import (
    "errors"
    "fmt"
    "testing"
)

var sink error

func BenchmarkErrorCreation(b *testing.B) {
    b.Run("errors.New", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            sink = errors.New("connection failed")
        }
    })

    b.Run("fmt.Errorf/static", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            sink = fmt.Errorf("connection failed")
        }
    })

    b.Run("fmt.Errorf/formatted", func(b *testing.B) {
        host := "localhost"
        for i := 0; i < b.N; i++ {
            sink = fmt.Errorf("connection to %s failed", host)
        }
    })
}

func BenchmarkErrorLocal(b *testing.B) {
    b.Run("errors.New", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            err := errors.New("local error")
            _ = err
        }
    })

    b.Run("fmt.Errorf", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            err := fmt.Errorf("local error")
            _ = err
        }
    })
}

Результаты на Go 1.25 (Apple M3 Pro):

BenchmarkErrorCreation/errors.New-11           1000000000   0.26 ns/op     0 B/op    0 allocs/op
BenchmarkErrorCreation/fmt.Errorf/static-11    30000000    40.0 ns/op    40 B/op    2 allocs/op
BenchmarkErrorCreation/fmt.Errorf/formatted-11 17000000    68.7 ns/op    40 B/op    2 allocs/op
BenchmarkErrorLocal/errors.New-11              1000000000   0.26 ns/op     0 B/op    0 allocs/op
BenchmarkErrorLocal/fmt.Errorf-11              20000000    60.0 ns/op    48 B/op    2 allocs/op

Результаты на Go 1.26 (Apple M3 Pro):

BenchmarkErrorCreation/errors.New-11           1000000000   0.26 ns/op     0 B/op    0 allocs/op
BenchmarkErrorCreation/fmt.Errorf/static-11    495000000    2.4 ns/op     0 B/op    0 allocs/op
BenchmarkErrorCreation/fmt.Errorf/formatted-11 20000000    60.4 ns/op    40 B/op    2 allocs/op
BenchmarkErrorLocal/errors.New-11              1000000000   0.26 ns/op     0 B/op    0 allocs/op
BenchmarkErrorLocal/fmt.Errorf-11              22000000    53.3 ns/op    48 B/op    2 allocs/op

Улучшение для статических строк драматическое. Вызов fmt.Errorf("static") ускорился с 40 ns до 2.4 ns — в 16 раз быстрее. Аллокации полностью устранены: было 40 B и 2 аллокации, стало 0 B и 0 аллокаций. Форматированные ошибки с параметрами тоже ускорились на 12% (с 68.7 ns до 60.4 ns).

Теперь можно унифицировать создание ошибок в проекте:

// Раньше рекомендовали различать
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("user %d not found", userID)

// В Go 1.26 можно везде использовать fmt.Errorf
var ErrNotFound = fmt.Errorf("not found")
err := fmt.Errorf("user %d not found", userID)

Оптимизация работает только для вызовов без аргументов:

fmt.Errorf("static")           // Оптимизировано
fmt.Errorf("value: %d", 42)    // Не оптимизировано — есть аргументы
fmt.Errorf("wrapped: %w", err) // Не оптимизировано — есть %w

Вызовы с %w по-прежнему используют обычный путь форматирования.

go/ast: ParseDirective для директив

Go 1.26 добавляет функцию ParseDirective и связанные типы для разбора директивных комментариев вида //go:generate, //go:embed и пользовательских директив. Это упрощает создание инструментов, которые работают с директивами в Go-коде.

У *ast.CommentGroup есть метод Text(), который возвращает текст комментариев без директив. Однако обратной операции для получения директив не было, и приходилось парсить сырые комментарии вручную.

Теперь, когда директивы стандартизированы и сторонние инструменты могут определять собственные, команда Go добавила удобный API.

Директивы следуют формату //toolname:directive arguments:

//go:generate stringer -type=Pill
//go:embed templates/*.html
//myapp:config key=value
//lint:ignore ST1003 this name is required

Регулярное выражение формата: //([a-z0-9]+):([a-z0-9]+)\s*(.*). Здесь первая группа представляет имя инструмента (namespace), вторая группа содержит имя директивы, а третья группа (опционально) включает аргументы. Пространство имён go зарезервировано для стандартных директив.

// Directive представляет разобранную директиву
type Directive struct {
    Namespace string // "go", "lint", "myapp"
    Name      string // "generate", "embed", "ignore"
    Arguments string // Всё после директивы
}

// Directives возвращает итератор по директивам в группе комментариев
func (g *CommentGroup) Directives() iter.Seq[Directive]

Извлечение директив из файла

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    src := `package example

//go:generate stringer -type=Status
//go:embed static/*
//myapp:version 1.2.3

// Regular comment
type Status int
`

    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "example.go", src, parser.ParseComments)
    if err != nil {
        panic(err)
    }

    // Обходим все группы комментариев
    for _, cg := range f.Comments {
        for dir := range cg.Directives() {
            fmt.Printf("Namespace: %s, Name: %s, Args: %s\n",
                dir.Namespace, dir.Name, dir.Arguments)
        }
    }
}

// Output:
// Namespace: go, Name: generate, Args: stringer -type=Status
// Namespace: go, Name: embed, Args: static/*
// Namespace: myapp, Name: version, Args: 1.2.3

Фильтрация по пространству имён

func findMyAppDirectives(cg *ast.CommentGroup) []ast.Directive {
    var result []ast.Directive
    for dir := range cg.Directives() {
        if dir.Namespace == "myapp" {
            result = append(result, dir)
        }
    }
    return result
}

Реализация собственного инструмента

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
    "strings"
)

// Пример: инструмент для генерации валидаторов
// //validate:range min=0 max=100
// //validate:pattern ^[a-z]+$

type ValidateDirective struct {
    Type   string
    Params map[string]string
}

func parseValidateDirectives(f *ast.File) []ValidateDirective {
    var directives []ValidateDirective

    for _, cg := range f.Comments {
        for dir := range cg.Directives() {
            if dir.Namespace != "validate" {
                continue
            }

            vd := ValidateDirective{
                Type:   dir.Name,
                Params: parseParams(dir.Arguments),
            }
            directives = append(directives, vd)
        }
    }

    return directives
}

func parseParams(args string) map[string]string {
    params := make(map[string]string)
    for _, part := range strings.Fields(args) {
        if kv := strings.SplitN(part, "=", 2); len(kv) == 2 {
            params[kv[0]] = kv[1]
        }
    }
    return params
}

func main() {
    src := `package example

//validate:range min=0 max=100
type Age int

//validate:pattern regex=^[a-z]+$
type Username string
`

    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "example.go", src, parser.ParseComments)

    for _, vd := range parseValidateDirectives(f) {
        fmt.Printf("Validation: %s, params: %v\n", vd.Type, vd.Params)
    }
}

// Output:
// Validation: range, params: map[max:100 min:0]
// Validation: pattern, params: map[regex:^[a-z]+$]

Директивы //line, //export и //extern появились до стандартизации формата. Для них поле Namespace устанавливается в "go":

//line foo.go:10       -> Directive{Namespace: "go", Name: "line", Arguments: "foo.go:10"}
//export MyFunction    -> Directive{Namespace: "go", Name: "export", Arguments: "MyFunction"}

Метод String() типа Directive правильно форматирует legacy-директивы:

dir := ast.Directive{Namespace: "go", Name: "line", Arguments: "foo.go:10"}
fmt.Println(dir.String()) // //line foo.go:10 (без "go:")

go/ast: поле BasicLit.ValueEnd

Go 1.26 добавляет поле ValueEnd в структуру ast.BasicLit. Это исправляет давнюю проблему с вычислением конечной позиции многострочных raw string литералов в Windows-файлах. Благодаря этому инструменты форматирования и анализа кода теперь корректно обрабатывают файлы с разными типами переносов строк.

Метод BasicLit.End() вычислял конечную позицию литерала эвристически, прибавляя длину значения к начальной позиции:

func (x *BasicLit) End() token.Pos {
    return token.Pos(int(x.ValuePos) + len(x.Value))
}

Это работало для большинства случаев, но ломалось на raw string литералах с \r\n в исходнике. Парсер Go удаляет \r из raw strings, но позиции в файле остаются прежними.

Пример проблемы:

// Файл с Windows line endings (CRLF)
var s = `line1
line2
line3`

После парсинга BasicLit.Value содержит "line1\nline2\nline3" (без \r), но End() должен указывать на позицию после закрывающего backtick в оригинальном файле с учётом удалённых \r.

Новое поле ValueEnd хранит точную конечную позицию:

type BasicLit struct {
    ValuePos token.Pos   // Начало литерала
    Kind     token.Token // INT, FLOAT, IMAG, CHAR, STRING
    Value    string      // Значение литерала
    ValueEnd token.Pos   // Конец литерала (новое в Go 1.26)
}

Метод End() теперь использует ValueEnd, если оно установлено:

func (x *BasicLit) End() token.Pos {
    if x.ValueEnd != 0 {
        return x.ValueEnd
    }
    return token.Pos(int(x.ValuePos) + len(x.Value))
}
package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    src := "package main\n\nvar s = `multi\nline\nstring`\n"

    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "test.go", src, 0)

    ast.Inspect(f, func(n ast.Node) bool {
        if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
            fmt.Printf("Value: %q\n", lit.Value)
            fmt.Printf("ValuePos: %v\n", fset.Position(lit.ValuePos))
            fmt.Printf("ValueEnd: %v\n", fset.Position(lit.ValueEnd))
            fmt.Printf("End(): %v\n", fset.Position(lit.End()))
        }
        return true
    })
}

// Output:
// Value: "`multi\nline\nstring`"
// ValuePos: test.go:3:9
// ValueEnd: test.go:5:7
// End(): test.go:5:7

Если твой код модифицирует AST и изменяет ValuePos, нужно также обновить или обнулить ValueEnd:

// До Go 1.26
lit.ValuePos = newPos
lit.Value = newValue

// Go 1.26+
lit.ValuePos = newPos
lit.Value = newValue
lit.ValueEnd = 0 // Сбрасываем, чтобы End() вычислялся из Value
// или
lit.ValueEnd = newPos + token.Pos(len(newValue))

Если ValueEnd не обнулить после изменения ValuePos, End() вернёт старую позицию, что может привести к некорректному форматированию.

Изменение влияет на gofmt и goimports при форматировании кода, на gopls при подсветке и навигации в IDE, а также на go/printer при печати AST. Если ты разрабатываешь кастомные инструменты анализа и трансформации кода, проверь обработку BasicLit с многострочными строками.

go/token: метод File.End

Go 1.26 добавляет метод File.End() в пакет go/token. Это convenience-метод, который возвращает конечную позицию файла. Теперь не нужно вычислять эту позицию вручную через арифметику с базовым адресом и размером.

Чтобы получить конечную позицию файла, приходилось вычислять её вручную:

fset := token.NewFileSet()
f := fset.AddFile("test.go", fset.Base(), 100)

// Конечная позиция = base + size
endPos := token.Pos(f.Base() + f.Size())

Или использовать Pos() с размером файла:

endPos := f.Pos(f.Size())

Оба варианта неочевидны и требуют знания внутренней структуры позиций.

func (f *File) End() token.Pos

Метод возвращает позицию сразу после последнего байта файла:

fset := token.NewFileSet()
f := fset.AddFile("test.go", fset.Base(), 100)

endPos := f.End() // Просто и понятно
package main

import (
    "fmt"
    "go/parser"
    "go/token"
)

func main() {
    src := `package main

import "fmt"

func main() {
    fmt.Println("Hello")
}
`

    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "main.go", src, 0)

    // Получаем файл из FileSet
    tokenFile := fset.File(f.Pos())

    fmt.Printf("File: %s\n", tokenFile.Name())
    fmt.Printf("Size: %d bytes\n", tokenFile.Size())
    fmt.Printf("Start: %v\n", fset.Position(token.Pos(tokenFile.Base())))
    fmt.Printf("End: %v\n", fset.Position(tokenFile.End()))
}

// Output:
// File: main.go
// Size: 68 bytes
// Start: main.go:1:1
// End: main.go:8:1

Не путай token.File.End() с ast.File.End(). Первый указывает на конец физического файла в байтах, а второй возвращает позицию конца последней декларации в AST. Это важное различие при работе с файлами, содержащими trailing-комментарии.

// ast.File.End() может быть раньше token.File.End(),
// если файл заканчивается комментарием

src := `package main
// trailing comment
`
// token.File.End() указывает после последнего символа
// ast.File.End() указывает после "package main"

go/types: удаление GODEBUG gotypesalias

Go 1.26 предупреждает об удалении настройки GODEBUG=gotypesalias в Go 1.27. После этого пакет go/types всегда будет создавать тип Alias для type aliases. Это завершает переходный период, начатый в Go 1.22, когда появился тип types.Alias.

В Go 1.22 пакет go/types получил новый тип types.Alias для представления алиасов типов. До этого алиасы представлялись напрямую как целевой тип:

type MyInt = int // До 1.22: types.Basic{kind: Int}
                 // С 1.22:  types.Alias{name: "MyInt", rhs: types.Basic{...}}

Изменение сломало бы существующие инструменты, поэтому ввели GODEBUG=gotypesalias. При значении 0 сохранялось старое поведение без Alias, а при значении 1 включалось новое поведение с Alias. Значение по умолчанию зависело от версии Go в go.mod.

В go 1.27 настройка gotypesalias будет удалена. Пакет go/types всегда будет создавать types.Alias для алиасов типов независимо от значения GODEBUG, версии языка в go.mod или любых других настроек. Это унифицирует поведение и упростит код инструментов анализа.

Обновление потребуется для линтеров (golangci-lint, staticcheck), генераторов кода (stringer, mockgen), IDE-плагинов (gopls уже обновлён) и кастомных анализаторов на базе go/types. Если ты поддерживаешь подобные инструменты, проверь обработку type aliases до выхода Go 1.27.

image/jpeg — обновления encoder и decoder

В Go 1.26 пакет image/jpeg получил обновлённые encoder и decoder. API остался прежним, однако битовое представление результатов может отличаться от предыдущих версий.

Внутренняя реализация кодирования и декодирования JPEG обновлена. API остался прежним, поэтому код продолжит компилироваться без изменений. Однако битовое представление результатов может отличаться от предыдущих версий.

Изменения включают развёртывание циклов unzig и shift-clamp, что увеличивает размер бинарника примерно на 16 КиБ. Мои бенчмарки на Apple M3 Pro показывают, что производительность декодирования практически не изменилась, а в некоторых случаях наблюдается небольшое замедление в пределах погрешности измерений. Возможно проблема в том что я тестировал на rc2.

Пример использования

package main

import (
    "bytes"
    "image"
    "image/color"
    "image/jpeg"
    "os"
)

func main() {
    // Создаём тестовое изображение
    img := image.NewRGBA(image.Rect(0, 0, 1920, 1080))
    for y := 0; y < 1080; y++ {
        for x := 0; x < 1920; x++ {
            img.Set(x, y, color.RGBA{
                R: uint8(x * 255 / 1920),
                G: uint8(y * 255 / 1080),
                B: 128,
                A: 255,
            })
        }
    }

    // Кодируем в JPEG
    var buf bytes.Buffer
    if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 85}); err != nil {
        panic(err)
    }

    // Декодируем обратно
    decoded, err := jpeg.Decode(&buf)
    if err != nil {
        panic(err)
    }

    bounds := decoded.Bounds()
    println("Size:", bounds.Dx(), "x", bounds.Dy())
}

Бенчмарк производительности

func BenchmarkJPEGDecode(b *testing.B) {
    sizes := []struct {
        name   string
        width  int
        height int
    }{
        {"640x480", 640, 480},
        {"1920x1080", 1920, 1080},
        {"3840x2160", 3840, 2160},
    }

    for _, size := range sizes {
        img := createTestImage(size.width, size.height)
        data := encodeToJPEG(img, 85)

        b.Run(size.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                reader := bytes.NewReader(data)
                jpeg.Decode(reader)
            }
        })
    }
}

Мои результаты на Apple M3 Pro:

Декодирование (Decode):

Разрешение

Go 1.25

Go 1.26

Изменение

640×480

1.17 ms/op

1.20 ms/op

~3% медленнее

1920×1080

7.74 ms/op

8.18 ms/op

~6% медленнее

3840×2160

29.9 ms/op

31.6 ms/op

~5% медленнее

Кодирование (Encode):

Разрешение

Go 1.25

Go 1.26

Изменение

640×480

2.44 ms/op

2.43 ms/op

без изменений

1920×1080

16.8 ms/op

16.8 ms/op

без изменений

3840×2160

62.1 ms/op

62.3 ms/op

без изменений

Небольшое замедление декодирования (3-6%) находится в пределах погрешности измерений. Кодирование работает идентично. Потребление памяти осталось неизменным для обеих операций.

Новая реализация может выдавать другие байты при тех же входных данных. Если твои тесты сравнивают результат jpeg.Encode с эталонными файлами, они могут сломаться.

Поэтому стоит заранее проверить, не полагаются ли тесты на побайтовое совпадение. Типичные проблемные паттерны:

// Этот тест может сломаться в Go 1.26
func TestEncodeGolden(t *testing.T) {
    img := loadTestImage()
    var buf bytes.Buffer
    jpeg.Encode(&buf, img, nil)

    golden, _ := os.ReadFile("testdata/golden.jpg")
    if !bytes.Equal(buf.Bytes(), golden) {
        t.Error("output mismatch") // Может упасть в Go 1.26
    }
}

Вместо побайтового сравнения лучше сравнивать декодированные пиксели с допуском:

func TestEncodeVisual(t *testing.T) {
    img := loadTestImage()
    var buf bytes.Buffer
    jpeg.Encode(&buf, img, nil)

    decoded, _ := jpeg.Decode(&buf)

    // Сравниваем пиксели с допуском на JPEG-артефакты
    if !imagesClose(img, decoded, 5) {
        t.Error("images differ too much")
    }
}

func imagesClose(a, b image.Image, threshold int) bool {
    boundsA := a.Bounds()
    boundsB := b.Bounds()
    if boundsA != boundsB {
        return false
    }

    for y := boundsA.Min.Y; y < boundsA.Max.Y; y++ {
        for x := boundsA.Min.X; x < boundsA.Max.X; x++ {
            r1, g1, b1, _ := a.At(x, y).RGBA()
            r2, g2, b2, _ := b.At(x, y).RGBA()

            if absDiff(r1, r2) > uint32(threshold*256) ||
               absDiff(g1, g2) > uint32(threshold*256) ||
               absDiff(b1, b2) > uint32(threshold*256) {
                return false
            }
        }
    }
    return true
}
  • image/jpeg — документация

  • PR #71618 — улучшение производительности decoder

io.ReadAll — оптимизация аллокаций

В Go 1.26 функция io.ReadAll стала выделять меньше промежуточной памяти и возвращает срез минимального размера. Для больших входных данных это даёт примерно двукратное ускорение.

Раньше ReadAll выделяла память с запасом и могла вернуть срез с избыточной capacity. Теперь функция выделяет меньше промежуточных буферов при чтении и возвращает срез с capacity, равной длине данных. В результате функция работает примерно в 2 раза быстрее и использует примерно в 2 раза меньше общей памяти. Выигрыш заметнее на больших объёмах данных, где экономия на аллокациях становится критически важной.

package main

import (
    "io"
    "os"
    "strings"
)

func main() {
    // Читаем данные из любого io.Reader
    reader := strings.NewReader("Hello, Go 1.26!")
    data, err := io.ReadAll(reader)
    if err != nil {
        panic(err)
    }

    os.Stdout.Write(data)
}

Бенчмарк

package main

import (
    "bytes"
    "io"
    "testing"
)

func BenchmarkReadAll(b *testing.B) {
    sizes := []int{
        1024,          // 1 KB
        1024 * 1024,   // 1 MB
        10 * 1024 * 1024, // 10 MB
    }

    for _, size := range sizes {
        data := make([]byte, size)
        for i := range data {
            data[i] = byte(i % 256)
        }

        b.Run(formatSize(size), func(b *testing.B) {
            b.SetBytes(int64(size))
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                reader := bytes.NewReader(data)
                io.ReadAll(reader)
            }
        })
    }
}

func formatSize(n int) string {
    switch {
    case n >= 1024*1024:
        return string(rune('0'+n/(1024*1024))) + "MB"
    case n >= 1024:
        return string(rune('0'+n/1024)) + "KB"
    default:
        return string(rune('0'+n)) + "B"
    }
}

Результаты на Apple M3 Pro (Go 1.25 vs Go 1.26):

Размер

Go 1.25

Go 1.26

Скорость

Память

Пропускная способность

1 KB

184 ns/op

201 ns/op

+9% (медленнее)

-22% (2864→2224 B)

5.5→5.1 GB/s

1 MB

157 µs/op

77 µs/op

-51% (быстрее)

-57% (5.2→2.2 MB)

6.7→13.6 GB/s

10 MB

1.70 ms/op

760 µs/op

-55% (быстрее)

-55% (52→23 MB)

6.2→13.8 GB/s

На больших объёмах ускорение достигает 2x, а потребление памяти снижается более чем в 2 раза. Для малых данных (1 KB) наблюдается небольшой регресс скорости (~9%), но при этом экономия памяти составляет 22%.

Эта оптимизация особенно полезна при чтении тел HTTP-ответов, загрузке файлов и обработке потоков. Благодаря снижению нагрузки на сборщик мусора приложения с высокой интенсивностью I/O получат ощутимый прирост производительности:

func fetchJSON(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // В Go 1.26 работает быстрее и использует меньше памяти
    return io.ReadAll(resp.Body)
}

Если твой код полагался на конкретную capacity возвращаемого среза, поведение может измениться:

// Раньше capacity могла быть больше len
data, _ := io.ReadAll(reader)
// В Go 1.26: cap(data) == len(data)

На практике это редко вызывает проблемы, так как полагаться на конкретное значение capacity обычно не следует. Поэтому для большинства проектов обновление пройдёт незаметно.

log/slog — NewMultiHandler и MultiHandler

В Go 1.26 пакет log/slog получил функцию NewMultiHandler, которая создаёт обработчик, направляющий логи сразу нескольким Handler'ам.

До Go 1.26 для отправки логов в несколько мест приходилось писать собственный Handler:

type multiHandler struct {
    handlers []slog.Handler
}

func (m *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
    for _, h := range m.handlers {
        if h.Enabled(ctx, level) {
            return true
        }
    }
    return false
}

func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
    for _, h := range m.handlers {
        if h.Enabled(ctx, r.Level) {
            if err := h.Handle(ctx, r); err != nil {
                return err
            }
        }
    }
    return nil
}

// ... WithAttrs, WithGroup

Решение: NewMultiHandler

func NewMultiHandler(handlers ...Handler) *MultiHandler

Функция создаёт MultiHandler, который вызывает все переданные Handler'ы. Это означает, что одна запись лога может одновременно попасть в консоль, файл, систему мониторинга и любые другие назначения.

package main

import (
    "log/slog"
    "os"
)

func main() {
    // Handler для консоли — только INFO и выше
    consoleHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })

    // Handler для файла — DEBUG и выше
    file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    defer file.Close()

    fileHandler := slog.NewJSONHandler(file, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    })

    // Объединяем оба Handler'а
    multi := slog.NewMultiHandler(consoleHandler, fileHandler)
    logger := slog.New(multi)

    // DEBUG попадёт только в файл
    logger.Debug("starting application", "config", "/etc/app.conf")

    // INFO попадёт и в консоль, и в файл
    logger.Info("server started", "port", 8080)

    // ERROR попадёт в оба места
    logger.Error("connection failed", "host", "db.example.com")
}

Метод Enabled возвращает true, если хотя бы один из вложенных Handler'ов включён для данного уровня:

// Если consoleHandler включён для INFO,
// а fileHandler включён для DEBUG,
// то multi.Enabled(ctx, slog.LevelDebug) вернёт true

Метод Handle вызывает Handle у всех включённых Handler'ов:

// Для записи DEBUG:
// - consoleHandler.Enabled() == false — пропускаем
// - fileHandler.Enabled() == true — вызываем Handle()

Методы WithAttrs и WithGroup создают новый MultiHandler, применяя соответствующий метод к каждому вложенному Handler'у:

// Добавляем атрибут ко всем Handler'ам
enriched := multi.WithAttrs([]slog.Attr{
    slog.String("service", "api"),
})

// Теперь все записи будут содержать service=api

На практике это позволяет один раз настроить общие атрибуты (имя сервиса, версия, окружение) и не дублировать их при создании каждого Handler'а.

// Пример: разные форматы для разных уровней
func setupLogger() *slog.Logger {
    // Ошибки — в JSON для парсинга системами мониторинга
    errorHandler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: slog.LevelError,
    })

    // Всё остальное — в текстовом формате для людей
    textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })

    return slog.New(slog.NewMultiHandler(errorHandler, textHandler))
}
// Пример: логи в несколько систем
func setupProductionLogger() *slog.Logger {
    handlers := make([]slog.Handler, 0, 3)

    // Stdout для контейнерных логов
    handlers = append(handlers, slog.NewJSONHandler(os.Stdout, nil))

    // Sentry для ошибок
    if sentryDSN := os.Getenv("SENTRY_DSN"); sentryDSN != "" {
        handlers = append(handlers, newSentryHandler(sentryDSN))
    }

    // Файл для локальной отладки
    if logFile := os.Getenv("LOG_FILE"); logFile != "" {
        f, _ := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
        handlers = append(handlers, slog.NewTextHandler(f, &slog.HandlerOptions{
            Level: slog.LevelDebug,
        }))
    }

    return slog.New(slog.NewMultiHandler(handlers...))
}

net.Dialer — методы DialIP, DialTCP, DialUDP, DialUnix

В Go 1.26 тип net.Dialer получил четыре новых метода для подключения к конкретным типам сетей с поддержкой context.

Раньше для установки соединения с конкретным типом адреса приходилось использовать низкоуровневые функции net.DialTCP, net.DialUDP и т.д., которые не поддерживают контекст и настройки Dialer:

// Старый подход: без контекста и без настроек Dialer
addr, _ := net.ResolveTCPAddr("tcp", "example.com:80")
conn, err := net.DialTCP("tcp", nil, addr)

Новые методы Dialer объединяют удобство типизированных соединений с возможностями контекста и настроек. Благодаря этому код становится проще и безопаснее: не нужен type assertion, а отмена через контекст работает из коробки.

func (d *Dialer) DialIP(ctx context.Context, network string, laddr, raddr *IPAddr) (*IPConn, error)
func (d *Dialer) DialTCP(ctx context.Context, network string, laddr, raddr *TCPAddr) (*TCPConn, error)
func (d *Dialer) DialUDP(ctx context.Context, network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func (d *Dialer) DialUnix(ctx context.Context, network string, laddr, raddr *UnixAddr) (*UnixConn, error)
// Пример: TCP с таймаутом и локальным адресом
package main

import (
    "context"
    "fmt"
    "net"
    "time"
)

func main() {
    dialer := &net.Dialer{
        Timeout:   5 * time.Second,
        KeepAlive: 30 * time.Second,
    }

    // Резолвим адреса
    raddr, err := net.ResolveTCPAddr("tcp", "example.com:80")
    if err != nil {
        panic(err)
    }

    // Создаём контекст с таймаутом
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Подключаемся с типизированным результатом
    conn, err := dialer.DialTCP(ctx, "tcp", nil, raddr)
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // conn имеет тип *net.TCPConn, можно использовать TCP-специфичные методы
    conn.SetNoDelay(true)
    conn.SetKeepAlivePeriod(30 * time.Second)

    fmt.Printf("Connected to %s\n", conn.RemoteAddr())
}
// Пример: UDP с привязкой к локальному порту
func startUDPClient(serverAddr string, localPort int) (*net.UDPConn, error) {
    dialer := &net.Dialer{}

    laddr := &net.UDPAddr{Port: localPort}
    raddr, err := net.ResolveUDPAddr("udp", serverAddr)
    if err != nil {
        return nil, err
    }

    ctx := context.Background()
    conn, err := dialer.DialUDP(ctx, "udp", laddr, raddr)
    if err != nil {
        return nil, err
    }

    // conn имеет тип *net.UDPConn
    return conn, nil
}
// Пример: Unix-сокет
func connectToSocket(path string) (*net.UnixConn, error) {
    dialer := &net.Dialer{
        Timeout: 5 * time.Second,
    }

    raddr := &net.UnixAddr{Name: path, Net: "unix"}

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    return dialer.DialUnix(ctx, "unix", nil, raddr)
}

func main() {
    conn, err := connectToSocket("/var/run/docker.sock")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // Используем соединение
    conn.Write([]byte("GET /info HTTP/1.0\r\n\r\n"))
}
// Пример: raw IP-сокет
func pingHost(host string) error {
    dialer := &net.Dialer{}

    raddr, err := net.ResolveIPAddr("ip4", host)
    if err != nil {
        return err
    }

    ctx := context.Background()
    conn, err := dialer.DialIP(ctx, "ip4:icmp", nil, raddr)
    if err != nil {
        return err
    }
    defer conn.Close()

    // conn имеет тип *net.IPConn для ICMP
    return nil
}

На практике это упрощает код и убирает лишние проверки типов в рантайме.

net/http — HTTP2Config.StrictMaxConcurrentRequests

В Go 1.26 структура http.HTTP2Config получила новое поле StrictMaxConcurrentRequests, которое управляет поведением при превышении лимита потоков на HTTP/2-соединении.

HTTP/2 позволяет мультиплексировать запросы в одном TCP-соединении. Сервер сообщает максимальное число параллельных потоков через SETTINGS_MAX_CONCURRENT_STREAMS.

По умолчанию Go-клиент, получив этот лимит, открывает новое соединение для дополнительных запросов. Это может быть нежелательно при работе с rate-limited API, когда сервер намеренно ограничивает нагрузку, или в средах с ограниченным числом соединений.

Если StrictMaxConcurrentRequests установлен в true, клиент не будет открывать новое соединение при превышении лимита потоков. Вместо этого запрос будет ожидать освобождения слота в текущем соединении. Благодаря этому можно контролировать нагрузку на сервер и избежать ситуации, когда клиент создаёт слишком много соединений.

package main

import (
    "net/http"
    "time"
)

func main() {
    transport := &http.Transport{
        HTTP2: &http.HTTP2Config{
            // Не открывать новые соединения при превышении лимита потоков
            StrictMaxConcurrentRequests: true,
        },
    }

    client := &http.Client{
        Transport: transport,
        Timeout:   30 * time.Second,
    }

    // Все запросы будут использовать одно соединение
    // Если лимит потоков достигнут, запросы будут ждать
    resp, err := client.Get("https://api.example.com/data")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}

Сценарий

StrictMaxConcurrentRequests=false

StrictMaxConcurrentRequests=true

Лимит 100 потоков, 150 запросов

Открывается второе соединение

50 запросов ждут освобождения

Latency

Ниже (запросы не ждут)

Выше (запросы могут ждать)

Число соединений

Может расти

Ограничено

Нагрузка на сервер

Выше

Контролируемая

// Пример с мониторингом
func createStrictClient() *http.Client {
    transport := &http.Transport{
        MaxConnsPerHost: 1, // Одно соединение на хост
        HTTP2: &http.HTTP2Config{
            StrictMaxConcurrentRequests: true,
        },
    }

    return &http.Client{
        Transport: transport,
        Timeout:   60 * time.Second, // Увеличенный таймаут для ожидания
    }
}

net/http — Transport.NewClientConn

В Go 1.26 тип http.Transport получил метод NewClientConn, который возвращает клиентское соединение к HTTP-серверу.

Метод предназначен для случаев, когда нужен прямой доступ к HTTP-соединению вне стандартного механизма управления соединениями Transport.

func (t *Transport) NewClientConn(conn net.Conn) *ClientConn

Для большинства задач следует использовать Transport.RoundTrip, который автоматически управляет пулом соединений. Однако NewClientConn полезен при реализации собственного управления соединениями, когда нужен полный контроль над жизненным циклом соединения.

package main

import (
    "crypto/tls"
    "fmt"
    "net"
    "net/http"
)

func main() {
    // Устанавливаем TCP-соединение вручную
    tcpConn, err := net.Dial("tcp", "example.com:443")
    if err != nil {
        panic(err)
    }

    // Оборачиваем в TLS
    tlsConn := tls.Client(tcpConn, &tls.Config{
        ServerName: "example.com",
    })
    if err := tlsConn.Handshake(); err != nil {
        panic(err)
    }

    // Создаём HTTP-клиентское соединение
    transport := &http.Transport{}
    clientConn := transport.NewClientConn(tlsConn)

    // Теперь можно отправлять запросы через это соединение
    req, _ := http.NewRequest("GET", "https://example.com/", nil)
    resp, err := clientConn.RoundTrip(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    fmt.Printf("Status: %s\n", resp.Status)
}
// Кастомный пул соединений
type CustomPool struct {
    transport *http.Transport
    conns     map[string]*http.ClientConn
    mu        sync.Mutex
}

func (p *CustomPool) GetConn(host string) (*http.ClientConn, error) {
    p.mu.Lock()
    defer p.mu.Unlock()

    if conn, ok := p.conns[host]; ok {
        return conn, nil
    }

    netConn, err := net.Dial("tcp", host)
    if err != nil {
        return nil, err
    }

    clientConn := p.transport.NewClientConn(netConn)
    p.conns[host] = clientConn
    return clientConn, nil
}
// Соединения через прокси с кастомной логикой
func connectViaProxy(proxyAddr, targetHost string) (*http.ClientConn, error) {
    // Подключаемся к прокси
    proxyConn, err := net.Dial("tcp", proxyAddr)
    if err != nil {
        return nil, err
    }

    // Отправляем CONNECT
    fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n",
        targetHost, targetHost)

    // Читаем ответ прокси...
    // ...

    // После установки туннеля создаём клиентское соединение
    transport := &http.Transport{}
    return transport.NewClientConn(proxyConn), nil
}
// Мониторинг соединений
type MonitoredConn struct {
    net.Conn
    bytesRead    int64
    bytesWritten int64
}

func (m *MonitoredConn) Read(b []byte) (int, error) {
    n, err := m.Conn.Read(b)
    atomic.AddInt64(&m.bytesRead, int64(n))
    return n, err
}

func (m *MonitoredConn) Write(b []byte) (int, error) {
    n, err := m.Conn.Write(b)
    atomic.AddInt64(&m.bytesWritten, int64(n))
    return n, err
}

func createMonitoredClientConn(addr string) (*http.ClientConn, *MonitoredConn, error) {
    netConn, err := net.Dial("tcp", addr)
    if err != nil {
        return nil, nil, err
    }

    monitored := &MonitoredConn{Conn: netConn}
    transport := &http.Transport{}
    clientConn := transport.NewClientConn(monitored)

    return clientConn, monitored, nil
}

Следует помнить, что ClientConn не управляется пулом соединений Transport, поэтому закрытие соединения остаётся ответственностью вызывающего кода. Для обычных HTTP-клиентов лучше использовать http.Client с Transport. Метод NewClientConn полезен для низкоуровневых задач: реализации прокси, мониторинга трафика и тестирования.

net/http/httptest — редирект example.com

В Go 1.26 HTTP-клиент, возвращаемый httptest.Server.Client(), автоматически перенаправляет запросы к example.com и его поддоменам на тестовый сервер.

При тестировании HTTP-клиентов часто нужно подменить реальные URL на адреса тестового сервера:

// До Go 1.26 — приходилось модифицировать URL
func TestAPIClient(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer ts.Close()

    // Нужно было передавать URL тестового сервера
    client := NewAPIClient(ts.URL) // Вместо "https://api.example.com"
    result, err := client.GetStatus()
    // ...
}

Это требует либо параметризации URL в клиенте, либо сложных подмен.

В результате, в go 1.26, клиент из Server.Client() автоматически перенаправляет example.com на тестовый сервер, что значительно упрощает написание тестов:

func TestAPIClient(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // r.Host будет содержать оригинальный хост (example.com)
        w.Write([]byte(`{"status": "ok"}`))
    }))
    defer ts.Close()

    // Используем клиент из тестового сервера
    client := ts.Client()

    // Запрос к example.com перенаправится на тестовый сервер
    resp, err := client.Get("https://example.com/api/status")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()
    // ...
}
// Пример: тестирование API-клиента
package myapi

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

// APIClient — клиент с захардкоженным базовым URL
type APIClient struct {
    baseURL string
    client  *http.Client
}

func NewAPIClient(client *http.Client) *APIClient {
    return &APIClient{
        baseURL: "https://api.example.com", // Захардкожен
        client:  client,
    }
}

func (c *APIClient) GetUser(id string) (*http.Response, error) {
    return c.client.Get(c.baseURL + "/users/" + id)
}

func TestAPIClient_GetUser(t *testing.T) {
    // Создаём тестовый сервер
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/users/123" {
            t.Errorf("unexpected path: %s", r.URL.Path)
        }
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"id": "123", "name": "Test User"}`))
    }))
    defer ts.Close()

    // Клиент из ts.Client() перенаправит api.example.com на тестовый сервер
    apiClient := NewAPIClient(ts.Client())

    resp, err := apiClient.GetUser("123")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        t.Errorf("unexpected status: %d", resp.StatusCode)
    }
}

Редирект работает для всех поддоменов example.com:

func TestSubdomains(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Все эти запросы придут сюда
        w.Write([]byte("ok"))
    }))
    defer ts.Close()

    client := ts.Client()

    // Все эти URL перенаправляются на тестовый сервер
    client.Get("https://example.com/")
    client.Get("https://api.example.com/v1/users")
    client.Get("https://cdn.example.com/images/logo.png")
    client.Get("https://subdomain.api.example.com/data")
}

Handler получает оригинальный Host из запроса:

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // r.Host содержит оригинальный хост
    switch r.Host {
    case "api.example.com":
        handleAPI(w, r)
    case "cdn.example.com":
        handleCDN(w, r)
    default:
        http.NotFound(w, r)
    }
}))

Эта возможность особенно ценна при тестировании клиентов с захардкоженными URL и в интеграционных тестах, где модификация кода нежелательна. Также она упрощает тестирование мультидоменных приложений и проверку поведения редиректов между доменами.

net/http/httputil — deprecation ReverseProxy.Director

В Go 1.26 поле ReverseProxy.Director помечено как deprecated. Вместо него следует использовать ReverseProxy.Rewrite.

Director позволяет модифицировать исходящий запрос, но не защищает от атаки через hop-by-hop заголовки:

// Уязвимый код
proxy := &httputil.ReverseProxy{
    Director: func(req *http.Request) {
        req.URL.Scheme = "http"
        req.URL.Host = "backend.internal:8080"
        req.Header.Set("X-Forwarded-For", req.RemoteAddr)
        req.Header.Set("Authorization", "Bearer secret-token") // Добавляем токен
    },
}

Злоумышленник может отправить запрос с заголовком Connection: Authorization. По стандарту HTTP hop-by-hop заголовки удаляются при проксировании. В итоге прокси удалит Authorization перед отправкой на backend, и запрос останется без авторизации.

Rewrite (добавлен в Go 1.20) решает эту проблему, предоставляя доступ к двум версиям запроса:

type ProxyRequest struct {
    In  *http.Request // Оригинальный входящий запрос
    Out *http.Request // Исходящий запрос (модифицируем его)
}

Заголовки Out изначально пусты. Нужные заголовки из In копируются явно, что даёт полный контроль над тем, что попадёт в исходящий запрос.

// Было (уязвимо):
proxy := &httputil.ReverseProxy{
    Director: func(req *http.Request) {
        req.URL.Scheme = "http"
        req.URL.Host = "backend:8080"
        req.Host = "backend:8080"
        req.Header.Set("X-Real-IP", getClientIP(req))
    },
}
// Стало (безопасно):
proxy := &httputil.ReverseProxy{
    Rewrite: func(r *httputil.ProxyRequest) {
        r.SetURL(&url.URL{
            Scheme: "http",
            Host:   "backend:8080",
        })
        r.SetXForwarded()
        r.Out.Header.Set("X-Real-IP", getClientIP(r.In))
    },
}
// Полный пример
package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    backendURL, _ := url.Parse("http://backend:8080")

    proxy := &httputil.ReverseProxy{
        Rewrite: func(r *httputil.ProxyRequest) {
            // Устанавливаем целевой URL
            r.SetURL(backendURL)

            // Копируем безопасные заголовки
            r.Out.Header = r.In.Header.Clone()

            // Удаляем заголовки, которые не должны попасть на backend
            r.Out.Header.Del("Cookie")
            r.Out.Header.Del("Authorization")

            // Добавляем служебные заголовки
            r.SetXForwarded()

            // Добавляем внутренний токен
            r.Out.Header.Set("X-Internal-Auth", "trusted-proxy")
        },
    }

    http.ListenAndServe(":8080", proxy)
}
// SetURL устанавливает целевой URL
func (r *ProxyRequest) SetURL(target *url.URL)

// SetXForwarded добавляет X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto
func (r *ProxyRequest) SetXForwarded()

Аспект

Director

Rewrite

Доступ к оригиналу

Нет (модифицируем на месте)

Да (r.In, только чтение)

Заголовки исходящего

Копируются автоматически

Изначально пусты

Hop-by-hop атака

Уязвим

Защищён

Добавлен

Go 1.0

Go 1.20

Статус

Deprecated в Go 1.26

Рекомендуемый

Если установлены оба поля, Rewrite имеет приоритет:

proxy := &httputil.ReverseProxy{
    Director: func(req *http.Request) {
        // Не вызывается, если есть Rewrite
    },
    Rewrite: func(r *httputil.ProxyRequest) {
        // Используется этот
    },
}

net/netip — метод Prefix.Compare

В Go 1.26 тип netip.Prefix получил метод Compare для сравнения двух префиксов.

func (p Prefix) Compare(p2 Prefix) int

Метод возвращает -1 если p < p2, 0 если p == p2, и +1 если p > p2. Сравнение выполняется сначала по адресу, затем по длине префикса. Это позволяет использовать префиксы в сортировке и бинарном поиске.

package main

import (
    "fmt"
    "net/netip"
)

func main() {
    p1 := netip.MustParsePrefix("192.168.1.0/24")
    p2 := netip.MustParsePrefix("192.168.2.0/24")
    p3 := netip.MustParsePrefix("192.168.1.0/16")

    fmt.Println(p1.Compare(p2)) // -1 (p1 < p2 по адресу)
    fmt.Println(p2.Compare(p1)) // +1 (p2 > p1 по адресу)
    fmt.Println(p1.Compare(p1)) //  0 (равны)
    fmt.Println(p1.Compare(p3)) // +1 (одинаковый адрес, но /24 > /16)
}

Сначала сравниваются адреса (Addr), и только при равных адресах сравниваются длины маски (Bits). На практике это означает, что более специфичные подсети с тем же базовым адресом идут после менее специфичных.

// Примеры порядка:
// 10.0.0.0/8  < 10.0.0.0/16  (одинаковый адрес, /8 < /16)
// 10.0.0.0/16 < 10.0.1.0/16  (разные адреса)
// 10.0.0.0/8  < 192.168.0.0/8 (разные адреса)
// Сортировка префиксов
package main

import (
    "fmt"
    "net/netip"
    "slices"
)

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("192.168.1.0/24"),
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
        netip.MustParsePrefix("10.0.0.0/16"),
    }

    slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })

    for _, p := range prefixes {
        fmt.Println(p)
    }
    // Вывод:
    // 10.0.0.0/8
    // 10.0.0.0/16
    // 172.16.0.0/12
    // 192.168.1.0/24
}
// Поиск в отсортированном списке
func findPrefix(prefixes []netip.Prefix, target netip.Prefix) (int, bool) {
    return slices.BinarySearchFunc(prefixes, target, func(a, b netip.Prefix) int {
        return a.Compare(b)
    })
}

func main() {
    prefixes := []netip.Prefix{
        netip.MustParsePrefix("10.0.0.0/8"),
        netip.MustParsePrefix("172.16.0.0/12"),
        netip.MustParsePrefix("192.168.0.0/16"),
    }

    target := netip.MustParsePrefix("172.16.0.0/12")
    idx, found := findPrefix(prefixes, target)
    fmt.Printf("Index: %d, Found: %v\n", idx, found)
    // Index: 1, Found: true
}

У типа netip.Addr тоже есть метод Compare. Prefix.Compare использует его для сравнения адресной части:

addr1 := netip.MustParseAddr("192.168.1.1")
addr2 := netip.MustParseAddr("192.168.1.2")
fmt.Println(addr1.Compare(addr2)) // -1

prefix1 := netip.MustParsePrefix("192.168.1.0/24")
prefix2 := netip.MustParsePrefix("192.168.2.0/24")
fmt.Println(prefix1.Compare(prefix2)) // -1

net/url — отклонение URL с колонами в host

В Go 1.26 функция url.Parse отклоняет URL с некорректными двоеточиями в части host. Это исправляет потенциальные проблемы безопасности при парсинге URL.

Раньше url.Parse принимала URL с двоеточиями в host, которые не являются частью порта или IPv6-адреса:

// Go 1.25: парсится (некорректно)
// Go 1.26: возвращает ошибку
url.Parse("http://::1/path")
url.Parse("http://localhost:80:80/path")
url.Parse("http://host:port:extra/path")

Теперь такие URL отклоняются с ошибкой. IPv6-адреса в квадратных скобках по-прежнему парсятся корректно:

// Правильно — IPv6 в скобках
url.Parse("http://[::1]/path")           // OK
url.Parse("http://[::1]:8080/path")      // OK
url.Parse("http://[2001:db8::1]/path")   // OK

// Правильно — обычный host:port
url.Parse("http://localhost:8080/path")  // OK
url.Parse("http://example.com:443/path") // OK
// Примеры ошибок
package main

import (
    "fmt"
    "net/url"
)

func main() {
    badURLs := []string{
        "http://::1/",                  // IPv6 без скобок
        "http://localhost:80:80/",      // Два порта
        "http://host:port:extra/",      // Лишнее двоеточие
        "http://192.168.1.1:8080:9090/", // Два порта
    }

    for _, raw := range badURLs {
        _, err := url.Parse(raw)
        if err != nil {
            fmt.Printf("Rejected: %s\n  Error: %v\n", raw, err)
        }
    }
}

Вывод:

Rejected: http://::1/
  Error: parse "http://::1/": invalid character ":" in host name
Rejected: http://localhost:80:80/
  Error: parse "http://localhost:80:80/": invalid character ":" in host name
...

Если код зависит от старого поведения, можно временно отключить проверку через GODEBUG:

GODEBUG=urlstrictcolons=0 go run main.go

Однако это временное решение, и лучше исправить код для работы с корректными URL.

Двоеточия в host без скобок создают неоднозначность. Например, непонятно, является ли http://::1/ IPv6-адресом ::1 или ошибкой, а в http://host:80:extra/ неясно, где находится порт. Разные библиотеки и серверы интерпретировали такие URL по-разному, что могло приводить к уязвимостям SSRF (Server-Side Request Forgery). Поэтому теперь такие URL отклоняются явно.

os — метод Process.WithHandle

В Go 1.26 у типа os.Process появился метод WithHandle, который предоставляет доступ к низкоуровневому хендлу процесса операционной системы.

При работе с процессами в Go иногда требуется выполнить операции, недоступные через стандартный API: дождаться завершения процесса без блокировки, получить информацию о ресурсах процесса или использовать специфичные системные вызовы.

До Go 1.26 приходилось хранить PID и работать с ним напрямую. Такой подход ненадёжен, поскольку PID может быть переиспользован системой после завершения процесса. Это создавало риск отправки сигнала не тому процессу.

func (p *Process) WithHandle(f func(handle uintptr) error) error

Метод принимает функцию, которой передаётся хендл процесса. На разных платформах хендл имеет разный смысл:

Платформа

Тип хендла

Linux 5.4+

pidfd (файловый дескриптор)

Windows

HANDLE процесса

Другие

Возвращает ErrNoHandle

// Ожидание процесса через pidfd
package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"

    "golang.org/x/sys/unix"
)

func main() {
    cmd := exec.Command("sleep", "2")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    process := cmd.Process

    // Получаем pidfd и ждём через poll
    err := process.WithHandle(func(handle uintptr) error {
        fd := int(handle)

        // Используем poll для ожидания завершения
        fds := []unix.PollFd{{
            Fd:     int32(fd),
            Events: unix.POLLIN,
        }}

        fmt.Println("Waiting for process via pidfd...")
        _, err := unix.Poll(fds, 5000) // таймаут 5 секунд
        return err
    })

    if err != nil {
        fmt.Println("Poll error:", err)
    }

    // Собираем статус
    state, _ := process.Wait()
    fmt.Println("Process exited:", state.ExitCode())
}
// Проверка поддержки платформы
func supportsProcessHandle() bool {
    cmd := exec.Command("true")
    if err := cmd.Start(); err != nil {
        return false
    }
    defer cmd.Wait()

    supported := true
    err := cmd.Process.WithHandle(func(handle uintptr) error {
        return nil
    })

    if err == os.ErrNoHandle {
        supported = false
    }

    return supported
}
// Получение информации о процессе в Windows
//go:build windows

package main

import (
    "fmt"
    "os/exec"

    "golang.org/x/sys/windows"
)

func main() {
    cmd := exec.Command("notepad.exe")
    if err := cmd.Start(); err != nil {
        panic(err)
    }

    err := cmd.Process.WithHandle(func(handle uintptr) error {
        h := windows.Handle(handle)

        // Получаем время создания процесса
        var creation, exit, kernel, user windows.Filetime
        err := windows.GetProcessTimes(h, &creation, &exit, &kernel, &user)
        if err != nil {
            return err
        }

        fmt.Println("Process creation time:", creation.Nanoseconds())
        return nil
    })

    if err != nil {
        fmt.Println("Error:", err)
    }

    cmd.Process.Kill()
    cmd.Wait()
}

Хендл действителен только внутри переданной функции. После её возврата хендл может быть закрыт:

// Неправильно — хендл недействителен вне WithHandle
var savedHandle uintptr
process.WithHandle(func(h uintptr) error {
    savedHandle = h // Не делайте так!
    return nil
})
// savedHandle здесь уже недействителен

// Правильно — вся работа внутри функции
process.WithHandle(func(h uintptr) error {
    // Используем хендл здесь
    return doSomethingWithHandle(h)
})

На Linux pidfd решает проблему повторного использования PID. Благодаря этому отпадает риск случайно повлиять на чужой процесс:

// Проблема с PID
pid := cmd.Process.Pid
// ... процесс завершился ...
// PID может быть переиспользован другим процессом
syscall.Kill(pid, syscall.SIGTERM) // Может убить чужой процесс!

// Безопасно с pidfd
cmd.Process.WithHandle(func(fd uintptr) error {
    // pidfd всегда указывает на исходный процесс
    return unix.PidfdSendSignal(int(fd), unix.SIGTERM, nil, 0)
})

os — Windows file flags в OpenFile

В Go 1.26 функция os.OpenFile на Windows теперь принимает дополнительные флаги файловой системы, специфичные для этой платформы.

Параметр flag в OpenFile теперь может содержать не только стандартные флаги (O_RDONLY, O_WRONLY, O_CREATE и т.д.), но и Windows-специфичные флаги для тонкой настройки поведения файловых операций.

func OpenFile(name string, flag int, perm FileMode) (*File, error)

В пакете os появились константы для Windows file flags:

const (
    O_FILE_FLAG_WRITE_THROUGH    = 0x80000000
    O_FILE_FLAG_OVERLAPPED       = 0x40000000
    O_FILE_FLAG_NO_BUFFERING     = 0x20000000
    O_FILE_FLAG_RANDOM_ACCESS    = 0x10000000
    O_FILE_FLAG_SEQUENTIAL_SCAN  = 0x08000000
    O_FILE_FLAG_DELETE_ON_CLOSE  = 0x04000000
    O_FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
    O_FILE_FLAG_POSIX_SEMANTICS  = 0x01000000
)

FILE_FLAG_SEQUENTIAL_SCAN подсказывает системе, что файл будет читаться последовательно. Windows оптимизирует кэширование:

// Последовательное чтение больших файлов
//go:build windows

package main

import (
    "io"
    "os"
)

func processLargeFile(path string) error {
    // Оптимизация для последовательного чтения
    f, err := os.OpenFile(path,
        os.O_RDONLY|os.O_FILE_FLAG_SEQUENTIAL_SCAN,
        0,
    )
    if err != nil {
        return err
    }
    defer f.Close()

    buf := make([]byte, 64*1024)
    for {
        _, err := f.Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        // Обработка данных...
    }
    return nil
}

FILE_FLAG_RANDOM_ACCESS оптимизирует кэш для произвольного доступа:

// Произвольный доступ к файлу
func openDatabaseFile(path string) (*os.File, error) {
    return os.OpenFile(path,
        os.O_RDWR|os.O_FILE_FLAG_RANDOM_ACCESS,
        0644,
    )
}

FILE_FLAG_WRITE_THROUGH гарантирует запись данных на диск:

// Прямая запись без буферизации
func openLogFile(path string) (*os.File, error) {
    // Данные сразу пишутся на диск, не остаются в кэше
    return os.OpenFile(path,
        os.O_WRONLY|os.O_CREATE|os.O_APPEND|os.O_FILE_FLAG_WRITE_THROUGH,
        0644,
    )
}

FILE_FLAG_DELETE_ON_CLOSE удаляет файл автоматически:

// Удаление файла при закрытии
func createTempFile() (*os.File, error) {
    // Файл будет удалён при закрытии
    return os.OpenFile(
        `C:\Temp\session_`+generateID()+`.tmp`,
        os.O_RDWR|os.O_CREATE|os.O_FILE_FLAG_DELETE_ON_CLOSE,
        0600,
    )
}

Флаги можно комбинировать через побитовое ИЛИ:

// Комбинирование флагов
// Файл для логов: последовательная запись + гарантия записи на диск
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND |
         os.O_FILE_FLAG_SEQUENTIAL_SCAN |
         os.O_FILE_FLAG_WRITE_THROUGH

f, err := os.OpenFile("app.log", flags, 0644)

На других платформах эти константы равны нулю, что позволяет писать переносимый код:

// Кроссплатформенный код
package main

import "os"

func openOptimized(path string) (*os.File, error) {
    // На Windows — с оптимизацией
    // На Linux/macOS — обычное открытие
    flags := os.O_RDONLY | os.O_FILE_FLAG_SEQUENTIAL_SCAN
    return os.OpenFile(path, flags, 0)
}

Альтернативно можно использовать build tags:

//go:build windows

package fileutil

import "os"

func OpenForSequentialRead(path string) (*os.File, error) {
    return os.OpenFile(path,
        os.O_RDONLY|os.O_FILE_FLAG_SEQUENTIAL_SCAN,
        0,
    )
}
//go:build !windows

package fileutil

import "os"

func OpenForSequentialRead(path string) (*os.File, error) {
    // На Unix флаг не нужен
    return os.Open(path)
}

Некоторые флаги требуют особых условий. Флаг FILE_FLAG_NO_BUFFERING требует, чтобы размер буфера был кратен размеру сектора диска. Флаг FILE_FLAG_OVERLAPPED предполагает асинхронный I/O через IOCP. Флаг FILE_FLAG_BACKUP_SEMANTICS требует привилегий SE_BACKUP_NAME. Поэтому перед использованием этих флагов важно убедиться, что приложение соответствует требованиям Windows API.

// FILE_FLAG_NO_BUFFERING требует выравнивания
func openUnbuffered(path string) (*os.File, error) {
    f, err := os.OpenFile(path,
        os.O_RDONLY|os.O_FILE_FLAG_NO_BUFFERING,
        0,
    )
    if err != nil {
        return nil, err
    }

    // Чтение должно быть выровнено по размеру сектора (обычно 512 или 4096)
    return f, nil
}

os/signal — NotifyContext с причиной отмены

В Go 1.26 функция signal.NotifyContext теперь отменяет контекст с указанием причины, показывая какой именно сигнал был получен.

До Go 1.26 при получении сигнала контекст отменялся, но понять, какой именно сигнал вызвал отмену, было невозможно. Это затрудняло реализацию разного поведения для разных сигналов:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

<-ctx.Done()
// Контекст отменён, но какой сигнал пришёл?
fmt.Println(ctx.Err()) // context canceled — и всё

Теперь NotifyContext использует context.CancelCauseFunc для отмены контекста. Благодаря этому причину можно получить через context.Cause, что позволяет определить конкретный сигнал и выбрать соответствующую стратегию обработки:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

<-ctx.Done()

cause := context.Cause(ctx)
fmt.Println("Cause:", cause) // signal: interrupt или signal: terminated

При отмене контекста причина содержит информацию о сигнале. Это означает, что можно использовать type assertion для извлечения конкретного сигнала:

cause := context.Cause(ctx)

// Проверка типа сигнала
if sigErr, ok := cause.(signal.SignalError); ok {
    switch sigErr.Signal {
    case os.Interrupt:
        fmt.Println("Received Ctrl+C")
    case syscall.SIGTERM:
        fmt.Println("Received SIGTERM")
    case syscall.SIGHUP:
        fmt.Println("Received SIGHUP")
    }
}
// Graceful shutdown с логированием
package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080"}

    ctx, stop := signal.NotifyContext(context.Background(),
        os.Interrupt,
        syscall.SIGTERM,
        syscall.SIGHUP,
    )
    defer stop()

    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Println("Server error:", err)
        }
    }()

    fmt.Println("Server started on :8080")

    // Ждём сигнал
    <-ctx.Done()

    // Логируем причину остановки
    cause := context.Cause(ctx)
    fmt.Printf("Shutting down: %v\n", cause)

    // Graceful shutdown
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(shutdownCtx); err != nil {
        fmt.Println("Shutdown error:", err)
    }

    fmt.Println("Server stopped")
}

На практике это позволяет реализовать гибкую обработку: перезагрузку конфигурации по SIGHUP, graceful shutdown по SIGTERM и быстрый выход по Ctrl+C.

// Разное поведение для разных сигналов
func runWorker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            cause := context.Cause(ctx)

            if sigErr, ok := cause.(signal.SignalError); ok {
                switch sigErr.Signal {
                case syscall.SIGHUP:
                    // Перечитываем конфигурацию
                    reloadConfig()
                    continue // Продолжаем работу
                case syscall.SIGTERM:
                    // Завершаем gracefully
                    cleanup()
                    return
                case os.Interrupt:
                    // Быстрое завершение
                    return
                }
            }
            return

        default:
            doWork()
        }
    }
}
// Передача причины дальше по стеку
func processRequest(ctx context.Context) error {
    // Выполняем длительную операцию
    result, err := longOperation(ctx)
    if err != nil {
        // Проверяем, отменён ли контекст сигналом
        if cause := context.Cause(ctx); cause != nil {
            return fmt.Errorf("operation interrupted: %w", cause)
        }
        return err
    }
    return nil
}

// В вызывающем коде
err := processRequest(ctx)
if err != nil {
    // Получаем исходную причину
    var sigErr signal.SignalError
    if errors.As(err, &sigErr) {
        log.Printf("Request cancelled by signal: %s", sigErr.Signal)
    }
}

Код, который не использует context.Cause, продолжает работать:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

<-ctx.Done()
fmt.Println(ctx.Err()) // По-прежнему возвращает context.Canceled

Причину можно проверять через errors.Is:

cause := context.Cause(ctx)

if errors.Is(cause, os.Interrupt) {
    fmt.Println("Interrupted by Ctrl+C")
}

if errors.Is(cause, syscall.SIGTERM) {
    fmt.Println("Terminated by SIGTERM")
}

reflect — методы-итераторы для полей и методов

В Go 1.26 пакет reflect получил методы-итераторы, которые возвращают iter.Seq и iter.Seq2. Благодаря этому можно использовать range для обхода полей структур, методов типов и параметров функций. Это упрощает код рефлексии и делает его более идиоматичным.

// Итератор по полям структуры
func (t Type) Fields() iter.Seq[StructField]

// Итератор по методам типа
func (t Type) Methods() iter.Seq[Method]

// Итератор по входным параметрам функции
func (t Type) Ins() iter.Seq[Type]

// Итератор по возвращаемым значениям функции
func (t Type) Outs() iter.Seq[Type]

// Итератор по полям с их значениями
func (v Value) Fields() iter.Seq2[StructField, Value]

// Итератор по методам с их значениями
func (v Value) Methods() iter.Seq2[Method, Value]

Обход полей структуры

// До Go 1.26:
t := reflect.TypeOf(myStruct)
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    fmt.Println(field.Name)
}
// После Go 1.26:
t := reflect.TypeOf(myStruct)
for field := range t.Fields() {
    fmt.Println(field.Name)
}

Обход методов

// До Go 1.26:
t := reflect.TypeOf(myValue)
for i := 0; i < t.NumMethod(); i++ {
    method := t.Method(i)
    fmt.Println(method.Name)
}
// После Go 1.26:
t := reflect.TypeOf(myValue)
for method := range t.Methods() {
    fmt.Println(method.Name)
}
// Пример: сериализация структуры
package main

import (
    "fmt"
    "reflect"
    "strings"
)

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"-"` // Исключить из сериализации
}

func serialize(v any) map[string]any {
    result := make(map[string]any)

    rv := reflect.ValueOf(v)
    if rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }

    for field, value := range rv.Fields() {
        // Пропускаем неэкспортируемые поля
        if !field.IsExported() {
            continue
        }

        // Получаем имя из тега или используем имя поля
        jsonTag := field.Tag.Get("json")
        if jsonTag == "-" {
            continue
        }

        name := jsonTag
        if name == "" {
            name = field.Name
        }

        result[name] = value.Interface()
    }

    return result
}

func main() {
    user := User{
        ID:       1,
        Name:     "Alice",
        Email:    "alice@example.com",
        Password: "secret123",
    }

    data := serialize(user)
    fmt.Println(data)
    // map[email:alice@example.com id:1 name:Alice]
    // Password исключён благодаря тегу json:"-"
}
// Пример: валидация структуры
type ValidationError struct {
    Field   string
    Message string
}

func validate(v any) []ValidationError {
    var errors []ValidationError

    rv := reflect.ValueOf(v)
    if rv.Kind() == reflect.Ptr {
        rv = rv.Elem()
    }

    for field, value := range rv.Fields() {
        tag := field.Tag.Get("validate")
        if tag == "" {
            continue
        }

        rules := strings.Split(tag, ",")
        for _, rule := range rules {
            if err := checkRule(field.Name, value, rule); err != nil {
                errors = append(errors, *err)
            }
        }
    }

    return errors
}

func checkRule(fieldName string, value reflect.Value, rule string) *ValidationError {
    switch rule {
    case "required":
        if value.IsZero() {
            return &ValidationError{fieldName, "is required"}
        }
    case "positive":
        if value.Kind() == reflect.Int && value.Int() <= 0 {
            return &ValidationError{fieldName, "must be positive"}
        }
    }
    return nil
}
// Пример: поиск методов с определённой сигнатурой
// Найти все методы, возвращающие error
func findErrorMethods(t reflect.Type) []string {
    var methods []string

    errorType := reflect.TypeOf((*error)(nil)).Elem()

    for method := range t.Methods() {
        mt := method.Type
        // Проверяем последний возвращаемый тип
        for out := range mt.Outs() {
            if out.Implements(errorType) {
                methods = append(methods, method.Name)
                break
            }
        }
    }

    return methods
}
// Пример: обход параметров функции
func describeFn(fn any) {
    t := reflect.TypeOf(fn)
    if t.Kind() != reflect.Func {
        return
    }

    fmt.Println("Input parameters:")
    i := 0
    for in := range t.Ins() {
        fmt.Printf("  %d: %s\n", i, in.String())
        i++
    }

    fmt.Println("Output parameters:")
    i = 0
    for out := range t.Outs() {
        fmt.Printf("  %d: %s\n", i, out.String())
        i++
    }
}

func main() {
    describeFn(func(ctx context.Context, id int) (string, error) {
        return "", nil
    })
    // Input parameters:
    //   0: context.Context
    //   1: int
    // Output parameters:
    //   0: string
    //   1: error
}
// Пример: копирование полей между структурами
func copyMatchingFields(dst, src any) {
    dstVal := reflect.ValueOf(dst).Elem()
    srcVal := reflect.ValueOf(src)
    if srcVal.Kind() == reflect.Ptr {
        srcVal = srcVal.Elem()
    }

    // Собираем исходные поля в map
    srcFields := make(map[string]reflect.Value)
    for field, value := range srcVal.Fields() {
        srcFields[field.Name] = value
    }

    // Копируем совпадающие поля
    for field, dstField := range dstVal.Fields() {
        if srcField, ok := srcFields[field.Name]; ok {
            if dstField.CanSet() && srcField.Type() == field.Type {
                dstField.Set(srcField)
            }
        }
    }
}

Итераторы не создают дополнительных аллокаций по сравнению с индексным доступом. Поэтому их можно использовать без опасений за производительность в критичных участках кода:

// Оба варианта имеют одинаковую производительность
func BenchmarkFieldsIndex(b *testing.B) {
    t := reflect.TypeOf(User{})
    for i := 0; i < b.N; i++ {
        for j := 0; j < t.NumField(); j++ {
            _ = t.Field(j)
        }
    }
}

func BenchmarkFieldsIterator(b *testing.B) {
    t := reflect.TypeOf(User{})
    for i := 0; i < b.N; i++ {
        for range t.Fields() {
        }
    }
}

runtime/metrics — новые метрики scheduler

В Go 1.26 пакет runtime/metrics получил новые метрики для мониторинга планировщика горутин и потоков операционной системы. Это даёт возможность отслеживать состояние scheduler в реальном времени и выявлять проблемы с производительностью.

Метрики горутин

Метрика

Описание

/sched/goroutines:goroutines

Текущее количество горутин

/sched/goroutines/created:goroutines

Общее количество созданных горутин

/sched/goroutines/waiting:goroutines

Горутины в состоянии ожидания

/sched/goroutines/runnable:goroutines

Горутины, готовые к выполнению

/sched/goroutines/running:goroutines

Выполняющиеся горутины

Метрики потоков

Метрика

Описание

/sched/threads:threads

Количество OS-потоков

/sched/threads/created:threads

Общее количество созданных потоков

// Пример: сбор метрик scheduler
package main

import (
    "fmt"
    "runtime/metrics"
    "time"
)

func main() {
    // Описания метрик, которые хотим собирать
    descs := []metrics.Description{
        {Name: "/sched/goroutines:goroutines"},
        {Name: "/sched/goroutines/created:goroutines"},
        {Name: "/sched/goroutines/runnable:goroutines"},
        {Name: "/sched/threads:threads"},
    }

    // Создаём samples для чтения
    samples := make([]metrics.Sample, len(descs))
    for i, desc := range descs {
        samples[i].Name = desc.Name
    }

    // Читаем метрики
    metrics.Read(samples)

    for _, s := range samples {
        fmt.Printf("%s: %v\n", s.Name, s.Value.Uint64())
    }
}
// Пример: мониторинг scheduler в реальном времени
package main

import (
    "context"
    "fmt"
    "runtime/metrics"
    "time"
)

type SchedulerStats struct {
    Goroutines        uint64
    GoroutinesCreated uint64
    Runnable          uint64
    Threads           uint64
}

func collectSchedulerStats() SchedulerStats {
    samples := []metrics.Sample{
        {Name: "/sched/goroutines:goroutines"},
        {Name: "/sched/goroutines/created:goroutines"},
        {Name: "/sched/goroutines/runnable:goroutines"},
        {Name: "/sched/threads:threads"},
    }

    metrics.Read(samples)

    return SchedulerStats{
        Goroutines:        samples[0].Value.Uint64(),
        GoroutinesCreated: samples[1].Value.Uint64(),
        Runnable:          samples[2].Value.Uint64(),
        Threads:           samples[3].Value.Uint64(),
    }
}

func monitorScheduler(ctx context.Context) {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    var prev SchedulerStats

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            stats := collectSchedulerStats()

            created := stats.GoroutinesCreated - prev.GoroutinesCreated
            fmt.Printf("goroutines: %d (+%d), runnable: %d, threads: %d\n",
                stats.Goroutines, created, stats.Runnable, stats.Threads)

            prev = stats
        }
    }
}
// Пример: экспорт в Prometheus
package main

import (
    "net/http"
    "runtime/metrics"

    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    goroutinesGauge = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "go_sched_goroutines",
        Help: "Number of goroutines",
    })

    goroutinesCreatedCounter = prometheus.NewCounter(prometheus.CounterOpts{
        Name: "go_sched_goroutines_created_total",
        Help: "Total number of goroutines created",
    })

    runnableGauge = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "go_sched_goroutines_runnable",
        Help: "Number of runnable goroutines",
    })

    threadsGauge = prometheus.NewGauge(prometheus.GaugeOpts{
        Name: "go_sched_threads",
        Help: "Number of OS threads",
    })
)

func init() {
    prometheus.MustRegister(goroutinesGauge)
    prometheus.MustRegister(goroutinesCreatedCounter)
    prometheus.MustRegister(runnableGauge)
    prometheus.MustRegister(threadsGauge)
}

type schedulerCollector struct {
    samples []metrics.Sample
}

func newSchedulerCollector() *schedulerCollector {
    return &schedulerCollector{
        samples: []metrics.Sample{
            {Name: "/sched/goroutines:goroutines"},
            {Name: "/sched/goroutines/created:goroutines"},
            {Name: "/sched/goroutines/runnable:goroutines"},
            {Name: "/sched/threads:threads"},
        },
    }
}

func (c *schedulerCollector) Collect() {
    metrics.Read(c.samples)

    goroutinesGauge.Set(float64(c.samples[0].Value.Uint64()))
    goroutinesCreatedCounter.Add(float64(c.samples[1].Value.Uint64()))
    runnableGauge.Set(float64(c.samples[2].Value.Uint64()))
    threadsGauge.Set(float64(c.samples[3].Value.Uint64()))
}

func main() {
    collector := newSchedulerCollector()

    // Обновляем метрики перед каждым запросом
    http.Handle("/metrics", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        collector.Collect()
        promhttp.Handler().ServeHTTP(w, r)
    }))

    http.ListenAndServe(":9090", nil)
}
// Пример: обнаружение goroutine leak
package main

import (
    "fmt"
    "runtime/metrics"
    "testing"
    "time"
)

func getGoroutineCount() uint64 {
    samples := []metrics.Sample{
        {Name: "/sched/goroutines:goroutines"},
    }
    metrics.Read(samples)
    return samples[0].Value.Uint64()
}

func TestNoGoroutineLeak(t *testing.T) {
    before := getGoroutineCount()

    // Запускаем тестируемый код
    runSomeCode()

    // Даём время завершиться
    time.Sleep(100 * time.Millisecond)

    after := getGoroutineCount()

    // Допускаем небольшую погрешность
    if after > before+5 {
        t.Errorf("goroutine leak detected: before=%d, after=%d", before, after)
    }
}

func runSomeCode() {
    for i := 0; i < 100; i++ {
        go func(n int) {
            time.Sleep(10 * time.Millisecond)
        }(i)
    }
}
// Пример: алерт при высокой нагрузке
func checkSchedulerHealth() error {
    samples := []metrics.Sample{
        {Name: "/sched/goroutines:goroutines"},
        {Name: "/sched/goroutines/runnable:goroutines"},
        {Name: "/sched/threads:threads"},
    }
    metrics.Read(samples)

    goroutines := samples[0].Value.Uint64()
    runnable := samples[1].Value.Uint64()
    threads := samples[2].Value.Uint64()

    // Слишком много горутин
    if goroutines > 100000 {
        return fmt.Errorf("too many goroutines: %d", goroutines)
    }

    // Слишком много горутин ждут выполнения
    if runnable > 1000 {
        return fmt.Errorf("high runnable queue: %d", runnable)
    }

    // Потоков больше, чем ожидается
    if threads > 1000 {
        return fmt.Errorf("too many OS threads: %d", threads)
    }

    return nil
}

Полный список метрик можно получить программно. Это полезно для автоматического обнаружения новых метрик при обновлении Go:

func listSchedulerMetrics() {
    descs := metrics.All()
    for _, desc := range descs {
        if strings.HasPrefix(desc.Name, "/sched/") {
            fmt.Printf("%s - %s\n", desc.Name, desc.Description)
        }
    }
}

testing — ArtifactDir и флаг -artifacts

В Go 1.26 пакет testing получил метод ArtifactDir() и флаг -artifacts для управления артефактами тестов. Под артефактами понимаются файлы, которые тесты создают для отладки или анализа: скриншоты, дампы данных, логи.

До Go 1.26 тесты, создающие файлы, должны были сами решать, куда их сохранять. Это приводило к засорению файловой системы и потере артефактов:

func TestScreenshot(t *testing.T) {
    // Куда сохранить скриншот?
    // В /tmp? В текущую директорию? В testdata?
    f, _ := os.CreateTemp("", "screenshot-*.png")
    defer f.Close()

    // После теста файл теряется или засоряет систему
    saveScreenshot(f)
}

Новый метод ArtifactDir() возвращает директорию для артефактов теста. Теперь все артефакты имеют единое предсказуемое место хранения:

func (t *T) ArtifactDir() string
func (b *B) ArtifactDir() string
func (f *F) ArtifactDir() string

Флаг -artifacts управляет поведением директории:

Режим

Поведение

Без флага

Ар��ефакты сохраняются во временной директории, удаляемой после теста

С флагом -artifacts

Артефакты сохраняются в -outputdir (или текущей директории) и остаются после теста

Пример: базовое использование

func TestWithArtifacts(t *testing.T) {
    dir := t.ArtifactDir()

    // Создаём файл в директории артефактов
    f, err := os.Create(filepath.Join(dir, "output.txt"))
    if err != nil {
        t.Fatal(err)
    }
    defer f.Close()

    f.WriteString("Test output data")
}

Запуск:

# Артефакты удаляются после теста
go test -v ./...

# Артефакты сохраняются
go test -v -artifacts ./...

# Артефакты сохраняются в указанную директорию
go test -v -artifacts -outputdir=./test-output ./...

Структура директории

При использовании -artifacts создаётся структура:

test-output/
├── TestPackageA/
│   ├── TestOne/
│   │   └── output.txt
│   └── TestTwo/
│       └── screenshot.png
└── TestPackageB/
    └── TestThree/
        └── dump.json

Пример: скриншоты в UI-тестах

func TestLoginPage(t *testing.T) {
    driver := setupWebDriver(t)
    defer driver.Quit()

    driver.Get("http://localhost:8080/login")

    // При ошибке сохраняем скриншот
    if err := driver.FindElement(ByID, "username"); err != nil {
        screenshot, _ := driver.Screenshot()
        path := filepath.Join(t.ArtifactDir(), "error-screenshot.png")
        os.WriteFile(path, screenshot, 0644)
        t.Fatalf("element not found, screenshot saved to %s", path)
    }
}

Пример: сохранение логов

func TestServerIntegration(t *testing.T) {
    // Логи сервера в артефакты
    logPath := filepath.Join(t.ArtifactDir(), "server.log")
    logFile, _ := os.Create(logPath)
    defer logFile.Close()

    server := startServer(t, logFile)
    defer server.Stop()

    // ... тесты ...

    // При падении теста логи останутся для анализа
}

Пример: дампы данных для отладки

func TestDataProcessing(t *testing.T) {
    input := generateTestData()
    result, err := processData(input)

    if err != nil {
        // Сохраняем входные данные для воспроизведения
        dumpPath := filepath.Join(t.ArtifactDir(), "failed-input.json")
        data, _ := json.MarshalIndent(input, "", "  ")
        os.WriteFile(dumpPath, data, 0644)
        t.Fatalf("processing failed: %v, input saved to %s", err, dumpPath)
    }

    if !validateResult(result) {
        // Сохраняем и вход, и выход
        inputPath := filepath.Join(t.ArtifactDir(), "input.json")
        outputPath := filepath.Join(t.ArtifactDir(), "output.json")

        inputData, _ := json.MarshalIndent(input, "", "  ")
        outputData, _ := json.MarshalIndent(result, "", "  ")

        os.WriteFile(inputPath, inputData, 0644)
        os.WriteFile(outputPath, outputData, 0644)

        t.Error("validation failed, data saved for analysis")
    }
}

Логирование при первом вызове

Первый вызов ArtifactDir() с флагом -artifacts записывает путь в лог теста. Это помогает быстро найти артефакты упавшего теста:

=== RUN   TestExample
=== ARTIFACTS TestExample /path/to/output/TestExample
--- PASS: TestExample (0.00s)

Пример: подтесты с артефактами

func TestMatrix(t *testing.T) {
    cases := []struct {
        name  string
        input int
    }{
        {"small", 10},
        {"medium", 100},
        {"large", 1000},
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            // Каждый подтест получает свою директорию
            dir := t.ArtifactDir()
            // /output/TestMatrix/small/
            // /output/TestMatrix/medium/
            // /output/TestMatrix/large/

            result := heavyComputation(tc.input)
            saveBenchmarkReport(filepath.Join(dir, "report.txt"), result)
        })
    }
}

Пример: бенчмарки с профилями

func BenchmarkSort(b *testing.B) {
    data := generateBenchData()

    // Сохраняем CPU-профиль в артефакты
    cpuProfile := filepath.Join(b.ArtifactDir(), "cpu.pprof")
    f, _ := os.Create(cpuProfile)
    pprof.StartCPUProfile(f)
    defer pprof.StopCPUProfile()
    defer f.Close()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sort.Ints(data)
    }
}

В CI можно сохранять артефакты для анализа упавших тестов. Это особенно полезно для UI-тестов и интеграционных тестов, где скриншоты и логи помогают быстро понять причину сбоя:

# GitHub Actions
- name: Run tests
  run: go test -v -artifacts -outputdir=./test-artifacts ./...

- name: Upload test artifacts
  if: failure()
  uses: actions/upload-artifact@v3
  with:
    name: test-artifacts
    path: test-artifacts/

testing/cryptotest — SetGlobalRandom

В Go 1.26 пакет testing/cryptotest получил функцию SetGlobalRandom, которая позволяет подменить глобальный источник криптографической случайности на детерминированный. Это делает тесты криптографического кода воспроизводимыми.

Криптографические функции по умолчанию используют crypto/rand.Reader. Это делает тесты недетерминированными, что затрудняет проверку конкретных значений и воспроизведение ошибок:

func SetGlobalRandom(r io.Reader) func()

Функция устанавливает глобальный источник случайности для всех криптографических операций и возвращает функцию для восстановления оригинального состояния.

// Пример: детерминированные тесты
package crypto_test

import (
    "bytes"
    "crypto/rand"
    "crypto/rsa"
    "testing"
    "testing/cryptotest"
)

func TestDeterministicKey(t *testing.T) {
    // Фиксированный источник случайности
    seed := bytes.Repeat([]byte{0x42}, 256)
    restore := cryptotest.SetGlobalRandom(bytes.NewReader(seed))
    defer restore()

    key1, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        t.Fatal(err)
    }

    // Тот же seed — тот же ключ
    restore()
    restore = cryptotest.SetGlobalRandom(bytes.NewReader(seed))
    defer restore()

    key2, err := rsa.GenerateKey(rand.Reader, 2048)
    if err != nil {
        t.Fatal(err)
    }

    if !key1.Equal(key2) {
        t.Error("expected identical keys with same seed")
    }
}
// Пример: тестирование с известными значениями
func TestKnownVector(t *testing.T) {
    // Известный вектор из спецификации
    knownRandom := []byte{
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
        // ... остальные байты ...
    }

    restore := cryptotest.SetGlobalRandom(bytes.NewReader(knownRandom))
    defer restore()

    result := performCryptoOperation()

    expected := []byte{0xAB, 0xCD, 0xEF} // Известный результат
    if !bytes.Equal(result, expected) {
        t.Errorf("got %x, want %x", result, expected)
    }
}
// Пример: воспроизводимые тесты с seed
func TestReproducible(t *testing.T) {
    // Seed записывается в лог для воспроизведения
    seed := make([]byte, 64)
    if _, err := rand.Read(seed); err != nil {
        t.Fatal(err)
    }
    t.Logf("seed: %x", seed)

    // Детерминированный PRNG из seed
    prng := newDeterministicPRNG(seed)
    restore := cryptotest.SetGlobalRandom(prng)
    defer restore()

    // Тесты теперь воспроизводимы по seed из лога
    runCryptoTests(t)
}

type deterministicPRNG struct {
    state []byte
    pos   int
}

func newDeterministicPRNG(seed []byte) *deterministicPRNG {
    // Растягиваем seed с помощью SHA256
    expanded := make([]byte, 1<<20) // 1 MB
    h := sha256.New()
    h.Write(seed)
    copy(expanded, h.Sum(nil))

    for i := 32; i < len(expanded); i += 32 {
        h.Reset()
        h.Write(expanded[i-32 : i])
        copy(expanded[i:], h.Sum(nil))
    }

    return &deterministicPRNG{state: expanded}
}

func (p *deterministicPRNG) Read(b []byte) (int, error) {
    n := copy(b, p.state[p.pos:])
    p.pos += n
    return n, nil
}

SetGlobalRandom влияет на crypto/rand.Reader и crypto/rand.Read, а также на внутренние источники случайности в пакетах crypto/.... В результате генерация ключей в crypto/rsa, crypto/ecdsa, crypto/ed25519, а также операции шифрования и подписи становятся детерминированными.

func TestAffectedOperations(t *testing.T) {
    seed := bytes.Repeat([]byte{0x42}, 1024)
    restore := cryptotest.SetGlobalRandom(bytes.NewReader(seed))
    defer restore()

    // Все эти операции теперь детерминированы:
    rand.Read(make([]byte, 16))                          // crypto/rand
    rsa.GenerateKey(rand.Reader, 2048)                   // crypto/rsa
    ecdsa.GenerateKey(elliptic.P256(), rand.Reader)      // crypto/ecdsa
    ed25519.GenerateKey(rand.Reader)                     // crypto/ed25519
}

Функцию можно использовать только для тестов. Никогда не применяйте её в production-коде:

// Неправильно — никогда так не делайте!
func main() {
    cryptotest.SetGlobalRandom(fixedReader) // Критическая уязвимость!
}

Также важно обеспечить достаточный объём данных:

// Неправильно — данных не хватит
cryptotest.SetGlobalRandom(bytes.NewReader([]byte{1, 2, 3}))
rsa.GenerateKey(rand.Reader, 4096) // Ошибка: EOF

// Правильно — достаточно данных
cryptotest.SetGlobalRandom(bytes.NewReader(make([]byte, 4096)))

Наконец, обязательно восстанавливайте состояние после теста:

func TestA(t *testing.T) {
    restore := cryptotest.SetGlobalRandom(myReader)
    defer restore() // Обязательно восстанавливаем!
}

func TestB(t *testing.T) {
    // Если TestA не вызвал restore, TestB получит сломанный rand
}
// Пример: fuzzing с воспроизводимыми случаями
func FuzzEncryption(f *testing.F) {
    f.Add([]byte("test"), []byte{0x01, 0x02, 0x03})

    f.Fuzz(func(t *testing.T, data []byte, seed []byte) {
        if len(seed) < 64 {
            return
        }

        // Воспроизводимая случайность для каждого входа
        restore := cryptotest.SetGlobalRandom(bytes.NewReader(seed))
        defer restore()

        // Тестируем криптографическую операцию
        encrypted, err := encrypt(data)
        if err != nil {
            return
        }

        // Сбрасываем seed для расшифровки
        restore()
        restore = cryptotest.SetGlobalRandom(bytes.NewReader(seed))
        defer restore()

        decrypted, err := decrypt(encrypted)
        if err != nil {
            t.Fatalf("decrypt failed with seed %x: %v", seed, err)
        }

        if !bytes.Equal(data, decrypted) {
            t.Fatalf("roundtrip failed with seed %x", seed)
        }
    })
}

time — удаление GODEBUG asynctimerchan

В Go 1.26 анонсировано удаление настройки GODEBUG=asynctimerchan в Go 1.27. Эта настройка позволяла вернуть старое поведение каналов таймеров. Это означает, что код, полагающийся на буферизацию канала, нужно обновить до выхода Go 1.27.

В Go 1.23 поведение каналов time.Timer и time.Ticker изменилось:

Версия

Поведение канала

Go 1.22 и ранее

Буферизованный канал (размер 1)

Go 1.23+

Небуферизованный (синхронный) канал

Для обратной совместимости добавили GODEBUG=asynctimerchan=1.

В go 1.26 настройка asynctimerchan всё ещё работает, но помечена как deprecated. В Go 1.27 она будет полностью удалена.

Различия в поведении

// Буферизованный канал (старое поведение)
// Go 1.22 и asynctimerchan=1
timer := time.NewTimer(100 * time.Millisecond)

// Можно остановить и не читать из канала
timer.Stop()
// Событие может остаться в буфере

// При Reset событие из буфера может «просочиться»
timer.Reset(200 * time.Millisecond)
select {
case <-timer.C:
    // Может сработать сразу из-за старого события в буфере!
case <-time.After(300 * time.Millisecond):
}
// Небуферизованный канал (новое поведение)
// Go 1.23+ по умолчанию
timer := time.NewTimer(100 * time.Millisecond)

timer.Stop()
// Канал пуст, событие не буферизуется

timer.Reset(200 * time.Millisecond)
select {
case <-timer.C:
    // Сработает ровно через 200ms
case <-time.After(300 * time.Millisecond):
}

Код, который полагался на буферизацию, может зависнуть в Go 1.23 и новее:

// Проблемный код — работал в Go 1.22
func oldPattern() {
    timer := time.NewTimer(time.Second)

    // Делаем что-то, что может занять больше секунды
    doWork()

    // Пытаемся остановить таймер
    if !timer.Stop() {
        // В Go 1.22 событие могло быть в буфере
        // В Go 1.23+ канал пуст после Stop
        <-timer.C // Может заблокироваться навсегда!
    }
}

Вместо этого используйте select для обработки событий таймера:

// Правильный код работает во всех версиях
func correctPattern() {
    timer := time.NewTimer(time.Second)
    defer timer.Stop()

    select {
    case <-timer.C:
        // Таймер сработал
        handleTimeout()
    case result := <-workDone:
        // Работа завершилась до таймаута
        handleResult(result)
    }
}

Можно протестировать код без asynctimerchan:

# Явно отключаем asynctimerchan
GODEBUG=asynctimerchan=0 go test ./...
// Пример: безопасный таймаут
func safeTimeout(d time.Duration, work func() error) error {
    done := make(chan error, 1)

    go func() {
        done <- work()
    }()

    timer := time.NewTimer(d)
    defer timer.Stop()

    select {
    case err := <-done:
        return err
    case <-timer.C:
        return errors.New("timeout")
    }
}

Порты

Go 1.26 продолжает эволюцию платформенной поддержки. Одни порты получают новые возможности, другие уходят в историю. Это нормальный процесс для зрелого языка, поскольку ресурсы команды ограничены и перераспределяются туда, где они приносят больше пользы реальным пользователям.

Главные изменения касаются трёх платформ. macOS 12 Monterey остаётся поддерживаемой, однако это последняя версия. Go 1.27 потребует минимум Ventura. FreeBSD на RISC-V помечен как broken из-за нерешённых проблем совместимости. 32-битный Windows ARM окончательно удалён после предупреждения в Go 1.24.

Вместе с тем есть и позитивные новости. Linux на RISC-V получил поддержку race detector, что открывает возможность полноценной разработки многопоточных приложений на этой архитектуре. Порт s390x теперь использует регистры для передачи аргументов функций, что ускоряет вызовы и снижает нагрузку на стек. WebAssembly стал требовать инструкции sign extension и non-trapping float-to-int, которые стандартизированы с Wasm 2.0. Это позволяет генерировать более компактный и быстрый код.

Darwin (macOS)

Go 1.26 станет последним релизом с поддержкой macOS 12 Monterey. Начиная с Go 1.27, минимальной версией станет macOS 13 Ventura.

Такой подход является стандартной практикой для Go. Поддержка операционной системы прекращается примерно через год после того, как Apple перестаёт выпускать для неё обновления безопасности. Monterey вышел в октябре 2021 года и уже не получает патчей. Поэтому поддержание совместимости с ним требует всё больше усилий без ощутимой пользы.

Для большинства разработчиков это изменение пройдёт незаметно, поскольку Ventura и более новые версии уже установлены на подавляющем большинстве Mac. Однако если ты поддерживаешь старые машины или используешь CI с фиксированными образами, стоит запланировать обновление заранее.

# Проверить версию macOS
sw_vers -productVersion

# Минимум для Go 1.27
# 13.0 (Ventura) или новее

Никаких изменений в коде не требуется. Программы, собранные Go 1.26, продолжат работать на всех поддерживаемых версиях macOS. Ограничение касается только сборки самого Go и запуска компилятора.

FreeBSD

Порт freebsd/riscv64 помечен как broken в Go 1.26. Это означает, что сборка программ для FreeBSD на архитектуре RISC-V временно недоступна.

# Эта комбинация больше не работает
GOOS=freebsd GOARCH=riscv64 go build

Причина в нерешённых проблемах совместимости, описанных в issue #76475. RISC-V на FreeBSD остаётся экспериментальной платформой, и текущее состояние порта не соответствует стандартам качества Go. Команда предпочла честно пометить порт как сломанный, чем оставлять пользователей с непредсказуемым поведением.

Статус broken означает, что порт официально не поддерживается, но код не удалён из репозитория. Благодаря этому, если проблемы будут решены, поддержка может вернуться в следующих версиях без необходимости писать код заново. На данный момент для RISC-V рекомендуется использовать Linux, поскольку там порт стабилен и даже получил поддержку race detector в Go 1.26.

Порты FreeBSD для других архитектур (amd64, arm64, 386, arm) продолжают работать без изменений.

Windows

32-битный порт Windows ARM (GOOS=windows GOARCH=arm) удалён в Go 1.26. Это было анонсировано ещё в Go 1.24, когда порт был помечен как broken.

# Больше не поддерживается
GOOS=windows GOARCH=arm go build  # ошибка

Причина удаления практическая. 32-битные ARM-устройства под Windows крайне редки. Microsoft прекратил поддержку Windows RT (единственной массовой платформы с этой комбинацией) ещё в 2023 году. Поддерживать порт без реального использования и тестирования нецелесообразно, поскольку это отвлекает ресурсы от более востребованных платформ.

В то же время 64-битный windows/arm64 остаётся полностью поддерживаемым. Более того, в Go 1.26 он получил важное улучшение в виде internal linking для cgo-программ.

# Windows ARM64 с internal linking
GOOS=windows GOARCH=arm64 go build -ldflags="-linkmode=internal" ./cmd/myapp

Раньше cgo-программы на windows/arm64 требовали внешнего линкера (GCC или MSVC). Теперь встроенного линкера Go достаточно для большинства случаев. На практике это упрощает сборку и устраняет зависимость от тулчейна C. Особенно это важно для CI/CD, где установка полноценного компилятора C может быть затруднительна.

Заключение

Go 1.26 стал релизом, в котором сошлись результаты нескольких лет работы над разными направлениями. Это не просто набор отдельных улучшений, а согласованное развитие языка, рантайма и стандартной библиотеки.

Главные изменения

Green Tea GC завершает переход на новую архитектуру сборщика мусора. От экспериментального статуса в Go 1.25 до production-ready в Go 1.26. Страничное сканирование с векторным ускорением даёт измеримое улучшение производительности. Для большинства приложений переход происходит автоматически. На практике это означает меньше пауз и более предсказуемую работу приложений под нагрузкой.

Постквантовая криптография входит в стандартную библиотеку и становится доступной без подключения сторонних зависимостей. ML-KEM включён в TLS по умолчанию, HPKE доступен через новый пакет crypto/hpke. Благодаря этому Go-приложения получают защиту от угроз квантовых компьютеров прямо из коробки.

Расширение new() решает мелкую, но давно раздражавшую проблему с созданием указателей на примитивные значения. Больше не нужны вспомогательные функции вроде ptr.Of(value). Это упрощает код и устраняет целый класс utility-функций из проектов.

Что требует внимания

Несколько изменений могут потребовать действий. Поддержка macOS 12 Monterey завершается, поэтому если в твоём окружении есть эти системы, Go 1.26 станет последним релизом с их поддержкой. Порт Windows ARM 32-bit удалён полностью, и при наличии таких устройств необходима миграция на arm64. Также стоит проверить свои скрипты на использование GODEBUG-настроек, поскольку ряд из них уйдёт в Go 1.27.

Рекомендации по переходу

Для безопасного перехода на новую версию стоит начать с обновления тестового окружения до Go 1.26. После этого запусти полный тестовый набор и бенчмарки, чтобы убедиться в отсутствии регрессий. Проверь использование deprecated API в своём коде. Особое внимание обрати на новые возможности errors.AsType и reflect-итераторов, которые могут упростить существующий код.

Что дальше

В Go 1.27 ожидается удаление GODEBUG-настроек, помеченных как deprecated, а также расширение simd/archsimd на другие архитектуры помимо AMD64. Стабилизация goroutine leak profiles позволит использовать их без ограничений в production. Опция GOEXPERIMENT=nogreenteagc будет удалена после того, как новый GC полностью зарекомендует себя.

Политика обратной совместимости сохраняется. Существующий код продолжает работать, deprecated API удаляются через несколько релизов с предупреждениями. Это даёт достаточно времени для плавной миграции.

Спасибо за много часов жизни потраченных на статью, надеюсь материал был полезен :-)

Кстати, веду небольшой дневник в телеге, вдруг кому интересно...