Что нового в Go 1.26
- вторник, 20 января 2026 г. в 00:00:10

Go 1.26 выходит в феврале, и на мой взгляд это один из самых насыщенных релизов за всю историю языка. Давай разберёмся что нас ждёт, от изменений в языке до оптимизаций runtime и новых инструментов. Статья основана на официальных release notes и исходном коде Go. Это не исчерпывающий список, за полной информацией обращайся к официальной документации.
Новая функция errors.AsType это дженерик-версия errors.As:
// Go 1.13+
func As(err error, target any) bool
// Go 1.26+
func AsType[E error](err error) (E, bool)Она типобезопасна и проще в использовании:
// Старый подход с errors.As
var netErr *NetworkError
if errors.As(err, &netErr) {
log.Printf("сеть недоступна: %s", netErr.Address)
}
// Новый подход с errors.AsType
if netErr, ok := errors.AsType[*NetworkError](err); ok {
log.Printf("сеть недоступна: %s", netErr.Address)
}AsType особенно удобен при проверке нескольких типов ошибок. Код становится короче, а переменные остаются в своей области видимости:
if connErr, ok := errors.AsType[*ConnectionRefusedError](err); ok {
log.Printf("соединение отклонено хостом %s:%d", connErr.Host, connErr.Port)
} else if authErr, ok := errors.AsType[*AuthenticationError](err); ok {
log.Printf("неверные учётные данные пользователя: %s", authErr.Username)
} else {
log.Println("произошла непредвиденная ошибка")
}Ещё одна проблема As использование рефлексии, которая может вызвать панику при неправильном использовании:
// Ошибка: передаём не указатель на указатель
var netErr NetworkError
if errors.As(err, &netErr) {
log.Printf("сеть недоступна: %s", netErr.Address)
}
// panic: errors: *target must be interface or implement errorAsType не паникует, она выдаёт понятную ошибку компиляции:
if netErr, ok := errors.AsType[NetworkError](err); ok {
log.Printf("сеть недоступна: %s", netErr.Address)
}
// ./main.go:24:32: NetworkError does not satisfy error (method Error has pointer receiver)AsType не использует reflect, работает быстрее и потребляет меньше памяти.
Поскольку AsType справляется со всеми задачами As, рекомендуется использовать её в новом коде.

На мой взгляд это одно из самых удобный изменений релиза. До Go 1.26 встроенная функция new работала только с типами:
counter := new(int)
*counter = 42
fmt.Println(*counter) // 42Теперь new принимает и выражения:
// Создаём указатель на int со значением 42
counter := new(42)
fmt.Println(*counter) // 42Если аргумент expr - это выражение типа T, то new(expr) выделяет переменную типа T, инициализирует её значением expr и возвращает указатель *T. Особенно это удобно при работе со структурами, где указатели обозначают опциональные поля для JSON или Protobuf:
type Product struct {
Title string `json:"title"`
InStock *bool `json:"in_stock,omitempty"`
}
item := Product{Title: "Ноутбук", InStock: new(true)}
payload, _ := json.Marshal(item)
fmt.Println(string(payload))
// { "title":"Ноутбук", "in_stock":true }Работает и с составными литералами:
prices := new([]float64{199.99, 299.99, 399.99})
fmt.Println(*prices) // [199.99 299.99 399.99]
type Settings struct{ Verbose bool }
opts := new(Settings{Verbose: true})
fmt.Println(*opts) // {true}И с вызовами функций:
fetchVersion := func() string { return "v1.26.0" }
version := new(fetchVersion())
fmt.Println(*version) // v1.26.0Передавать nil по-прежнему нельзя:
ref := new(nil) // ошибка компиляции
Новый сборщик мусора (впервые появился как экспериментальный в 1.25) спроектирован для более эффективной работы с памятью на современных многоядерных процессорах.
Традиционный GC в Go работает как с графом, где объекты это узлы, а указатели это рёбра, не учитывая физическое расположение в памяти. Сканер прыгает между отдалёнными участками памяти, вызывая частые промахи кэша.
В результате процессор тратит слишком много времени на ожидание данных из памяти. Более 35% времени сканирования уходит на простой в ожидании доступа к памяти. С ростом числа ядер эта проблема усугубляется.
Green Tea смещает фокус с процессоро-центричного подхода на память-ориентированный. Вместо сканирования отдельных объектов он сканирует память непрерывными блоками по 8 Кб, называемыми span. Алгоритм фокусируется на малых объектах (до 512 байт), так как они наиболее распространены и сложнее всего поддаются эффективному сканированию. Каждый span делится на равные слоты в зависимости от назначенного класса размера, и содержит только объекты этого класса. Например, если span назначен классу 32 байта, весь блок делится на 32-байтные слоты. Благодаря фиксированной компоновке GC может быстро находить метаданные объекта простой арифметикой адресов. Когда алгоритм находит объект для сканирования, он помечает его расположение в span, но не сканирует сразу. Вместо этого он ждёт, пока в том же span накопится несколько объектов для сканирования. Затем GC обрабатывает их все разом - это намного быстрее, чем проходить по одной области памяти несколько раз. Для лучшего использования ядер GC-воркеры делят нагрузку через кражу задач друг у друга. У каждого воркера своя локальная очередь span для сканирования, и если воркер простаивает, он может забрать задачи у занятых коллег. Этот децентрализованный подход убирает необходимость в глобальном списке и снижает конкуренцию между ядрами.
Green Tea использует векторизованные инструкции CPU (только на amd64) для пакетной обработки span при достаточном количестве объектов.
Бенчмарки варьируются, но команда Go ожидает снижение накладных расходов GC на 10–40% в реальных программах с интенсивным использованием сборщика мусора. Плюс дополнительные 10% при использовании векторизации на процессорах типа Intel Ice Lake или AMD Zen 4 и новее.
Новый GC включён по умолчанию. Чтобы использовать старый, установи GOEXPERIMENT=nogreenteagc при сборке (эта опция будет удалена в Go 1.27).

