Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

Релиз Golang 1.25 за 10 минут — что улучшили и добавили в новой версии языка

Обзор заметок релиза Go 1.25. Изменения в инструментах, среде выполнения, компиляторе, компоновщике и стандартной библиотеке.

1К открытий4К показов
Релиз Golang 1.25 за 10 минут — что улучшили и добавили в новой версии языка

Релиз Golang 1.25 направлен на производительность и улучшение инструментов. Добавили новый сборщик мусора, автонастройку GOMAXPROCS для контейнеров, пакеты testing/synctest, json/v2. В публикации собрали самые заметные изменения Go 1.25 и рассказали про сценарии их применения.

Сборщик мусора Green Tea

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

Green Tea — это альтернативный алгоритм сборки мусора. Он настроен на борьбу с мелкими объектами. Пока что это экспериментальная функция, судьба Green Tea зависит от результатов тестирования сообществом.

			//Попробовать новый сборщик мусора
GOEXPERIMENT=greentea go build myapp.go
		

Green Tea очищает последовательные блоки памяти (батчи). Размер блоков 8 КБ, в них хранятся объекты до 512 МБ. Объекты больше этого лимита обрабатываются по старому алгоритму.

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

В нагрузочных тестах с миллионом объектов Green Tea использовал на 22% меньше CPU, на 8% меньше потреблял память. Время выполнения тоже улучшилось на 5%.

Время пауз в основном остаётся коротким, но 99-й процентиль показал более длительные остановки — 1.84 мс против 0.92 мс у классического GC. То есть в редких случаях всё же могут возникать заметные задержки.

Пакет synctest

Ситуация: вы написали код с таймерами и хотите его протестировать. Паузы длятся по несколько секунд — совсем немного. Проблема в том, что у нас 100 тестов, т. е. одна итерация тестирования займёт несколько минут.

До synctest у вас было на выбор четыре сценария:

  • пропускать тесты,
  • ждать все таймауты,
  • создавать интерфейсы-обёртки,
  • передавать функции времени как параметры.

В Golang 1.25 можно сделать проще — обернуть тест в synctest.Run() и пропустить время ожидания.

Как работает synctest

Функция подменяет системное время. Например, когда код вызывает time.Sleep(5 * time.Second), виртуальные часы переводятся на 5 секунд вперёд, и выполнение теста мгновенно продолжается.

			func ProcessWithTimeout(data string) error {
    ctx, cancel := context.WithTimeout(context.Background(), 30 * time.Second)
    defer cancel()
    return heavyProcess(ctx, data)
}

