Go 1.26 вышел, пройдемся по всем изменениям…
- пятница, 13 февраля 2026 г. в 00:00:06

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, поэтому стоит проверить код на использование устаревших флагов.
В процессе...

В 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 наконец отражает эту практику в настройках по умолчанию. В результате разработчику не нужно каждый раз переключать вид вручную.
Все три изменения объединяет общая идея: убрать лишнее и сделать инструмен��ы такими, какими их и так уже используют.
Начиная с 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 полностью переработана. Теперь она использует тот же фреймворк анализа (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 с набором анализаторов для модернизации кода. Эти анализаторы помогают переводить проекты на современные конструкции языка, появившиеся в последних версиях Go. Рассмотрим новые из них:
// До func ptrTo(x int) *int { return &x } // После func ptrTo(x int) *int { return new(x) }
Доступно с Go 1.26. Новый синтаксис делает намерение явным и избавляет от необходимости создавать промежуточную переменную.
// До 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.
Веб-интерфейс pprof (запускаемый с флагом -http) теперь открывает flame graph по умолчанию. Раньше по умолчанию показывался граф вызовов, и разработчикам приходилось вручную переключаться на более удобное представление.
# Открыть pprof с веб-интерфейсом go tool pprof -http=:8080 cpu.prof
При переходе по адресу http://localhost:8080 ты сразу увидишь flame graph вместо графа вызовов. Граф вызовов при этом никуда не делся. Чтобы его открыть, можно использовать меню View → Graph или перейти напрямую по URL /ui/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. Главное из них: Green Tea GC становится сборщиком мусора по умолчанию. Этот алгоритм был экспериментальным в Go 1.25 и за это время показал себя достаточно стабильным для production-использования в Google и других крупных компаниях. На практике это означает, что большинство приложений получат снижение нагрузки на CPU от сборки мусора без каких-либо изменений в коде.
Green Tea переосмысливает подход к сканированию памяти. Традиционный GC обходил объекты по указателям, прыгая между далёкими адресами и постоянно промахиваясь по кэшу процессора. Новый алгоритм работает со страницами памяти целиком, обрабатывая объекты последовательно. Благодаря этому кардинально улучшается работа с кэшем, а предсказуемый порядок доступа позволяет использовать векторные инструкции для дополнительного ускорения.
Помимо нового GC, в этом релизе улучшена производительность cgo-вызовов (примерно на 30%) и аллокации мелких объектов (до 37% быстрее для объектов менее 512 байт). Оба изменения работают автоматически и не требуют модификации кода. Для приложений с активным использованием FFI или частыми аллокациями это даёт заметный прирост производительности.
Отдельного внимания заслуживает экспериментальная функция обнаружения утечек горутин. Она основана на исследовании Vlad Saioc из Uber и использует фазу маркировки GC для поиска заблокированных горутин, которые никогда не смогут продолжить выполнение. Это позволяет находить проблемы, которые раньше требовали специальных инструментов или ручного анализа.
В результате все изменения этого раздела направлены на одну цель: сделать Go ещё эффективнее на современном железе без дополнительных усилий со стороны разработчика.
Традиционный сборщик мусора 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 по умолчанию и получает векторное ускорение.
The Green Tea Garbage Collector - официальная статья в блоге Go.
Issue #73581 - обсуждение.
Green Tea GC открывает возможность, которая была недоступна традиционному сборщику мусора: использование векторных инструкций процессора. Когда объекты обрабатываются последовательно в пределах страницы, данные становятся предсказуемыми, и это позволяет применить SIMD. На практике это означает, что одна инструкция обрабатывает сразу несколько элементов данных вместо последовательного цикла.
В Go 1.26 реализовано векторное ускорение сканирования на процессорах с поддержкой AVX-512. Это даёт дополнительные ~10% снижения нагрузки на GC поверх базового улучшени�� от Green Tea. Для приложений с высоким давлением на сборщик мусора суммарное улучшение может достигать 50%.
При традиционном сканировании GC обрабатывает объекты по одному. Для каждого объекта:
Загрузить метаданные объекта
Проверить, какие слова содержат указатели
Для каждого указателя — проверить и отметить целевой объект
Это работает, но не использует возможности современных процессоров обрабатывать несколько элементов данных одной инструкцией.
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.
Базовый 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.
cgo documentation (документация cgo)
Компилятор 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) недостижим из корней. Затем горутина, заблокированная на этом канале, помечается как утечка. Наконец, информация становится доступна через профиль.
Ключевая идея: если канал недостижим, никто никогда не сможет записать в него или прочитать из него. Поэтому горутина, ожидающая на таком канале, заблокирована навсегда и никогда не сможет продолжить выполнение.
import ( "net/http" _ "net/http/pprof" ) func main() { go http.ListenAndServe(":6060", nil) // ... }
Профиль доступен по адресу:
http://localhost:6060/debug/pprof/goroutineleak
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
Профиль обнаруживает горутины, заблокированные на:
Примитив | Пример |
|---|---|
Небуферизированный канал |
|
Буферизированный канал (полный/пустой) |
|
sync.Mutex |
|
sync.RWMutex |
|
sync.Cond |
|
Профиль спроектирован с нулевым 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 без изменений кода. При этом он точно указывает на место утечки, что значительно упрощает диагностику.
Proposal #74609 (исходный proposal)
Issue #75280 (реализация как GOEXPERIMENT)
Saioc et al. Paper (научная публикация)
uber-go/goleak (инструмент от Uber)

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), эти изменения стоит учесть. Для остальных разработчиков всё работает как раньше.
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 (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 | Описание | Размер публичного ключа |
|---|---|---|
| Чистый ML-KEM-768 | 1184 байт |
| Чистый ML-KEM-1024 | 1568 байт |
| Гибрид ML-KEM-768 + X25519 | 1216 байт |
| Гибрид ML-KEM-768 + P-256 | 1249 байт |
| Гибрид 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 для защиты соединений, симметричное шифрование для больших объёмов данных и подпись данных, поскольку протокол не аутентифицирует отправителя. Каждый из этих сценариев требует специализированных инструментов.
RFC 9180: спецификация HPKE
crypto/hpke: документация пакета
draft-ietf-hpke-pq: постквантовые расширения
Экспериментальный пакет 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)
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 решает проблему надёжного удаления криптографических ключей из памяти. Это критически важно для 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 выполняет несколько действий:
Запоминает состояние стека и кучи
Выполняет переданную функцию
Очищает регистры перед возвратом
Затирает стековый фрейм функции
Помечает новые аллокации для затирания при 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 прозрачно для пользователя.
Issue #21865: proposal и обсуждение
runtime/secret: документация пакета
Go feature: Secret mode: подробный разбор
Пакет 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.
Issue #73627: proposal для derandomized encapsulation
crypto/mlkem: основной пакет ML-KEM
FIPS 203: спецификация ML-KEM

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 для нового кода.
Наберись терпения, это самый большой блок... Дальше про каждое изменение отдельно.
В 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 |
|---|---|---|
Ошибка при нехватке данных | Нет, возвращает что есть | Да, возвращает |
Может блокироваться | Нет | Да, при чтении из сети |
Внутренний буфер | Общий с Buffer | Отдельный буфер чтения |
bytes.Buffer.Peek — документация
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 |
|---|---|---|
| Да | Нет |
| Да | Нет |
| Да (через | Да |
| Да (через | Да |
ECDH-ключи не реализуют эти интерфейсы напрямую, потому что ECDH это ключевой обмен, а не инкапсуляция. Однако пакет crypto/hpke оборачивает ECDH в 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.Encapsulator — документация
crypto.Decapsulator — документация
ML-KEM (FIPS 203) — стандарт постквантового KEM
В 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(¶ms, 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/dsa — документация пакета
testing/cryptotest.SetGlobalRandom — документация для тестирования
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/ecdh — документация пакета
crypto/ecdh.KeyExchanger — интерфейс
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, теперь игнорируют этот параметр:
Функция | Изменение |
|---|---|
|
|
|
|
|
|
|
|
Функции используют внутренний криптографически стойкий генератор. Это означает, что можно безопасно передавать 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 — рекомендуемый пакет для обмена ключами
В 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/ed25519 — документация пакета
RFC 8032 — спецификация Ed25519
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/fips140 — документация пакета
FIPS 140-3 — стандарт NIST
Go FIPS 140-3 — документация Go
В 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() и 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/mlkem — документация пакета
FIPS 203 (ML-KEM) — стандарт NIST
В 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/rand.Prime — документация функции
testing/cryptotest.SetGlobalRandom — для тестирования
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, теперь игнорируют этот параметр:
Функция | Изменение |
|---|---|
|
|
|
|
|
|
|
|
Метод 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/rsa — документация пакета
RFC 8017 — PKCS#1 v2.2 (OAEP)
Bleichenbacher attack — описание уязвимости PKCS#1 v1.5
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/subtle — документация пакета
Data Operand Independent Timing — руководство Intel
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 — документация пакета
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.
RFC 8446, Section 4.1.4 — HelloRetryRequest в TLS 1.3
crypto/tls — документация пакета
Go 1.26 помечает несколько GODEBUG-настроек TLS как устаревшие. В Go 1.27 они будут удалены, и устаревшие криптографические режимы станут недоступны.
Пять настроек перестанут работать в следующей версии Go:
Настройка | Что делает | Версия добавления |
|---|---|---|
| Разрешает ExportKeyingMaterial без EMS или TLS 1.3 | Go 1.22 |
| Включает RSA-only обмен ключами | Go 1.22 |
| Разрешает TLS 1.0/1.1 на сервере | Go 1.22 |
| Включает 3DES в cipher suites | Go 1.23 |
| Контролирует заполнение Certificate.Leaf | Go 1.23 |
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) }
RSA key exchange передаёт pre-master secret, зашифрованный открытым ключом сервера. Если приватный ключ сервера скомпрометирован, злоумышленник расшифрует весь записанный трафик. Это происходит из-за отсутствия forward secrecy, когда один скомпрометированный ключ раскрывает все прошлые соединения.
Сейчас:
GODEBUG=tlsrsakex=1 ./myapp
После Go 1.27 останутся только ECDHE и другие механизмы с forward secrecy. На практике это не вызовет проблем, потому что все современные TLS-реализации поддерживают ECDHE.
Миграция не требуется, если твои серверы и клиенты обновлены за последние 10 лет.
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")) }
3DES (Triple DES) использует 64-битные блоки. При объёме данных около 32 ГБ становится возможной атака Sweet32 на восстановление открытого текста.
Сейчас:
GODEBUG=tls3des=1 ./myapp
После Go 1.27 3DES будет полностью удалён из cipher suites. Современные альтернативы, такие как AES-GCM, работают значительно быстрее и обеспечивают лучшую защиту.
Настройка x509keypairleaf контролирует, заполняют ли X509KeyPair и LoadX509KeyPair поле Certificate.Leaf распарсенным сертификатом.
Сейчас можно отключить:
GODEBUG=x509keypairleaf=0 ./myapp
После Go 1.27 поле Certificate.Leaf будет заполняться всегда. Это ускоряет работу, потому что сертификат не нужно парсить повторно при каждом handshake. В результате приложения получат небольшой прирост производительности без каких-либо изменений в коде.
Проверь, используешь ли ты эти настройки:
grep -r "GODEBUG" . | grep -E "(tlsunsafeekm|tlsrsakex|tls10server|tls3des|x509keypairleaf)"
Убедитесь, что клиенты поддерживают 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") }
Обновите мониторинг для отслеживания соединений с устаревшими протоколами.
Если тебе нужна поддержка древних клиентов, рассмотри TLS-терминирующий прокси (nginx, HAProxy, Envoy) с отдельной настройкой для legacy-трафика.
Go 1.22 Release Notes — первое добавление GODEBUG
TLS 1.0 Deprecation — RFC 8996
Sweet32 Attack — атака на 64-битные блочные шифры
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
Возвращает имя 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
Возвращает 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
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 |
RFC 5280 — X.509 PKI Certificate and CRL Profile
crypto/x509 — документация пакета
OID Repository — база данных OID
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 |
|---|---|---|
Определение |
| Непрозрачный тип |
Парсинг | Прямой доступ к элементам | Через методы |
Строковое представление |
|
|
Сравнение |
|
|
Использование | Низкоуровневый 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) }
crypto/x509 — документация пакета
encoding/asn1 — документация ASN.1
RFC 5280 — X.509 PKI Certificate
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 изменение прозрачно. Компилятор и линкер используют эти константы внутренне, поэтому разработчику не нужно ничего менять в коде.
debug/elf — документация пакета
LoongArch ELF psABI — спецификация ABI
Go на LoongArch — страница в Go Wiki

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, а не ошибку компиляции.
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.
errors.AsType — документация
Proposal #51945 — обсуждение
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 по-прежнему используют обычный путь форматирования.
fmt.Errorf — документация
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.Directive — документация
Proposal #68021 — обсуждение
Go Doc Comments — синтаксис директив
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 с многострочными строками.
ast.BasicLit — документация
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"
token.File.End — документация
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.
types.Alias — документация
types.Unalias — разворачивание алиасов
Proposal: Type Aliases — оригинальный proposal
В 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
В 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 обычно не следует. Поэтому для большинства проектов обновление пройдёт незаметно.
io.ReadAll — документация
В 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
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...)) }
slog.NewMultiHandler — документация
slog.MultiHandler — тип
В 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.Dialer.DialTCP — документация
net.Dialer.DialUDP — документация
net.Dialer.DialUnix — документация
net.Dialer.DialIP — документация
В 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, // Увеличенный таймаут для ожидания } }
http.HTTP2Config — документация
В 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 полезен для низкоуровневых задач: реализации прокси, мониторинга трафика и тестирования.
http.Transport.NewClientConn — документация
http.ClientConn — тип
В 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 и в интеграционных тестах, где модификация кода нежелательна. Также она упрощает тестирование мультидоменных приложений и проверку поведения редиректов между доменами.
httptest.Server.Client — документация
В 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) { // Используется этот }, }
httputil.ReverseProxy.Rewrite — документация
httputil.ProxyRequest — тип
В 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
netip.Prefix.Compare — документация
В 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 отклоняются явно.
url.Parse — документация
В 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.Process.WithHandle — документация
pidfd_open(2) — man-страница pidfd
В 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.OpenFile — документация
CreateFile — документация Windows API
В 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") }
signal.NotifyContext — документация
context.Cause — получение причины отмены
В 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() { } } }
reflect.Type.Fields — документация
reflect.Value.Fields — документация
В Go 1.26 пакет runtime/metrics получил новые метрики для мониторинга планировщика горутин и потоков операционной системы. Это даёт возможность отслеживать состояние scheduler в реальном времени и выявлять проблемы с производительностью.
Метрика | Описание |
|---|---|
| Текущее количество горутин |
| Общее количество созданных горутин |
| Горутины в состоянии ожидания |
| Горутины, готовые к выполнению |
| Выполняющиеся горутины |
Метрика | Описание |
|---|---|
| Количество OS-потоков |
| Общее количество созданных потоков |
// Пример: сбор метрик 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) } } }
runtime/metrics — документация
runtime/metrics.Read — чтение метрик
В 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 управляет поведением директории:
Режим | Поведение |
|---|---|
Без флага | Ар��ефакты сохраняются во временной директории, удаляемой после теста |
С флагом | Артефакты сохраняются в |
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
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.T.ArtifactDir — документация
testing flags — флаги тестирования
В 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) } }) }
testing/cryptotest.SetGlobalRandom — документация
crypto/rand — пакет криптографической случайности
В 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") } }
time.Timer — документация
Go 1.23 Release Notes: Timers — описание изменений
GODEBUG — документация по GODEBUG

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. Это позволяет генерировать более компактный и быстрый код.
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/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) продолжают работать без изменений.
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 удаляются через несколько релизов с предупреждениями. Это даёт достаточно времени для плавной миграции.
Спасибо за много часов жизни потраченных на статью, надеюсь материал был полезен :-)
Кстати, веду небольшой дневник в телеге, вдруг кому интересно...