В runtime Go процессор (P) это ресурс, необходимый для выполнения кода. Чтобы поток (M) мог выполнить горутину (G), он должен сначала захватить процессор. Процессоры переходят между состояниями: Prunning (выполняет код), Pidle (ожидает работу), _Pgcstop (остановлен для GC).
Раньше у процессоров было состояние _Psyscall для горутин, делающих системные вызовы или вызовы cgo. Теперь это состояние убрали. Вместо отдельного состояния система проверяет статус горутины, привязанной к процессору. Это снижает внутренние накладные расходы и упрощает код. В release notes заявлено -30% накладных расходов cgo, а в коммите упоминается улучшение на 18%.

Runtime Go теперь имеет специализированные версии функции аллокации памяти для малых объектов (от 1 до 512 байт). Используются таблицы переходов для быстрого выбора нужной функции вместо единой универсальной реализации. Это снижает стоимость аллокации малых объектов до 30%. Команда Go ожидает общее улучшение около 1% в программах с интенсивной аллокацией.
Новая реализация включена по умолчанию. Отключить можно через GOEXPERIMENT=nosizespecializedmalloc при сборке (опция будет удалена в Go 1.27).

Пакет log/slog, появившийся в версии 1.21, предлагает надёжное, готовое к продакшену решение для логирования. С его выхода многие проекты перешли с сторонних пакетов на него. Однако ему не хватало одной ключевой функции: возможности отправлять записи нескольким обработчикам, например, в stdout и файл одновременно.
Новый тип MultiHandler решает эту проблему. Он реализует стандартный интерфейс Handler и вызывает все настроенные обработчики.
// Создаём обработчик для консольного вывода:
consoleHandler := slog.NewTextHandler(os.Stderr, nil)
// Создаём обработчик для записи в файл аудита:
const fileFlags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
auditFile, _ := os.OpenFile("/var/log/audit.json", fileFlags, 0600)
defer auditFile.Close()
auditHandler := slog.NewJSONHandler(auditFile, nil)
// Объединяем обработчики с помощью `MultiHandler`:
// MultiHandler отправляет логи и в консоль, и в файл аудита
combined := slog.NewMultiHandler(consoleHandler, auditHandler)
log := slog.New(combined)
// Записываем событие о платеже
log.Info("платёж обработан",
slog.String("order_id", "ORD-78542"),
slog.Float64("amount", 1599.99),
)
/*
time=2025-12-31T11:46:14.521Z level=INFO msg=платёж обработан order_id=ORD-78542 amount=1599.99
{"time":"2025-12-31T11:46:14.521126342Z","level":"INFO","msg":"платёж обработан","order_id":"ORD-78542","amount":1599.99}
*/Когда MultiHandler получает запись, он отправляет её каждому включённому обработчику по очереди. Если какой-то обработчик возвращает ошибку, MultiHandler не останавливается? он объединяет все ошибки через errors.Join:
hDebug := slog.NewTextHandler(
os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug},
)
hCritical := slog.NewTextHandler(
os.Stderr, &slog.HandlerOptions{Level: slog.LevelError},
)
hFailing := &FailingHandler{
Handler: hDebug,
err: fmt.Errorf("обработчик временно недоступен"),
}
combined := slog.NewMultiHandler(hFailing, hDebug, hCritical)
entry := slog.NewRecord(time.Now(), slog.LevelDebug, "старт сервиса", 0)
// Вызывает hDebug и hFailing, пропускает hCritical.
// Возвращает ошибку от hFailing.
err := combined.Handle(context.Background(), entry)
fmt.Println(err)
/*
time=2025-12-31T13:32:52.110Z level=DEBUG msg=старт сервиса
обработчик временно недоступен
*/Метод Enabled сообщает, включён ли хотя бы один из настроенных обработчиков:
hDebug := slog.NewTextHandler(
os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug},
)
hWarn := slog.NewTextHandler(
os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn},
)
combined := slog.NewMultiHandler(hDebug, hWarn)
// hDebug включён для уровня Debug
active := combined.Enabled(context.Background(), slog.LevelDebug)
fmt.Println(active)
// trueОстальные методы WithAttr и WithGroup вызывают соответствующие методы на каждом из включённых обработчиков.