// Имитация долгой работы
func heavyProcess(ctx context.Context, data string) error {
    select {
    case <-time.After(1 * time.Hour):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func TestProcessTimeout(t *testing.T) {
    synctest.Run(func() {
        // Таймаут срабатывает мгновенно
        err := ProcessWithTimeout("any")
        require.ErrorIs(t, err, context.DeadlineExceeded)
    })
}
		

Ограничения

Synctest работает только со стандартными функциями. Подменять системное время бесполезно, если ваш код обращается, например, к PostgreSQL, или читает timestamp из файла.

			// Будет замокано
time.Sleep(time.Second)
time.After(5 * time.Second)

// Не будет замокано  
db.QueryRow("SELECT NOW()")
os.Stat("file.txt") // ModTime() вернёт реальное время
		

Пакет encoding/json/v2

Разработчики Golang не стали изменять существующий пакет, чтобы сохранить обратную совместимость. Вместо этого добавили вторую версию пакета — encoding/json/v2.

По скорости новая версия сравнима с внешними библиотеками вроде jsoniter. При декодировании v2 быстрее предыдущей версии в 3-10 раз. Ещё json/v2 в 38,6 раза быстрее читает JSON.

Прямая работа с Reader и Writer

Функции MarshalWrite и UnmarshalRead работают напрямую с io.Writer и io.Reader. Они экономят память при обработке большого JSON, т. к. не создаются промежуточные байтовые буферы Encoder и Decoder.

Настройки прямо при вызове

В encoding/json/v2 можно передавать опции прямо в функции маршалинга.

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

			// Настройки прямо в вызове
data, _ := json.Marshal(user, json.Indent("  "))

// Или числа как строки
data, _ := json.Marshal(product, json.StringifyNumbers(true))
		

Новые теги для структур

  • inline встраивает поля вложенной структуры на уровень родителя — больше не нужно дублировать поля адреса в структуре пользователя.
  • case строго сопоставляет имена полей при парсинге JSON — с настройкой «ignore» программа будет считать одинаковыми userName, user_name и UserName. С настройкой «strict» потребует точного совпадения регистра и символов.
  • unknown собирает все неизвестные поля в одну map — удобно для работы с динамическими API, где набор полей может меняться.
  • format указывает формат для времени, дат и других типов прямо в структуре.

Кастомные преобразователи без изменения типов

В Go 1.24 для кастомной логики сериализации приходилось создавать новый тип и реализовывать Marshaler/Unmarshaler. Теперь можно написать функцию-преобразователь через MarshalFunc и использовать её только там, где нужно.

			// Функция-преобразователь
upperMarshaler := json.MarshalFunc(func(v any) ([]byte, error) {
    return json.Marshal(strings.ToUpper(v.(string)))
})

// Используем только там, где нужно
data, _ := json.Marshal("привет", upperMarshaler)
		

Изменения в поведении по умолчанию

encoding/json/v2 может сломать существующий код:

  • Nil-слайсы теперь кодируются как пустые массивы [] вместо null, nil-карты — как пустые объекты {} вместо null.
  • Байтовые массивы автоматически кодируются в base64-строки, а не в массивы чисел.
  • При декодировании имена полей сравниваются с учётом регистра — поле «name» в JSON не найдёт поле Name в структуре.

Улучшена потоковая обработка

Добавили функции UnmarshalDecode и MarshalEncode для работы с большими JSON-файлами, где данные идут потоком (логи, экспорты БД). Они работают с jsontext.Decoder и jsontext.Encoder, обрабатывая по одному JSON-объекту за раз без загрузки файла в память.

Композиция настроек и маршалеров

Опции и маршалеры объединяются через JoinOptions и JoinMarshalers. Т. е. можно создавать переиспользуемые наборы настроек для разных сценариев.

Например, один набор для внешнего API с красивым форматированием, другой для внутренних сервисов с компактным выводом.

Метод WaitGroup.Go

Добавили новый метод Go(), чтобы упростить работу с горутинами. Изменение небольшое, но очень полезное — ваш код в версии 1.25 станет чище и безопаснее.

			var wg sync.WaitGroup

wg.Go(func() {
    // логика горутины
})

wg.Go(func() {
    // логика другой горутины
})

wg.Wait()
		

Метод упаковывает часто используемый код в удобную функцию. WaitGroup.Go() автоматически вызывает wg.Add(1), запускает переданную функцию в новой горутине и добавляет defer wg.Done() в начало выполнения функции.

Work Pattern для go.work

Представьте, что у вас есть большой проект с множеством микросервисов: сервис A, сервис B, сервис C и так далее. Раньше, чтобы запустить тесты во всех сервисах, приходилось писать длинные команды, явно указывая каждый модуль:

			go test ./service-a && go test ./service-b && go test ./service-c
		

В Go 1.25 можно просто написать:

			go test work
		

И тесты запустятся автоматически во всех модулях, указанных в файле go.work.

Трассировка runtime/trace.FlightRecorder

Трейсинг в Go — это дорогое удовольствие. Обычное трассирование записывает всё подряд с самого начала до конца программы. Flight Recorder работает по-другому:

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

FlightRecorder вызовы функций, активность горутин, время выполнения операций, использование памяти, работу сборщика мусора.

Группировка атрибутов в slog

За группировку атрибутов отвечает метод slog.Group(). Он принимает второй параметр типа []any, куда можно передать абсолютно любые данные — строки, числа, объекты. Проблема в том, что компилятор не знает, действительно ли вы передаёте корректные атрибуты для логирования.

В Go 1.25 появился метод groupAttrs, который принимает строго типизированный слайс атрибутов []slog.Attr. В этом случае компилятор сможет проверить корректность данных на этапе компиляции.

			attrs := []slog.Attr{
    slog.String("name", "John"),
    slog.Int("age", 25),
}
logger.Info("User info", slog.GroupAttrs("user", attrs...))
		

Если попытаетесь добавить в слайс что-то кроме slog.Attr, получите ошибку компиляции.

GOMAXPROCS учитывает запуск в контейнерах

Эта переменная окружения определяет максимальное количество потоков ОС для одновременного выполнения горутин. По умолчанию GOMAXPROCS равняется количеству логических процессоров на машине.

Например, ваше приложение запущено в Docker-контейнере с ограничением 2 CPU на сервере с 16 ядрами. В старых версиях Go пытается использовать все 16 ядер. Чтобы приложение соблюдало ограничения, разработчики вынужденно ставили стороннюю библиотеку automaxprocs.

В новой версии Go автоматически:

  1. Читает настройки cgroups.
  2. Определяет доступное количество CPU для контейнера.
  3. Устанавливает GOMAXPROCS в соответствии с этими ограничениями.

Если контейнер ограничен 2 CPU, Go автоматически установит GOMAXPROCS=2, независимо от количества ядер у физического сервера.

Исправление бага в компиляторе Go 1.21+

Компилятор переставляет местами строки кода во время компиляции. Если вы писали код, который использовал результат функции без предварительной проверки ошибки, то могли наткнуться на этот баг.

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

			func main() {
	f, err = os.Open(name: "nonExistentFile")
	name = f.Name()

	if err != nil {
		return
	}

	println(name)
}
		

В Go 1.21-1.24 компилятор «оптимизировал» код, переставляя строку name = f.Name() после проверки ошибки. Поэтому программа завершалась без паники.

В Go 1.25 компилятор выдаёт «invalid memory address or nil pointer dereference», как и должно быть.

Ждёте стабильную версию GC Green Tea в Golang 1.26? Расскажите в комментариях, как новый сборщик показал себя на реальных рабочих нагрузках — другим разработчикам будет интересно почитать про ваш опыт.

Следите за новыми постами
Следите за новыми постами по любимым темам
1К открытий4К показов