Часто указывают, что fmt.Errorf("x") для простых строк аллоцирует больше памяти, чем errors.New("x"). Из-за этого некоторые предлагают переходить с fmt.Errorf на errors.New, когда форматирование не нужно. В новом релизе этот спор должен наконец утихнуть. Для неформатированных строк fmt.Errorf теперь аллоцирует меньше и в целом соответствует errors.New.
Конкретно, fmt.Errorf переходит с 2 аллокаций к 0 для неубегающей ошибки и с 2 к 1 для убегающей:
_ = fmt.Errorf("foo") // неубегающая ошибка
sink = fmt.Errorf("foo") // убегающая ошибкаЭто соответствует аллокациям errors.New в обоих случаях.

Раньше io.ReadAll аллоцировал много промежуточной памяти при увеличении результирующего слайса до размера входных данных. Теперь используются промежуточные слайсы экспоненциально растущего размера, а затем они копируются в финальный слайс идеального размера.
Новая реализация примерно вдвое быстрее и использует примерно вдвое меньше памяти для входа 65 КиБ; для больших входов эффективность ещё выше. Гарантия минимального размера финального слайса тоже полезна. Слайс может жить долго, и неиспользуемая ёмкость в backing array (как в старой версии) просто тратила бы память.
Новые методы Type.Fields и Type.Methods в пакете reflect возвращают итераторы для полей и методов типа:
// Перечисляем поля структуры
t := reflect.TypeFor[os.FileInfo]()
for field := range t.Fields() {
fmt.Println(field.Name, field.Type)
}
/*
name string
size int64
mode FileMode
modTime time.Time
sys any
*/// Перечисляем методы типа
t := reflect.TypeFor[*bytes.Buffer]()
for method := range t.Methods() {
fmt.Println(method.Name)
}
/*
Bytes
Cap
Grow
Len
Next
...
WriteString
*/Новые методы Type.Ins и Type.Outs возвращают итераторы для входных и выходных параметров функционального типа:
t := reflect.TypeFor[func(context.Context, string) ([]byte, error)]()
fmt.Println("Аргументы функции:")
for param := range t.Ins() {
fmt.Println("-", param.Name())
}
fmt.Println("Возвращаемые значения:")
for ret := range t.Outs() {
fmt.Println("-", ret.Name())
}
/*
Аргументы функции:
- Context
- string
Возвращаемые значения:
- []uint8
- error
*/Новые методы Value.Fields и Value.Methods возвращают итераторы для полей и методов значения. Каждая итерация возвращает и информацию о типе (StructField или Method), и значение:
cfg := &tls.Config{ServerName: "api.example.com", MinVersion: tls.VersionTLS13}
v := reflect.ValueOf(cfg)
fmt.Println("Ненулевые поля:")
for field, val := range v.Elem().Fields() {
if !val.IsZero() {
fmt.Printf("- %s = %v\n", field.Name, val.Interface())
}
}
/*
Ненулевые поля:
- ServerName = api.example.com
- MinVersion = 772
*/Раньше можно было получить всю эту информацию через цикл for-range с методами NumX:
// Go 1.25
t := reflect.TypeFor[tls.Config]()
for i := range t.NumField() {
f := t.Field(i)
fmt.Println(f.Name, f.Type)
}Использование итератора лаконичнее.

Новый метод Buffer.Peek в пакете bytes возвращает следующие N байт из буфера без продвижения позиции:
stream := bytes.NewBufferString("HTTP/1.1 200 OK")
header, err := stream.Peek(8)
fmt.Printf("peek=%s err=%v\n", header, err)
// peek=HTTP/1.1 err=<nil>
stream.Next(9) // пропускаем "HTTP/1.1 "
status, err := stream.Peek(6)
fmt.Printf("peek=%s err=%v\n", status, err)
// peek=200 OK err=<nil>Если Peek возвращает меньше N байт, возвращается также io.EOF:
stream := bytes.NewBufferString("tiny")
chunk, err := stream.Peek(256)
fmt.Printf("peek=%s err=%v\n", chunk, err)
// peek=tiny err=EOFСрез, возвращаемый Peek, указывает на содержимое буфера и остаётся валидным до следующей модификации буфера. Если изменить срез, это повлияет на будущие чтения:
stream := bytes.NewBufferString("cat")
preview, err := stream.Peek(3)
fmt.Printf("peek=%s err=%v\n", preview, err)
// peek=cat err=<nil>
preview[0] = 'b' // меняем базовый буфер
preview[1] = 'a'
preview[2] = 't'
content, err := stream.ReadBytes(0)
fmt.Printf("data=%s err=%v\n", content, err)
// data=bat err=EOFСрез от Peek валиден только до следующего вызова метода чтения или записи.
Со временем команда go fix превратилась в забытый набор перезаписей для очень древних фич Go. Но теперь она возвращается. Новый go fix переписан с использованием Go analysis framework - того же, что использует go vet. Хотя go fix и go vet теперь используют одну инфраструктуру, у них разные цели и разные наборы анализаторов:
Vet для сообщения о проблемах. Его анализаторы описывают реальные проблемы, но не всегда предлагают исправления, и исправления не всегда безопасны для применения.
Fix (в основном) для модернизации кода под новые возможности языка и библиотек. Его анализаторы производят исправления, которые всегда безопасны для применения, но не обязательно указывают на проблемы в коде.
usage: go fix [build flags] [-fixtool prog] [fix flags] [packages]
Fix запускает инструмент Go fix (cmd/fix) на указанных пакетах и применяет предложенные исправления.
Поддерживаемые флаги:
-diff
вместо применения исправлений выводит патч как unified diff
Флаг -fixtool=prog выбирает другой инструмент анализа с альтернативными или дополнительными фиксерами.По умолчанию go fix запускает полный набор анализаторов (сейчас их больше 20). Чтобы выбрать конкретные, используй флаг -NAME для каждого, или -NAME=false для запуска всех, кроме указанных. Например, включаем только анализатор forvar:
go fix -forvar ./...А здесь включаем все, кроме omitzero:
go fix -omitzero=false ./...Пока нет способа отключить конкретные анализаторы для определённых файлов или участков кода.
Для примера вот один из анализаторов в действии. Он заменяет циклы на slices.Contains или slices.ContainsFunc:
// до go fix
func hasPermission(roles []string, required string) bool {
for _, role := range roles {
if required == role {
return true
}
}
return false
}
// после go fix
func hasPermission(roles []string, required string) bool {
return slices.Contains(roles, required)
}Новые метрики в пакете runtime/metrics дают лучшее понимание планирования горутин:
Общее число горутин с начала программы
Число горутин в каждом состоянии
Число активных потоков
Полный список:
/sched/goroutines-created:goroutines
Количество горутин, созданных с начала программы.
/sched/goroutines/not-in-go:goroutines
Приблизительное число горутин, выполняющихся
или заблокированных в системном вызове или cgo.
/sched/goroutines/runnable:goroutines
Приблизительное число горутин, готовых к выполнению,
но не выполняющихся.
/sched/goroutines/running:goroutines
Приблизительное число выполняющихся горутин.
Всегда <= /sched/gomaxprocs:threads.
/sched/goroutines/waiting:goroutines
Приблизительное число горутин, ожидающих
ресурсов (I/O или примитивов синхронизации).
/sched/threads/total:threads
Текущее число живых потоков, принадлежащих Go runtime.
Метрики состояний горутин можно связать с типичными продакшен-проблемами. Например, растущее число waiting может указывать на lock contention. Высокое число not-in-go означает, что горутины застряли в syscall или cgo. Растущий runnable backlog говорит о том, что CPU не справляются с нагрузкой.
Рассмотрим практический пример — мониторинг состояния горутин в приложении-краулере:
// SchedulerStats собирает метрики планировщика Go
type SchedulerStats struct {
keys []string
}
func NewSchedulerStats() *SchedulerStats {
return &SchedulerStats{
keys: []string{
"/sched/goroutines-created:goroutines",
"/sched/goroutines:goroutines",
"/sched/goroutines/running:goroutines",
"/sched/goroutines/runnable:goroutines",
"/sched/goroutines/waiting:goroutines",
"/sched/goroutines/not-in-go:goroutines",
"/sched/threads/total:threads",
},
}
}
func (s *SchedulerStats) Collect() map[string]uint64 {
samples := make([]metrics.Sample, len(s.keys))
for idx, key := range s.keys {
samples[idx].Name = key
}
metrics.Read(samples)
data := make(map[string]uint64, len(samples))
for _, sample := range samples {
data[sample.Name] = sample.Value.Uint64()
}
return data
}
func (s *SchedulerStats) Display() {
data := s.Collect()
fmt.Println("╔═══════════════════════════════════════╗")
fmt.Println("║ Мониторинг планировщика Go ║")
fmt.Println("╠═══════════════════════════════════════╣")
fmt.Printf("║ Создано за всё время: %12d ║\n", data["/sched/goroutines-created:goroutines"])
fmt.Printf("║ Текущее количество: %12d ║\n", data["/sched/goroutines:goroutines"])
fmt.Println("╠═══════════════════════════════════════╣")
fmt.Printf("║ ▶ Исполняются: %12d ║\n", data["/sched/goroutines/running:goroutines"])
fmt.Printf("║ ⏳ Ожидают процессор: %12d ║\n", data["/sched/goroutines/runnable:goroutines"])
fmt.Printf("║ 💤 Ожидают события: %12d ║\n", data["/sched/goroutines/waiting:goroutines"])
fmt.Printf("║ 🔧 Вне Go runtime: %12d ║\n", data["/sched/goroutines/not-in-go:goroutines"])
fmt.Println("╠═══════════════════════════════════════╣")
fmt.Printf("║ Системных потоков: %12d ║\n", data["/sched/threads/total:threads"])
fmt.Println("╚═══════════════════════════════════════╝")
}
func main() {
stats := NewSchedulerStats()
// Запускаем краулеры
urls := make(chan string, 200)
for range 12 {
go crawler(urls)
}
// Добавляем URL для обработки
for i := range 80 {
urls <- fmt.Sprintf("https://site%d.example.com", i)
}
time.Sleep(100 * time.Millisecond)
stats.Display()
close(urls)
}
func crawler(urls <-chan string) {
for url := range urls {
time.Sleep(time.Duration(len(url)) * time.Millisecond)
}
}
/*
╔═══════════════════════════════════════╗
║ Мониторинг планировщика Go ║
╠═══════════════════════════════════════╣
║ Создано за всё время: 18 ║
║ Текущее количество: 18 ║
╠═══════════════════════════════════════╣
║ ▶ Исполняются: 1 ║
║ ⏳ Ожидают процессор: 0 ║
║ 💤 Ожидают события: 17 ║
║ 🔧 Вне Go runtime: 0 ║
╠═══════════════════════════════════════╣
║ Системных потоков: 6 ║
╚═══════════════════════════════════════╝
*/Сумма метрик по состояниям (not-in-go + runnable + running + waiting) не гарантированно равна общему числу живых горутин (/sched/goroutines:goroutines, доступному с Go 1.16). Все новые метрики используют счётчики uint64.
Утечка происходит, когда одна или несколько горутин бесконечно заблокированы на примитивах синхронизации (каналах), в то время как другие горутины продолжают работать и программа в целом функционирует. Простой пример:
func startBackgroundTask() <-chan string {
results := make(chan string)
go func() {
results <- "done" // утечка, если результат никто не забирает
}()
return results
}Если вызвать startBackgroundTask и не прочитать из канала, внутренняя горутина останется заблокированной до конца программы:
func main() {
startBackgroundTask()
// ...
}В отличие от deadlock, утечки не вызывают паники, поэтому их намного сложнее обнаружить. И в отличие от data race, инструментарий Go долгое время не решал эту проблему. Ситуация начала меняться в Go 1.24 с появлением пакета synctest. Мало кто об этом говорит, но synctest - отличный инструмент для обнаружения утечек при тестировании.
Go 1.26 добавляет новый экспериментальный профиль goroutineleak, предназначенный для отслеживания утекших горутин в продакшене. Вот как его использовать:
func main() {
leakProfile := pprof.Lookup("goroutineleak")
startBackgroundTask()
time.Sleep(50 * time.Millisecond)
leakProfile.WriteTo(os.Stdout, 2)
// ...
}goroutine 7 [chan send (leaked)]:
main.startBackgroundTask.func1()
/tmp/sandbox/main.go:16 +0x1e
created by main.startBackgroundTask in goroutine 1
/tmp/sandbox/main.go:15 +0x67Получаем красивый стектрейс горутины, показывающий точное место утечки.
Профиль goroutineleak находит утечки, используя фазу маркировки GC для проверки, какие заблокированные горутины всё ещё связаны с активным кодом. Он начинает с выполняемых горутин, помечает все sync-объекты, которых они могут достичь, и продолжает добавлять заблокированные горутины, ожидающие на этих объектах. Когда добавить больше нечего, оставшиеся заблокированные горутины ждут недостижимых ресурсов - они считаются утекшими. Он экспериментальный, включается через GOEXPERIMENT=goroutineleakprofile при сборке. При включении профиль также становится доступен как endpoint net/http/pprof: /debug/pprof/goroutineleak.
По словам авторов, реализация уже готова к продакшену. Экспериментальный статус нужен для сбора отзывов об API.

Пакет net имеет функции верхнего уровня для подключения к адресу по разным протоколам - DialTCP, DialUDP, DialIP, DialUnix. Они были созданы до появления context.Context, поэтому не поддерживают отмену:
serverAddr, _ := net.ResolveTCPAddr("tcp", "192.168.1.100:5432")
dbConn, err := net.DialTCP("tcp", nil, serverAddr)
fmt.Printf("соединение с БД, err=%v\n", err)
defer dbConn.Close()Есть также тип net.Dialer с универсальным методом DialContext. Он поддерживает отмену и может подключаться к любому известному протоколу:
var dialer net.Dialer
ctx := context.Background()
dbConn, err := dialer.DialContext(ctx, "tcp", "192.168.1.100:5432")
fmt.Printf("соединение с БД, err=%v\n", err)
defer dbConn.Close()Однако DialContext немного менее эффективен, чем протокол-специфичные функции типа net.DialTCP из-за накладных расходов на разрешение адресов и диспетчеризацию по типу сети.
Итак, протокол-специфичные функции в net более эффективны, но не поддерживают отмену. Тип Dialer поддерживает отмену, но менее эффективен.
Команда Go решила устранить это противоречие. Новые контекстно-осведомлённые методы Dialer (DialTCP, DialUDP, DialIP, DialUnix) объединяют эффективность существующих протокол-специфичных функций с возможностями отмены Dialer.DialContext:
var dialer net.Dialer
ctx := context.Background()
serverAddr := netip.MustParseAddrPort("192.168.1.100:5432")
dbConn, err := dialer.DialTCP(ctx, "tcp", netip.AddrPort{}, serverAddr)
fmt.Printf("соединение с БД, err=%v\n", err)
defer dbConn.Close()Три разных способа подключения не очень удобно, но такова цена обратной совместимости.
IP-адресный префикс представляет подсеть. Обычно записывается в CIDR-нотации:
10.0.0.0/16
127.0.0.0/8
169.254.0.0/16
203.0.113.0/24В Go IP-префикс представлен типом netip.Prefix. Новый метод Prefix.Compare позволяет сравнивать два IP-префикса, упрощая их сортировку без написания собственного компаратора:
allowedNetworks := []netip.Prefix{
netip.MustParsePrefix("10.20.30.0/24"),
netip.MustParsePrefix("172.31.0.0/16"),
netip.MustParsePrefix("192.168.100.0/24"),
netip.MustParsePrefix("10.0.0.0/8"),
netip.MustParsePrefix("172.31.128.0/20"),
}
slices.SortFunc(allowedNetworks, netip.Prefix.Compare)
for _, network := range allowedNetworks {
fmt.Println(network.String())
}
/*
10.0.0.0/8
10.20.30.0/24
172.31.0.0/16
172.31.128.0/20
192.168.100.0/24
*/Порядок сортировки Compare:
Сначала по валидности (невалидные перед валидными)
По семейству адресов (IPv4 перед IPv6): 10.0.0.0/8 < ::/8
По маскированному IP-адресу (сетевой IP): 10.0.0.0/16 < 10.1.0.0/16
По длине префикса: 10.0.0.0/8 < 10.0.0.0/16
По немаскированному адресу (оригинальный IP): 10.0.0.0/8 < 10.0.0.1/8
Это соответствует порядку Python netaddr.IPNetwork и стандартной конвенции IANA.
После запуска процесса в Go можно получить его ID:
procAttr := &os.ProcAttr{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}
child, _ := os.StartProcess("/usr/bin/date", []string{"date", "+%Y-%m-%d"}, procAttr)
defer child.Wait()
fmt.Println("pid =", child.Pid)Внутри тип os.Process использует handle процесса вместо PID (который просто целое число), если ОС это поддерживает. В Linux используется pidfd - файловый дескриптор, ссылающийся на процесс. Использование handle вместо PID гарантирует, что методы Process всегда работают с тем же процессом ОС, а не с другим процессом, который случайно получил тот же ID.
Раньше доступа к handle процесса не было. Теперь он есть благодаря новому методу Process.WithHandle:
func (p *Process) WithHandle(f func(handle uintptr)) errorWithHandle вызывает указанную функцию и передаёт handle процесса как аргумент:
procAttr := &os.ProcAttr{Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}}
child, _ := os.StartProcess("/usr/bin/date", []string{"date", "+%Y-%m-%d"}, procAttr)
defer child.Wait()
fmt.Println("pid =", child.Pid)
child.WithHandle(func(fd uintptr) {
fmt.Println("handle =", fd)
})
/*
pid = 127
handle = 8
2025-12-31
*/Handle гарантированно ссылается на процесс до возврата из callback-функции, даже если процесс уже завершился. Поэтому реализовано как callback, а не как поле или метод Process.Handle. WithHandle поддерживается только на Linux 5.4+ и Windows. На других ОС не выполняет callback и возвращает ошибку os.ErrNoHandle.
signal.NotifyContext возвращает контекст, который отменяется при получении указанных сигналов. Раньше отменённый контекст показывал только стандартную причину «context canceled»:
// Go 1.25
// Контекст завершится при получении SIGTERM
shutdownCtx, cancelShutdown := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer cancelShutdown()
// Симулируем получение SIGTERM
currentProcess, _ := os.FindProcess(os.Getpid())
_ = currentProcess.Signal(syscall.SIGTERM)
// Ожидаем завершения
<-shutdownCtx.Done()
fmt.Println("err =", shutdownCtx.Err())
fmt.Println("cause =", context.Cause(shutdownCtx))
/*
err = context canceled
cause = context canceled
*/Теперь причина контекста показывает, какой именно сигнал был получен:
// Go 1.26
// Контекст завершится при получении SIGTERM
shutdownCtx, cancelShutdown := signal.NotifyContext(context.Background(), syscall.SIGTERM)
defer cancelShutdown()
// Симулируем получение SIGTERM
currentProcess, _ := os.FindProcess(os.Getpid())
_ = currentProcess.Signal(syscall.SIGTERM)
// Ожидаем завершения
<-shutdownCtx.Done()
fmt.Println("err =", shutdownCtx.Err())
fmt.Println("cause =", context.Cause(shutdownCtx))
/*
err = context canceled
cause = terminated signal received
*/Возвращаемый тип signal.signalError основан на string, поэтому не предоставляет само значение os.Signal - только его строковое представление.
Текущие криптографические API, такие как ecdsa.GenerateKey или rand.Prime, принимают io.Reader как источник случайных данных:
// Создаём ключевую пару для ECDSA подписи
signingKey, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
fmt.Println(signingKey.D)
// 42198376521098347612098374612098347612098347612098347612098374612098347612091
// Генерируем 128-битное простое число для криптографии
largePrime, _ := rand.Prime(rand.Reader, 128)
fmt.Println(largePrime)
// 287946352987463529874635298746352987463Эти API не фиксируют конкретный способ использования случайных байтов из reader. Любое изменение в криптоалгоритмах может изменить последовательность или количество читаемых байтов. Если код приложения (ошибочно) полагается на конкретную реализацию версии X, он может сломаться в версии X+1.
Команда Go выбрала довольно радикальное решение. Теперь большинство крипто-API будут просто игнорировать параметр io.Reader и всегда использовать системный источник случайности (crypto/internal/sysrand.Read).
// Параметр reader больше не используется, можно передать nil
// Создаём ключевую пару для ECDSA подписи
signingKey, _ := ecdsa.GenerateKey(elliptic.P384(), nil)
fmt.Println(signingKey.D)
// 91823746519823746519823746519823746519823746519823746519823746519823746519827
// Генерируем 128-битное простое число для криптографии
largePrime, _ := rand.Prime(nil, 128)
fmt.Println(largePrime)
// 198273465198273465198273465198273465Изменение затрагивает следующие подпакеты crypto:
// crypto/dsa
func GenerateKey(priv *PrivateKey, rand io.Reader) error
// crypto/ecdh
type Curve interface {
GenerateKey(rand io.Reader) (*PrivateKey, error)
}
// crypto/ecdsa
func GenerateKey(c elliptic.Curve, rand io.Reader) (*PrivateKey, error)
func SignASN1(rand io.Reader, priv *PrivateKey, hash []byte) ([]byte, error)
func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error)
func (priv *PrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)
// crypto/rand
func Prime(rand io.Reader, bits int) (*big.Int, error)
// crypto/rsa
func GenerateKey(random io.Reader, bits int) (*PrivateKey, error)
func GenerateMultiPrimeKey(random io.Reader, nprimes int, bits int) (*PrivateKey, error)
func EncryptPKCS1v15(random io.Reader, pub *PublicKey, msg []byte) ([]byte, error)ed25519.GenerateKey(rand) всё ещё использует переданный reader, если он не nil. Если rand равен nil, используется внутренний защищённый источник случайности вместо crypto/rand.Reader (который можно переопределить).
Для поддержки детерминированного тестирования появился новый пакет testing/cryptotest с функцией SetGlobalRandom. Она устанавливает глобальный детерминированный источник криптографической случайности на время теста:
func TestDeterministicPrimes(t *testing.T) {
cryptotest.SetGlobalRandom(t, 777)
// Все запуски теста сгенерируют одинаковые числа
prime1, _ := rand.Prime(nil, 32)
prime2, _ := rand.Prime(nil, 32)
prime3, _ := rand.Prime(nil, 32)
actual := [3]int64{prime1.Int64(), prime2.Int64(), prime3.Int64()}
expected := [3]int64{2891726341, 4017283649, 3298174523}
if actual != expected {
t.Errorf("actual %v, expected %v", actual, expected)
}
}SetGlobalRandom влияет на crypto/rand и все неявные источники криптографической случайности в пакетах crypto/*:
func TestRandomBytes(t *testing.T) {
cryptotest.SetGlobalRandom(t, 777)
t.Run("ReadBytes", func(t *testing.T) {
var actual [4]byte
rand.Read(actual[:])
expected := [4]byte{91, 27, 183, 42}
if actual != expected {
t.Errorf("actual %v, expected %v", actual, expected)
}
})
t.Run("RandomNumber", func(t *testing.T) {
actual, _ := rand.Int(rand.Reader, big.NewInt(50000))
const expected = 31847
if actual.Int64() != expected {
t.Errorf("actual %v, expected %v", actual.Int64(), expected)
}
})
}Для временного возврата к старому поведению с reader установи GODEBUG=cryptocustomrand=1 (эта опция будет удалена в будущем релизе).

Криптографические протоколы типа WireGuard или TLS имеют свойство «forward secrecy» (прямая секретность). Даже если злоумышленник получит доступ к долгосрочным секретам (например, приватному ключу TLS), он не сможет расшифровать прошлые сессии. Для этого эфемерные ключи (временные ключи для согласования сессии) должны быть немедленно удалены из памяти после handshake.
В Go runtime управляет памятью и не гарантирует, когда и как память очищается. Чувствительные данные могут оставаться в куче или стековых фреймах, потенциально попадая в core dump или становясь доступными через memory-атаки. Разработчики часто используют ненадёжные хаки с рефлексией, чтобы обнулить внутренние буферы криптобиблиотек.
Решение команды Go - новый пакет runtime/secret. Он позволяет запустить функцию в защищённом режиме. После завершения функции регистры и стек, которые она использовала, немедленно обнуляются. Аллокации в куче обнуляются, как только GC определит их недостижимость.
secret.Do(func() {
// Обрабатываем конфиденциальные данные
// и производим безопасный обмен ключами
})Пример использования в более реалистичном сценарии - генерация сессионного ключа с защитой эфемерного приватного ключа:
// EstablishSecureChannel создаёт защищённый канал связи с партнёром
func EstablishSecureChannel(remotePubKey *ecdh.PublicKey) (*ecdh.PublicKey, []byte, error) {
var localPubKey *ecdh.PublicKey
var channelKey []byte
var err error
// secret.Do ограничивает чувствительные данные временем handshake.
// Эфемерный приватный ключ и сырой shared secret
// будут уничтожены по завершении функции.
secret.Do(func() {
// 1. Генерируем эфемерный приватный ключ.
// Критически важен - при утечке forward secrecy нарушается.
ephemeralKey, e := ecdh.P384().GenerateKey(rand.Reader)
if e != nil {
err = e
return
}
// 2. Вычисляем shared secret (ECDH).
// Этот сырой секрет тоже критически важен.
rawSecret, e := ephemeralKey.ECDH(remotePubKey)
if e != nil {
err = e
return
}
// 3. Выводим финальный ключ канала (например, через HKDF).
// Результат копируется наружу; входные данные (ephemeralKey, rawSecret)
// будут уничтожены secret.Do при потере достижимости.
channelKey = deriveKeyHKDF(rawSecret)
localPubKey = ephemeralKey.PublicKey()
})
// Ключ канала возвращается для использования, но «рецепт» его создания
// уничтожен. Поскольку ключ был аллоцирован внутри secret-блока,
// runtime автоматически обнулит его, когда приложение закончит с ним работу.
return localPubKey, channelKey, err
}Эфемерный приватный ключ и сырой shared secret - «токсичные отходы»: они нужны для создания сессионного ключа, но опасны для хранения. Если они останутся в куче и злоумышленник получит доступ к памяти приложения (через core dump или уязвимость типа Heartbleed), он сможет восстановить сессионный ключ и расшифровать прошлые разговоры.
Оборачивая вычисления в secret.Do, мы гарантируем, что как только сессионный ключ создан, «ингредиенты» уничтожены навсегда.
func main() {
// Имитируем публичный ключ удалённой стороны
remotePriv, _ := ecdh.P384().GenerateKey(nil)
remotePubKey := remotePriv.PublicKey()
// Устанавливаем защищённый канал
localPubKey, channelKey, err := EstablishSecureChannel(remotePubKey)
fmt.Printf("local public key = %x...\n", localPubKey.Bytes()[:16])
fmt.Printf("error = %v\n", err)
_ = channelKey
}local public key = 047c3f91ab2e8d4c5f6a7b8c9d0e1f2a...
error = <nil>Текущая реализация secret.Do поддерживает только Linux (amd64 и arm64). На неподдерживаемых платформах Do просто вызывает функцию напрямую. Попытка запустить горутину внутри функции вызывает панику (это исправят в Go 1.27).
Пакет runtime/secret в первую очередь для разработчиков криптографических библиотек. Большинство приложений должны использовать высокоуровневые библиотеки, которые применяют secret.Do под капотом.
Пакет экспериментальный, включается через GOEXPERIMENT=runtimesecret при сборке.
Новый пакет simd/archsimd предоставляет доступ к архитектурно-специфичным векторизованным операциям (SIMD - single instruction, multiple data). Это низкоуровневый пакет с функциональностью, специфичной для железа. Пока поддерживается только amd64. Поскольку разные архитектуры CPU имеют очень разные SIMD-операции, сложно создать единый портируемый API. Поэтому команда Go решила начать с низкоуровневого API для amd64 - самой распространённой серверной платформы. Пакет определяет векторные типы как структуры, например Int8x16 (128-битный SIMD-вектор с шестнадцатью 8-битными целыми) и Float64x8 (512-битный вектор с восемью 64-битными float). Они соответствуют аппаратным векторным регистрам. Поддерживаются векторы шириной 128, 256 и 512 бит. Большинство операций определены как методы векторных типов и обычно напрямую транслируются в аппаратные инструкции без накладных расходов.
Пример функции умножения векторов float32 с использованием SIMD:
func MultiplyVectors(a, b []float32) []float32 {
if len(a) != len(b) {
panic("массивы должны быть одинаковой длины")
}
// Проверяем поддержку AVX-512
if !archsimd.X86.AVX512() {
return multiplyScalar(a, b)
}
output := make([]float32, len(a))
total := len(a)
idx := 0
// 1. SIMD-цикл: обрабатываем по 16 элементов за раз
for idx <= total-16 {
// Загружаем по 16 элементов из обоих массивов
va := archsimd.LoadFloat32x16Slice(a[idx : idx+16])
vb := archsimd.LoadFloat32x16Slice(b[idx : idx+16])
// Перемножаем все 16 элементов одной инструкцией
// и сохраняем в выходной массив
vProduct := va.Mul(vb) // транслируется в VMULPS
vProduct.StoreSlice(output[idx : idx+16])
idx += 16
}
// 2. Обработка остатка (0-15 элементов)
for ; idx < total; idx++ {
output[idx] = a[idx] * b[idx]
}
return output
}Проверим на двух векторах:
func main() {
prices := []float32{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170}
quantities := []float32{2, 3, 1, 4, 2, 1, 3, 2, 1, 4, 2, 1, 3, 2, 1, 4, 2}
totals := MultiplyVectors(prices, quantities)
fmt.Println(totals)
}
// [20 60 30 160 100 60 210 160 90 400 220 120 390 280 150 640 340]Основные операции в archsimd:
Load / Store - загрузка и сохранение вектора из/в массив или слайс
Арифметика: Add, Sub, Mul, Div, DotProduct
Побитовые: And, Or, Not, Xor, Shift
Сравнение: Equal, Greater, Less, Min, Max
Преобразование: As, SaturateTo, TruncateTo
Маскирование: Compress, Masked, Merge
Перестановка: Permute
Пакет использует только AVX-инструкции, не SSE. Пакет экспериментальный, включается через GOEXPERIMENT=simd при сборке.Итоги
Отличный релиз! Go 1.26 - один из самых насыщенных релизов за всю мою память:
Полезные обновления: расширенный new, типобезопасная проверка ошибок errors.AsType, детектор утечек горутин
Оптимизации производительности: новый GC Green Tea, ускорение cgo и аллокации памяти, оптимизация fmt.Errorf и io.ReadAll
Quality-of-life фичи: множественные обработчики логов slog.MultiHandler, артефакты тестов, обновлённый go fix
Специализированные экспериментальные пакеты: SIMD для векторизации и runtime/secret для forward secrecy