Антон ЖияновGo, SQL и разработка софтаhttps://antonz.ru/https://antonz.ru/assets/favicon/favicon.pngАнтон Жияновhttps://antonz.ru/Hugo -- gohugo.ioru-ruTue, 09 Dec 2025 10:00:00 +0000Go-фича: Защита секретовhttps://antonz.ru/accepted/runtime-secret/Tue, 09 Dec 2025 10:00:00 +0000https://antonz.ru/accepted/runtime-secret/Автоматически очищать память, чтобы не допустить утечки секретов.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

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

Версия 1.26 • Станд. библиотека • Мелочь

Что

Новый пакет runtime/secret позволяет запускать функцию в защищенном режиме (secret mode). После завершения функции все использованные ей регистры и стек сразу очищаются (затираются нулями). Выделенная функцией память в куче очищается после того, как сборщик мусора решит, что она недостижима (unreachable).

secret.Do(func() {
    // Сгенерировть сессионный ключ
    // и зашифровать им данные.
})

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

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

Зачем

Криптографические протоколы, такие как WireGuard или TLS, обладают свойством под названием "прямая секретность" (forward secrecy). Это значит, что даже если злоумышленник получит доступ к долгосрочным секретам (например, к приватному ключу в TLS), он все равно не сможет расшифровать прошлые сессии. Для этого сессионные ключи (которые используются для шифрования и расшифровки данных во время конкретной сессии) должны удаляться из памяти после использования. Если нет надежного способа очистить эту память, ключи могут остаться там надолго, и тогда прямая секретность нарушится.

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

Решение — создать рантайм-механизм, который автоматически очищает всю временную память, задействованную во время операций над секретными данными. Так разработчикам библиотек будет проще писать безопасный код без всяких костылей.

Как

Добавить пакет runtime/secret с функциями Do и Enabled:

// Do invokes f.
//
// Do ensures that any temporary storage used by f is erased in a
// timely manner. (In this context, "f" is shorthand for the
// entire call tree initiated by f.)
//   - Any registers used by f are erased before Do returns.
//   - Any stack used by f is erased before Do returns.
//   - Any heap allocation done by f is erased as soon as the garbage
//     collector realizes that it is no longer reachable.
//   - Do works even if f panics or calls runtime.Goexit. As part of
//     that, any panic raised by f will appear as if it originates from
//     Do itself.
func Do(f func())
// Enabled reports whether Do appears anywhere on the call stack.
func Enabled() bool

Текущее решение имеет несколько ограничений:

  • Работает только на linux/amd64 и linux/arm64. На других платформах функция Do просто вызывает f напрямую.
  • Защита не распространяется на глобальные переменные, которые изменяет f.
  • Если внутри f запускается новая горутина, произойдет паника.
  • Если f вызывает runtime.Goexit, очистка откладывается до выполнения всех отложенных функций.
  • Данные в куче очищаются только если ➊ на них больше нет ссылок, и ➋ сборщик мусора заметил это. Первый пункт зависит от программы, второй — от того, когда рантайм решит провести сборку мусора.
  • Если в f происходит паника, значение паники может содержать ссылки на память, выделенную внутри f. Эта память не будет очищена, пока значение паники доступно.
  • Адреса указателей могут попасть в буферы данных, которые использует сборщик мусора. Не храните конфиденциальную информацию в указателях.

Последний пункт может быть не сразу понятен, поэтому вот пример. Если смещение в массиве само по себе является секретом (у вас есть массив data, и секретный ключ всегда начинается с data[100]), не создавайте указатель на это место (не делайте указатель p на &data[100]). Иначе сборщик мусора может сохранить этот указатель (ему нужно знать обо всех активных указателях для работы). Если кто-то получит доступ к памяти сборщика мусора, ваше секретное смещение может быть раскрыто.

Пакет в основном предназначен для разработчиков, которые работают с криптографическими библиотеками. Большинству приложений лучше использовать более высокоуровневые библиотеки, которые внутри уже используют secret.Do.

В Go 1.26 пакет runtime/secret объявлен экспериментальным. Чтобы его включить, установите переменную GOEXPERIMENT=runtimesecret при сборке.

Пример

Используем secret.Do, чтобы сгенерировать сессионный ключ и зашифровать сообщение с помощью AES-GCM:

// Encrypt создает временный ключ и шифрует сообщение.
// Вся чувствительная операция обернута в secret.Do,
// чтобы ключ и внутреннее состояние AES были удалены из памяти.
func Encrypt(message []byte) ([]byte, error) {
    var ciphertext []byte
    var encErr error

    secret.Do(func() {
        // 1. Сгенерировать временный ключ длиной 32 байта.
        // Эта операция защищена с помощью secret.Do.
        key := make([]byte, 32)
        if _, err := io.ReadFull(rand.Reader, key); err != nil {
            encErr = err
            return
        }

        // 2. Создаем шифр (раундовые ключи).
        // Эта структура тоже защищена.
        block, err := aes.NewCipher(key)
        if err != nil {
            encErr = err
            return
        }

        gcm, err := cipher.NewGCM(block)
        if err != nil {
            encErr = err
            return
        }

        nonce := make([]byte, gcm.NonceSize())
        if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
            encErr = err
            return
        }

        // 3. Запечатываем данные.
        // Из замыкания выходит только зашифрованный текст.
        ciphertext = gcm.Seal(nonce, nonce, message, nil)
    })

    return ciphertext, encErr
}

Здесь secret.Do защищает не только сам ключ, но и структуру cipher.Block, которая создается внутри функции и содержит расширенные ключи.

Ссылки

𝗣 21865 • 𝗖𝗟 704615 • 👥 Daniel Morsing, Dave Anderson, Filippo Valsorda, Jason A. Donenfeld, Keith Randall, Russ Cox

]]>
Go-фича: Безопасная проверка ошибокhttps://antonz.ru/accepted/errors-astype/Tue, 02 Dec 2025 09:30:00 +0000https://antonz.ru/accepted/errors-astype/errors.AsType — современная альтернатива errors.As.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Проверка типа ошибки через errors.AsType — современная и безопасная альтернатива errors.As.

Версия 1.26 • Станд. библиотека • Важно

Что

Новая функция errors.AsType — это generic-версия errors.As:

// go 1.13+
func As(err error, target any) bool
// go 1.26+
func AsType[E error](err error) (E, bool)

Она безопасная (type-safe), работает быстрее и проще в использовании:

// используем errors.As
var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}
// используем errors.AsType
if appErr, ok := errors.AsType[AppError](err); ok {
    fmt.Println("Got an AppError:", appErr)
}

errors.As не объявлена устаревшей (пока), но для нового кода рекомендуется использовать errors.AsType.

Зачем

Функция errors.As требует заранее объявить переменную ошибки нужного типа и передать указатель на нее:

var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}

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

var connErr *net.OpError
var dnsErr *net.DNSError

if errors.As(err, &connErr) {
    fmt.Println("Network operation failed:", connErr.Op)
} else if errors.As(err, &dnsErr) {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

С помощью generic-функции errors.AsType можно указать тип ошибки прямо при вызове. Это делает код короче и позволяет держать переменные ошибок внутри блоков if:

if connErr, ok := errors.AsType[*net.OpError](err); ok {
    fmt.Println("Network operation failed:", connErr.Op)
} else if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

Кроме того, As использует рефлексию и может вызвать панику, если использовать ее неправильно (например, если передать не указатель или тип, который не реализует error). Обычно статические анализаторы находят такие ошибки, но у AsType есть несколько преимуществ:

  • Не использует рефлексию.
  • Нет паники во время выполнения.
  • Меньше аллокаций памяти.
  • Проверка типов на этапе компиляции.
  • Работает быстрее.

Наконец, AsType умеет все то же, что и As, так что ее можно спокойно использовать вместо As в новом коде.

Подробности

Добавить в пакет errors функцию AsType:

// AsType finds the first error in err's tree that matches the type E,
// and if one is found, returns that error value and true. Otherwise, it
// returns the zero value of E and false.
//
// The tree consists of err itself, followed by the errors obtained by
// repeatedly calling its Unwrap() error or Unwrap() []error method.
// When err wraps multiple errors, AsType examines err followed by a
// depth-first traversal of its children.
//
// An error err matches the type E if the type assertion err.(E) holds,
// or if the error has a method As(any) bool such that err.As(target)
// returns true when target is a non-nil *E. In the latter case, the As
// method is responsible for setting target.
func AsType[E error](err error) (E, bool)

Рекомендовать использовать AsType вместо As:

// As finds the first error in err's tree that matches target, and if one
// is found, sets target to that error value and returns true. Otherwise,
// it returns false.
// ...
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its
// target argument rather than returning the matching error and doesn't require
// its target argument to implement error.
// ...
func As(err error, target any) bool

Пример

Открыть файл и проверить, связана ли ошибка с путем к файлу:

// go 1.25
var pathError *fs.PathError
if _, err := os.Open("non-existing"); err != nil {
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing
// go 1.26
if _, err := os.Open("non-existing"); err != nil {
    if pathError, ok := errors.AsType[*fs.PathError](err); ok {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing

Ссылки

𝗣 51945 • 𝗖𝗟 707235

]]>
Курс: Знакомство с Gohttps://antonz.ru/go-intro/Sat, 29 Nov 2025 16:00:00 +0000https://antonz.ru/go-intro/Для всех, кто уверенно программирует на другом языке и хочет попробовать Go.Я опубликовал бесплатный вводный курс по Go. Он идеально подходит для свитчеров — тех, кто уже уверенно программирует на другом языке и хочет попробовать Go.

В отличие от обычных курсов-знакомств, тут никто не рассказывает, что такое переменная и чем она отличается от цикла. Бестолковых задач вроде «что напечатает функция» тоже нет. Все кратко и по делу.

Фрагмент урока о горутинах
Фрагмент урока о горутинах

Освоить язык целиком по курсу не получится, а вот понять «это вообще мое или нет» — запросто. Если «ваше» — дальше можно пройти и полный курс.

Часть 1. Основы

  • 1.1 О курсе
  • 1.2 Базовые конструкции
  • 1.3 Массивы и карты
  • 1.4 Функции и указатели
  • 1.5 Структуры и методы
  • 1.6 Резюме

Часть 2. Другие темы

  • 2.1 О модуле
  • 2.2 Пакеты и модули
  • 2.3 Дженерики
  • 2.4 Горутины
  • 2.5 Текст
  • 2.6 Резюме

Лекции текстовые, видео нет. Я умею понятно объяснять сложные вещи, так что читать их приятно. Много примеров, нет сухой теории. Нет ИИ-генерированного контента — все лекции написаны лично мной.

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

Используется версия Go 1.25.

Записаться на курс

]]>
Go-фича: Метрики горутинhttps://antonz.ru/accepted/goroutine-metrics/Wed, 26 Nov 2025 12:00:00 +0000https://antonz.ru/accepted/goroutine-metrics/Подробные метрики горутин от рантайма.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Подробные метрики горутин от рантайма.

Версия 1.26 • Станд. библиотека • Полезно

Что

Новые метрики в пакете runtime/metrics позволяют лучше понять, как работают горутины:

  • Общее количество горутин с начала работы программы.
  • Количество горутин в каждом состоянии.
  • Количество активных потоков.

Зачем

Пакет runtime/metrics уже предоставляет много информации о работе программы, но в нем не хватало метрик по состояниям горутин и количеству потоков.

Метрики по состояниям горутин помогают находить типичные проблемы в продакшене. Например, если растет число горутин в состоянии waiting, это может указывать на проблемы с блокировками. Много горутин в состоянии not-in-go — значит, они застряли в системных вызовах или cgo. Если увеличивается очередь runnable — CPU не справляются с нагрузкой.

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

Как

Добавить следующие метрики в пакет 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.

Сумма значений показателей по каждому состоянию не обязательно равна общему количеству активных горутин (метрика /sched/goroutines:goroutines, доступна в Go 1.16+).

Все метрики используют счетчики типа uint64.

Пример

Запускаем несколько горутин и выводим метрики через 100 мс работы:

func main() {
	go work() // omitted for brevity
	time.Sleep(100 * time.Millisecond)

	fmt.Println("Goroutine metrics:")
	printMetric("/sched/goroutines-created:goroutines", "Created")
	printMetric("/sched/goroutines:goroutines", "Live")
	printMetric("/sched/goroutines/not-in-go:goroutines", "Syscall/CGO")
	printMetric("/sched/goroutines/runnable:goroutines", "Runnable")
	printMetric("/sched/goroutines/running:goroutines", "Running")
	printMetric("/sched/goroutines/waiting:goroutines", "Waiting")

	fmt.Println("Thread metrics:")
	printMetric("/sched/gomaxprocs:threads", "Max")
	printMetric("/sched/threads/total:threads", "Live")
}

func printMetric(name string, descr string) {
	sample := []metrics.Sample{{Name: name}}
	metrics.Read(sample)
    // Предполагаем, что значение типа uint64; так делать в продакшене не стоит.
    // Лучше действовать в соответствии с sample[0].Value.Kind.
	fmt.Printf("  %s: %v\n", descr, sample[0].Value.Uint64())
}
Goroutine metrics:
  Created: 52
  Live: 12
  Syscall/CGO: 0
  Runnable: 0
  Running: 4
  Waiting: 8
Thread metrics:
  Max: 8
  Live: 4

Никаких сюрпризов: новые значения метрик читаем так же, как и раньше — с помощью metrics.Read.

Ссылки

𝗣 15490 • 𝗖𝗟 690397, 690398, 690399

]]>
Go-фича: Dialer с контекстомhttps://antonz.ru/accepted/net-dialer-context/Thu, 13 Nov 2025 11:30:00 +0000https://antonz.ru/accepted/net-dialer-context/Подключение к сокетам TCP, UDP, IP или Unix с таймаутом.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Методы net.Dialer для подключения по конкретным протоколам с поддержкой контекста.

Версия 1.26 • Станд. библиотека • Мелочь

Что

Тип net.Dialer подключается к сетевому адресу, используя указанный протокол (network) — TCP, UDP, IP или Unix-сокеты.

Новые методы Dialer с поддержкой контекста — DialTCP, DialUDP, DialIP и DialUnix — объединяют эффективную реализацию (как в существующих Dial-функциях) с возможностью отмены (как в Dialer.DialContext).

Зачем

В пакете net уже есть отдельные функции для разных протоколов (DialTCP, DialUDP, DialIP и DialUnix), но их сделали до появления context.Context, поэтому они не поддерживают отмену:

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
func DialUDP(network string, laddr, raddr *UDPAddr) (*UDPConn, error)
func DialIP(network string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)

С другой стороны, у типа net.Dialer есть универсальный метод DialContext. Он поддерживает отмену и может использоваться для подключения по любому из поддерживаемых протоколов:

func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)

Если вы уже знаете протокол и адрес, использовать DialContext менее эффективно, чем специализированные функции вроде DialTCP, по следующим причинам:

  • Резолв адреса: DialContext сам выполняет разрешение адреса (например, DNS-запросы и преобразование в net.TCPAddr или net.UDPAddr) на основе названия протокола и исходного адреса. Специализированные функции принимают уже готовый объект адреса, поэтому этот шаг пропускается.

  • Роутинг по протоколу: DialContext должен определить, какой протокол использовать, и направить вызов нужному обработчику. Специализированные функции уже знают, какой протокол нужен, и этот шаг тоже пропускается.

Таким образом, функции net.Dial* работают быстрее, но не поддерживают отмену операций. Тип net.Dialer поддерживает отмену, но работает медленнее. Решение — добавить в Dialer методы, которые учитывают контекст и работают с конкретными протоколами.

Заодно это позволит использовать новые типы адресов из пакета netip (например, netip.AddrPort вместо net.TCPAddr), которые более предпочтительны в современном Go-коде.

Как

Добавить четыре новых метода в net.Dialer:

DialTCP(ctx context.Context, network string, laddr, raddr netip.AddrPort) (*TCPConn, error)
DialUDP(ctx context.Context, network string, laddr, raddr netip.AddrPort) (*UDPConn, error)
DialIP(ctx context.Context, network string, laddr, raddr netip.Addr) (*IPConn, error)
DialUnix(ctx context.Context, network string, laddr, raddr *UnixAddr) (*UnixConn, error)

Сигнатуры методов похожи на существующие функции пакета net, но дополнительно принимают контекст и используют новые типы адресов из пакета netip.

Пример

Используем метод DialTCP, чтобы подключиться к TCP-серверу:

var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Подключение не пройдет, потому что сервер не запущен.
raddr := netip.MustParseAddrPort("127.0.0.1:12345")
conn, err := d.DialTCP(ctx, "tcp", netip.AddrPort{}, raddr)
if err != nil {
    log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()

if _, err := conn.Write([]byte("Hello, World!")); err != nil {
    log.Fatal(err)
}
Failed to dial: dial tcp 127.0.0.1:12345: connect: connection refused (exit status 1)

Используем метод DialUnix, чтобы подключиться к Unix-сокету:

var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Подключение не пройдет, потому что сервер не запущен.
raddr := &net.UnixAddr{Name: "/path/to/unix.sock", Net: "unix"}
conn, err := d.DialUnix(ctx, "unix", nil, raddr)
if err != nil {
    log.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()

if _, err := conn.Write([]byte("Hello, socket!")); err != nil {
    log.Fatal(err)
}
Failed to dial: dial unix /path/to/unix.sock: connect: no such file or directory (exit status 1)

В обоих случаях подключение не проходит, потому что я не запустил сервер в песочнице :)

Ссылки

𝗣 49097 • 𝗖𝗟 657296

]]>
Go-фича: Сравнение IP-подсетейhttps://antonz.ru/accepted/netip-prefix-compare/Mon, 20 Oct 2025 08:30:00 +0000https://antonz.ru/accepted/netip-prefix-compare/Как это делают IANA и Python.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Сравнивать префиксы IP-адресов так же, как это делает IANA (администрация адресного пространства интернета).

Версия 1.26 • Станд. библиотека • Мелочь

Что

Префикс IP-адреса задает определенную подсеть. Обычно такие префиксы записывают в формате CIDR:

10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
203.0.113.0/24

В Go IP-префикс представлен типом netip.Prefix.

Новый метод Prefix.Compare позволяет сравнить два IP-префикса, что упрощает их сортировку — не придется писать свой код сравнения. Порядок сравнения совпадает с реализацией в Python и с порядком, который использует IANA.

Зачем

Когда команда Go изначально разрабатывала тип IP-подсети (net/netip.Prefix), они решили не добавлять метод Compare, потому что не было общепринятого способа упорядочивать такие значения.

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

Чтобы улучшить ситуацию, в Go добавят стандартный способ сравнения IP-префиксов. Это должно уменьшить количество шаблонного кода и помочь программам сортировать IP-подсети одинаково.

Подробности

Добавить метод Compare в тип netip.Prefix:

// Compare возвращает целое число - результат сравнения двух префиксов.
// Результат будет 0, если p == p2, -1, если p < p2, и +1, если p > p2.
func (p Prefix) Compare(p2 Prefix) int

Вот как Compare сравнивает префиксы:

  • Сначала по корректности (некорректные идут перед корректными).
  • Потом по семейству адресов (IPv4 перед IPv6).
    10.0.0.0/8 < ::/8
  • Потом по маскированному IP-адресу (IP подсети).
    10.0.0.0/8 < 10.0.1.0/8
  • Потом по длине префикса.
    10.0.0.0/8 < 10.0.0.0/16
  • Потом по немаскированному адресу (оригинальный IP).
    10.0.0.0/8 < 10.0.0.1/8

Это тот же порядок, что использует netaddr.IPNetwork в Python и IANA.

Пример

Отсортировать список IP-префиксов:

prefixes := []netip.Prefix{
    netip.MustParsePrefix("10.0.1.0/8"),
    netip.MustParsePrefix("203.0.113.0/24"),
    netip.MustParsePrefix("10.0.0.0/8"),
    netip.MustParsePrefix("169.254.0.0/16"),
    netip.MustParsePrefix("203.0.113.0/8"),
}

slices.SortFunc(prefixes, func(a, b netip.Prefix) int {
    return a.Compare(b)
})

for _, p := range prefixes {
    fmt.Println(p.String())
}
10.0.0.0/8
10.0.1.0/8
169.254.0.0/16
203.0.113.0/8
203.0.113.0/24

Ссылки

𝗣 61642 • 𝗖𝗟 700355

]]>
Go-фича: Хешерыhttps://antonz.ru/accepted/maphash-hasher/Sun, 28 Sep 2025 12:30:00 +0000https://antonz.ru/accepted/maphash-hasher/Стандартный подход к хешированию и проверке на равенство в коллекциях.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Стандартный подход к вычислению хеша и проверке на равенство в пользовательских структурах данных.

Версия 1.26 • Станд. библиотека • Полезно

Что

Новый интерфейс maphash.Hasher — это стандартный способ хешировать и сравнивать элементы в пользовательских коллекциях, например, в собственных картах или множествах.

// Hasher реализует хеширование и проверку на равенство для типа T.
type Hasher[T any] interface {
    Hash(hash *maphash.Hash, value T)
    Equal(T, T) bool
}

Тип ComparableHasher — это стандартная реализация хешера для сравнимых (comparable) типов — таких как числа, строки и структуры с сравнимыми полями.

Зачем

Пакет maphash предоставляет хеш-функции для срезов байтов и строк, но не объясняет, как создавать собственные структуры данных, основанные на хешировании.

Чтобы это исправить, в Go добавят хешер (hasher) — стандартный интерфейс для хеширования и сравнения элементов коллекции, а также его реализацию по умолчанию.

Подробности

Добавить интерфейс хешера в пакет maphash:

// Hasher - это тип, который реализует хеширование
// и сравнение на равенство для типа T.
//
// Hasher не должен иметь состояние, и его
// нулевое значение должно быть валидным.
// Поэтому обычно Hasher - это пустая структура.
type Hasher[T any] interface {
    // Hash обновляет хеш в соответствии со значением value.
    //
    // Если два значения равны (Equal), их хеш тоже должен совпадать.
    // То есть, если Equal(a, b) возвращает true, то Hash(h, a) и Hash(h, b)
    // должны записывать одинаковые данные в h.
    Hash(hash *maphash.Hash, value T)
    Equal(T, T) bool
}

Добавить умолчательную реализацию хешера для сравнимых типов:

// ComparableHasher - это реализация Hasher для сравнимых типов.
// Его метод Equal(x, y) логически соответствует x == y.
type ComparableHasher[T comparable] struct{}
func (h ComparableHasher[T]) Hash(hash *Hash, value T)
func (h ComparableHasher[T]) Equal(x, y T) bool

Пример

Вот нечувствительный к регистру строк хешер:

// CaseInsensitive - это хешер для сравнения строк без учета регистра.
type CaseInsensitive struct{}

func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
    // Используем lowercase вместо case folding для простоты.
    h.WriteString(strings.ToLower(s))
}

func (CaseInsensitive) Equal(a, b string) bool {
    return strings.ToLower(a) == strings.ToLower(b)
}

И дженерик-множество Set, с настраиваемым хешером для проверки на равенство и хеширования:

// Set - это универсальная реализация множества
// с возможностью подставить свой хешер.
// Значения хранятся в карте корзин (bucket),
// корзины идентифицируются по хешу от значения.
// Если несколько значений попадают в одну и ту же корзину,
// используется линейный поиск, чтобы найти нужное.
type Set[H maphash.Hasher[V], V any] struct {
    seed   maphash.Seed   // случайный посев для хеширования
    hasher H              // выполняет хеширование и проверку на равенство
    data   map[uint64][]V // корзины значений, индексированные по хешу
}

// NewSet создает новый экземпляр Set с указанной функцией хеширования.
func NewSet[H maphash.Hasher[V], V any](hasher H) *Set[H, V] {
    return &Set[H, V]{
        seed:   maphash.MakeSeed(),
        hasher: hasher,
        data:   make(map[uint64][]V),
    }
}

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

// calcHash вычисляет хеш значения.
func (s *Set[H, V]) calcHash(val V) uint64 {
    var h maphash.Hash
    h.SetSeed(s.seed)
    s.hasher.Hash(&h, val)
    return h.Sum64()
}

Этот хеш используется в методах Has и Add. Он служит ключом в карте корзин, чтобы найти нужную корзину для значения.

Метод Has проверяет, есть ли значение в соответствующей корзине:

// Has возвращает true, если указанное значение есть в множестве.
func (s *Set[H, V]) Has(val V) bool {
    hash := s.calcHash(val)
    if bucket, ok := s.data[hash]; ok {
        for _, item := range bucket {
            if s.hasher.Equal(val, item) {
                return true
            }
        }
    }
    return false
}

Add добавляет значение в соответствующую корзину:

// Add добавляет значение в множество, если его там еще нет.
func (s *Set[H, V]) Add(val V) {
    if s.Has(val) {
        return
    }
    hash := s.calcHash(val)
    s.data[hash] = append(s.data[hash], val)
}

Теперь мы можем создать регистро-независимое множество строк:

func main() {
    set := NewSet(CaseInsensitive{})

    set.Add("hello")
    set.Add("world")

    fmt.Println(set.Has("HELLO")) // true
    fmt.Println(set.Has("world")) // true
}
true
true

Или обычное множество строк с помощью maphash.ComparableHasher:

func main() {
    set := NewSet(maphash.ComparableHasher[string]{})

    set.Add("hello")
    set.Add("world")

    fmt.Println(set.Has("HELLO")) // false
    fmt.Println(set.Has("world")) // true
}
false
true

Ссылки

𝗣 70471 • 𝗖𝗟 657296

]]>
Go-фича: new(expr)https://antonz.ru/accepted/new-expr/Wed, 24 Sep 2025 07:50:00 +0000https://antonz.ru/accepted/new-expr/Использовать new для выражений.Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Использовать встроенную функцию new для выражений.

Версия 1.26 • Язык • Важно

Что

Раньше new можно было использовать только с типами:

p1 := new(int)
fmt.Println(*p1)
// 0

А теперь можно и с выражениями:

// Указатель на переменную типа int со значением 42.
p := new(42)
fmt.Println(*p)
// 42

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

Зачем

Есть простой способ создать указатель на составной литерал:

type Person struct { name string }
p := &Person{name: "alice"}

Но нет простого способа создать указатель на значение простого типа:

n := 42
p := &n

Теперь это исправят.

Подробности

Обновить раздел Allocation в спецификации языка следующим образом (далее перевод с английского):

Выделение памяти

Встроенная функция new создает новую переменную, инициализирует ее и возвращает указатель на нее. Она принимает один аргумент — это может быть выражение или тип.

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

➁ Если аргумент — это тип T, то new(T) выделяет переменную, инициализированную нулевым значением типа T.

Например, new(123) и new(int) оба возвращают указатель на новую переменную типа int. Значение первой переменной — 123, второй — 0.

➀ — это новая фича, ➁ уже работает как описано.

Примеры

Указатель на простой тип:

// go 1.25
n := 42
p1 := &n
fmt.Println(*p1)

s := "go"
p2 := &s
fmt.Println(*p2)
42
go
// go 1.26
p1 := new(42)
fmt.Println(*p1)

s := "go"
p2 := new(s)
fmt.Println(*p2)
42
go

Указатель на составное значение:

// go 1.25
s := []int{11, 12, 13}
p1 := &s
fmt.Println(*p1)

type Person struct{ name string }
p2 := &Person{name: "alice"}
fmt.Println(*p2)
[11 12 13]
{alice}
// go 1.26
p1 := new([]int{11, 12, 13})
fmt.Println(*p1)

type Person struct{ name string }
p2 := new(Person{name: "alice"})
fmt.Println(*p2)
[11 12 13]
{alice}

Указатель на результат вызова функции:

// go 1.25
f := func() string { return "go" }
v := f()
p := &v
fmt.Println(*p)
go
// go 1.26
f := func() string { return "go" }
p := new(f())
fmt.Println(*p)
go

Передавать nil по-прежнему нельзя:

// go 1.25 and go 1.26
p := new(nil)
// compilation error

Ссылки

𝗣 45624 • 𝗖𝗟 704935, 704737, 704955, 705157

]]>
Выразительные тесты без testify/asserthttps://antonz.ru/do-not-testify/Wed, 16 Jul 2025 07:00:00 +0000https://antonz.ru/do-not-testify/Equal, Err и True — их вполне достаточно.Многие Go-разработчики предпочитают ассерты (asserts, проверки в тестах) без if, чтобы тесты были короче и понятнее. Вместо того чтобы писать if с t.Errorf:

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    age, err := db.Str().Get("age")
    if !errors.Is(err, redka.ErrNotFound) {
        t.Errorf("want ErrNotFound, got %v", err)
    }
    if age != nil {
        t.Errorf("want nil, got %v", age)
    }
}
PASS

Они используют пакет testify/assert (или его злодейского близнеца testify/require):

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    age, err := db.Str().Get("age")
    assert.ErrorIs(t, err, redka.ErrNotFound)
    assert.Nil(t, age)
}

Но я не думаю, что для хороших тестов действительно нужен testify/assert с его 40 разными ассертами. Расскажу о другом способе.

Пакет testify также содержит моки и тест-сьюты. Мы не будем их рассматривать — поговорим только про ассерты.

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

Самая распространенная проверка в тестах — проверка на равенство:

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    name, _ := db.Str().Get("name") // отложим пока проверку ошибок
    if name.String() != "alice" {
        t.Errorf("want 'alice', got '%v'", name)
    }
}
PASS

Давайте напишем простую дженерик-функцию для таких проверок:

// AssertEqual проверяет, что got равно want.
func AssertEqual[T any](tb testing.TB, got T, want T) {
    tb.Helper()

    // Проверяем, что оба равны nil.
    if isNil(got) && isNil(want) {
        return
    }

    // Иначе сравниваем через рефлексию.
    if reflect.DeepEqual(got, want) {
        return
    }

    // Нет совпадения, сообщаем о проблеме.
    tb.Errorf("want %#v, got %#v", want, got)
}

Приходится использовать вспомогательную функцию isNil, потому что компилятор не позволит сравнивать типизированное значение T с нетипизированным nil:

// isNil проверяет, что v равно nil.
func isNil(v any) bool {
    if v == nil {
        return true
    }

    // Интерфейс может быть не nil, но содержать nil,
    // поэтому проверяем внутреннее значение.
    rv := reflect.ValueOf(v)
    switch rv.Kind() {
    case reflect.Chan, reflect.Func, reflect.Interface,
        reflect.Map, reflect.Pointer, reflect.Slice,
        reflect.UnsafePointer:
        return rv.IsNil()
    default:
        return false
    }
}

Применим ассерт в нашем тесте:

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    name, _ := db.Str().Get("name")
    AssertEqual(t, name.String(), "alice")
}
PASS

Порядок параметров в AssertEqual такой: (got, want), а не (want, got), как в testify. Так проще читается — ведь в жизни мы говорим «ее зовут Алиса», а не «Алиса — это ее имя».

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

AssertEqual уже отлично подходит для любых проверок на равенство — а это, по моему опыту, до 70% всех проверок в тестах. Неплохо для альтернативы testify размером в 20 строк! Но мы можем сделать ее еще лучше, так что давайте не упускать этот шанс.

Во-первых, типы вроде time.Time и net.IP предоставляют специальный метод Equal. Следует его использовать, чтобы сравнение было точным:

// equaler - интерфейс для типов с методом Equal
// (вроде time.Time или net.IP).
type equaler[T any] interface {
    Equal(T) bool
}

// areEqual проверяет, что a равно b.
func areEqual[T any](a, b T) bool {
    // Проверяем, что оба равны nil.
    if isNil(a) && isNil(b) {
        return true
    }

    // Пробуем сравнить с помощью метода Equal.
    if eq, ok := any(a).(equaler[T]); ok {
        return eq.Equal(b)
    }

    // Иначе сравниваем через рефлексию.
    return reflect.DeepEqual(a, b)
}

Во-вторых, мы можем быстро сравнивать байтовые срезы через bytes.Equal:

// areEqual проверяет, что a равно b.
func areEqual[T any](a, b T) bool {
    // ...

    // Особый случай для байтовых срезов.
    if aBytes, ok := any(a).([]byte); ok {
        bBytes := any(b).([]byte)
        return bytes.Equal(aBytes, bBytes)
    }

    // ...
}

Наконец, будем вызывать функцию areEqual внутри AssertEqual:

// AssertEqual проверяет, что got равно want.
func AssertEqual[T any](tb testing.TB, got T, want T) {
    tb.Helper()
    if areEqual(got, want) {
        return
    }
    tb.Errorf("want %#v, got %#v", want, got)
}

Посмотрим, как работает:

func Test(t *testing.T) {
    // date1 и date2 обозначают одно и то же время.
    date1 := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
    date2 := time.Date(2025, 1, 1, 5, 0, 0, 0, time.FixedZone("UTC+5", 5*3600))
    AssertEqual(t, date1, date2) // ok

    // b1 и b2 - одинаковые байтовые срезы.
    b1 := []byte("abc")
    b2 := []byte{97, 98, 99}
    AssertEqual(t, b1, b2) // ok

    // m1 и m2 - отличающиеся карты.
    m1 := map[string]int{"age": 25}
    m2 := map[string]int{"age": 42}
    AssertEqual(t, m1, m2) // fail
}
FAIL: Test (0.00s)
main_test.go:47: want map[string]int{"age":42}, got map[string]int{"age":25}

Работает отлично!

Проверка ошибок

Ошибки в Go используются постоянно, поэтому их проверка — важная часть тестирования:

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    _, err := db.Str().Get("name") // игнорируем значение для краткости
    if err != nil {
        t.Errorf("unexpected error: %v'", err)
    }
}
PASS

Проверка ошибок, по моей оценке, составляет до 30% всех ассертов, поэтому вынесем ее в отдельную функцию.

Сначала реализуем базовые сценарии — когда ошибки не должно быть, и когда ожидается ошибка:

// AssertErr проверяет, что ошибка got соответствует want.
func AssertErr(tb testing.TB, got error, want error) {
    tb.Helper()

    // Ожидали nil ошибку, но получили не-nil.
    // Это критическая проблема, поэтому сразу завершаем тест.
    if want == nil && got != nil {
        tb.Fatalf("unexpected error: %v", got)
        return
    }

    // Ожидали не-nil ошибку, а получили nil.
    if want != nil && got == nil {
        tb.Errorf("want error, got <nil>")
        return
    }

    // Остальное сделаем чуть позже.
    return
}

Обычно мы не останавливаем тест, если отдельный ассерт упал. Это позволяет увидеть все проблемы разом, а не выискивать их по одной. Единственное исключение — ситуация unexpected error в коде выше (ожидали nil, получили не-nil). Здесь тест сразу завершается, потому что следующие проверки, скорее всего, уже не имеют смысла и могут вызвать панику.

Посмотрим, как работает:

func Test(t *testing.T) {
    db := getDB(t)
    db.Str().Set("name", "alice")

    _, err := db.Str().Get("name")
    AssertErr(t, err, nil)
}
PASS

Пока неплохо. Теперь реализуем остальные проверки, но без создания отдельных функций (ErrorIs, ErrorAs, ErrorContains и т.д.), как это делает testify.

Если want — ошибка (тип error), используем errors.Is, чтобы проверить, совпадает ли ошибка с ожидаемым значением:

// AssertErr проверяет, что ошибка got соответствует want.
func AssertErr(tb testing.TB, got error, want any) {
    tb.Helper()

    if want != nil && got == nil {
        tb.Error("want error, got <nil>")
        return
    }

    switch w := want.(type) {
    case nil:
        if got != nil {
            tb.Fatalf("unexpected error: %v", got)
        }
    case error:
        if !errors.Is(got, w) {
            tb.Errorf("want %T(%v), got %T(%v)", w, w, got, got)
        }
    default:
        tb.Errorf("unsupported want type: %T", want)
    }
}

Пример:

func Test(t *testing.T) {
    err := &fs.PathError{
        Op: "open",
        Path: "file.txt",
        Err: fs.ErrNotExist,
    }
    AssertErr(t, err, fs.ErrNotExist)
}
PASS

Если want — строка, проверим, что сообщение об ошибке содержит ожидаемый текст:

// AssertErr проверяет, что ошибка got соответствует want.
func AssertErr(tb testing.TB, got error, want any) {
    // ...
    switch w := want.(type) {
    case string:
        if !strings.Contains(got.Error(), w) {
            tb.Errorf("want %q, got %q", w, got.Error())
        }
    //...
    }
}

Пример:

func Test(t *testing.T) {
    _, err := regexp.Compile("he(?o") // invalid
    AssertErr(t, err, "invalid or unsupported")
}
PASS

Наконец, если want — тип, используем errors.As, чтобы проверить, совпадает ли тип ошибки с ожидаемым:

// AssertErr проверяет, что ошибка got соответствует want.
func AssertErr(tb testing.TB, got error, want any) {
    // ...
    switch w := want.(type) {
    case reflect.Type:
        target := reflect.New(w).Interface()
        if !errors.As(got, target) {
            tb.Errorf("want %s, got %T", w, got)
        }
    //...
    }
}

Пример:

func Test(t *testing.T) {
    got := &fs.PathError{
        Op: "open",
        Path: "file.txt",
        Err: fs.ErrNotExist,
    }

    var want *fs.PathError
    AssertErr(t, got, reflect.TypeOf(want))

    // Или так.
    AssertErr(t, got, reflect.TypeFor[*fs.PathError]())
}
PASS

И вот что еще. Хотелось бы иметь возможность проверить, что произошла просто какая-то ошибка, не проверяя ее значение или тип (так работает Error в testify). Чтобы поддержать такой сценарий, сделаем параметр want необязательным:

// AssertErr проверяет, что ошибка got соответствует любому из wants.
func AssertErr(tb testing.TB, got error, wants ...any) {
    tb.Helper()

    // Если wants не указаны, ожидаем, что got будет не-nil ошибкой.
    if len(wants) == 0 {
        if got == nil {
            tb.Error("want error, got <nil>")
        }
        return
    }

    // Здесь для простоты сравниваем got только с первым want.
    // Можно (и стоило бы) сравнивать с каждым из wants.
    want := wants[0]
    // ...
}

Пример:

func Test(t *testing.T) {
    _, err := regexp.Compile("he(?o") // invalid
    AssertErr(t, err) // хотим не-nil ошибку
}
PASS

Теперь AssertErr обрабатывает все нужные нам случаи:

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

И все это в 40 строк кода. Неплохо, правда?

Другие проверки

AssertEqual и AssertErr покрывают 85-95% тестовых проверок в типичном проекте. Но все равно остаются эти злосчастные 5-15%.

Например, нам могут понадобиться такие проверки:

func Test(t *testing.T) {
    s := "go is awesome"
    if len(s) < 5 {
        t.Error("too short")
    }

    if !strings.Contains(s, "go") {
        t.Error("too weak")
    }
}
PASS

Технически, можно использовать AssertEqual. Но выглядит это так себе:

func Test(t *testing.T) {
    s := "go is awesome"
    AssertEqual(t, len(s) >= 5, true)
    AssertEqual(t, strings.Contains(s, "go"), true)
}
PASS

Поэтому давайте добавим третий и последний ассерт — AssertTrue. Эта функция самая простая:

// AssertTrue проверяет, что got истинно.
func AssertTrue(tb testing.TB, got bool) {
    tb.Helper()
    if !got {
        tb.Error("not true")
    }
}

Теперь логические проверки выглядят лучше:

func Test(t *testing.T) {
    s := "go is awesome"
    AssertTrue(t, len(s) >= 5)
    AssertTrue(t, strings.Contains(s, "go"))
}
PASS

Неплохо!

Заключение

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

Я постоянно использую «трио ассертов» — Equal, Err и True — в своих проектах. Поэтому вынес их в отдельный мини-пакет github.com/nalgeon/be. Если вам понравился описанный в статье подход — попробуйте и вы!

]]>
Интерактивный тур по Go 1.25https://antonz.ru/go-1-25/Thu, 26 Jun 2025 17:30:00 +0000https://antonz.ru/go-1-25/Фейковые часы, новый GC, бортовой регистратор и многое другое.Go 1.25 уже вышел, так что сейчас самое время изучить, что нового. Официальные заметки к релизу сухие как прошлогодние сухари, поэтому я подготовил интерактивную версию с множеством примеров.

synctestjson/v2GOMAXPROCSНовый GCАнти-CSRFWaitGroup.GoFlightRecorderos.Rootreflect.TypeAssertT.Attrslog.GroupAttrshash.Cloner

Статья основана на официальных заметках к релизу от команды Go. Они распространяются под лицензией BSD-3-Clause. Это не полный список изменений; если нужен полный — смотрите официальную документацию.

По тексту вы увидите ссылки на спецификации (𝗣) и коммиты (𝗖𝗟) для описанных фич. Посмотрите их, если хотите узнать больше о причинах и деталях изменений.

Чтобы не усложнять код, в примерах часто отсутствует обработка ошибок. Не делайте так в продакшене ツ

Поехали!

# Синтетическое время в тестах

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

// Read читает значение из входного канала и возвращает его.
// Завершается по таймауту через 60 секунд.
func Read(in chan int) (int, error) {
    select {
    case v := <-in:
        return v, nil
    case <-time.After(60 * time.Second):
        return 0, errors.New("timeout")
    }
}

Вызываем ее так:

func main() {
    ch := make(chan int)
    go func() {
        ch <- 42
    }()
    val, err := Read(ch)
    fmt.Printf("val=%v, err=%v\n", val, err)
}
val=42, err=<nil>

Как проверить ситуацию с таймаутом? Конечно, мы не хотим, чтобы тест действительно ждал 60 секунд. Мы могли бы сделать таймаут параметром функции (наверное, так и стоило бы), но допустим, что это не вариант.

Новый пакет synctest приходит на помощь! Функция synctest.Test выполняет изолированный «пузырь». Внутри пузыря функции пакета time используют искусственные часы, что позволяет тесту пройти мгновенно:

func TestReadTimeout(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        ch := make(chan int)
        _, err := Read(ch)
        if err == nil {
            t.Fatal("expected timeout error, got nil")
        }
    })
}
PASS

Начальное время в пузыре — полночь 2000-01-01 UTC. Время идет вперед, когда все горутины в пузыре заблокированы. В нашем примере, когда единственная горутина заблокирована на select в Read, часы пузыря прыгают на 60 секунд вперед, что вызывает срабатывание в селекте ветки таймаута.

Имейте в виду, что t, передаваемый во внутреннюю функцию — не совсем обычный testing.T. В частности, никогда не вызывайте на нем T.Run, T.Parallel или T.Deadline:

func TestSubtest(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        t.Run("subtest", func (t *testing.T) {
            t.Log("ok")
        })
    })
}
panic: testing: t.Run called inside synctest bubble [recovered, repanicked]

Как видите, дочерние тесты внутри пузыря запустить не получится.

Другая полезная функция — synctest.Wait. Она ждет, пока все горутины в пузыре заблокируются, а затем продолжает выполнение:

func TestWait(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        var innerStarted bool
        done := make(chan struct{})

        go func() {
            innerStarted = true
            time.Sleep(time.Second)
            close(done)
        }()

        // Ждем, пока внутренняя горутина заблокируется на time.Sleep.
        synctest.Wait()
        // Гарантируется, что здесь innerStarted равно true.
        fmt.Printf("inner started: %v\n", innerStarted)

        <-done
    })
}
inner started: true

Попробуйте закомментировать вызов Wait() и посмотрите, как изменится значение innerStarted.

Пакет testing/synctest впервые появился как экспериментальный в версии 1.24. Сейчас он считается стабильным и готов к использованию. Обратите внимание, что функция Run, которая была добавлена в 1.24, теперь устарела. Вместо нее следует использовать Test.

𝗣 67434, 73567 • 𝗖𝗟 629735, 629856, 671961

# JSON v2

Вторая версия пакета json — это большое обновление, в котором полно несовместимых изменений. Поэтому я написал отдельный пост с подробным разбором изменений и множеством интерактивных примеров.

Здесь покажу только одну из самых впечатляющих функций.

В json/v2 вы больше не ограничены одним маршалером для конкретного типа. Теперь можно создавать кастомные маршалеры и анмаршалеры «на лету» — с помощью универсальных функций MarshalToFunc и UnmarshalFromFunc.

Например, можно преобразовать логические значения (true/false) и «логические» строки (on/off) в значения или — не создав при этом ни одного типа!

Сначала создадим маршалер для логических значений:

// Преобразует значения true/false в ✓ или ✗.
boolMarshaler := json.MarshalToFunc(
    func(enc *jsontext.Encoder, val bool) error {
        if val {
            return enc.WriteToken(jsontext.String("✓"))
        }
        return enc.WriteToken(jsontext.String("✗"))
    },
)

Затем маршалер для «логических» строк:

// Преобразует строки вида "true"/"false" в ✓ или ✗.
strMarshaler := json.MarshalToFunc(
    func(enc *jsontext.Encoder, val string) error {
        if val == "on" || val == "true" {
            return enc.WriteToken(jsontext.String("✓"))
        }
        if val == "off" || val == "false" {
            return enc.WriteToken(jsontext.String("✗"))
        }
        // SkipFunc — специальная ошибка, которая инструктирует Go пропустить
        // текущий маршалер и перейти к следующему. В нашем случае
        // следующим будет стандартный маршалер для строк.
        return json.SkipFunc
    },
)

Наконец, объединим маршалеры с помощью JoinMarshalers и передадим их в функцию маршалинга через опцию WithMarshalers:

// Объединяем маршалеры с помощью JoinMarshalers.
marshalers := json.JoinMarshalers(boolMarshaler, strMarshaler)

// Кодируем в JSON несколько значений.
vals := []any{true, "off", "hello"}
data, err := json.Marshal(vals, json.WithMarshalers(marshalers))
fmt.Println(string(data), err)
["✓","✗","hello"] <nil>

Здорово, правда?

В json/v2 еще много полезного: поддержка I/O-читателей и писателей, инлайнинг вложенных объектов, куча разных настроек и заметный прирост производительности. Поэтому еще раз советую посмотреть пост, посвященный изменениям во второй версии.

𝗣 63397, 71497

# GOMAXPROCS для контейнеров

Параметр рантайма GOMAXPROCS определяет максимальное количество потоков операционной системы, которые планировщик Go может использовать для одновременного выполнения горутин. Начиная с Go 1.5, по умолчанию он равен значению runtime.NumCPU, то есть количеству логических CPU на машине (точнее, это либо общее число логических CPU, либо число, разрешенное маской аффинности процессора, если оно меньше).

Например, на моем ноутбуке с 8 ядрами значение GOMAXPROCS по умолчанию тоже равно 8:

maxProcs := runtime.GOMAXPROCS(0) // возвращает текущее значение
fmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", maxProcs)
NumCPU: 8
GOMAXPROCS: 8

Программы на Go часто запускаются в контейнерах, например, под управлением Docker или Kubernetes. В этих системах можно ограничить использование процессора для контейнера с помощью функции Linux, которая называется cgroups.

Cgroup (control group) в Linux позволяет объединять процессы в группы и управлять тем, сколько процессорного времени, памяти и сетевых ресурсов они могут использовать, устанавливая лимиты и приоритеты.

Например, вот как можно ограничить контейнер Docker четырьмя CPU:

docker run --cpus=4 golang:1.24-alpine go run /app/nproc.go

До версии 1.25 рантайм Go не учитывал ограничение по CPU (CPU-квоту) при установке значения GOMAXPROCS. Как бы вы ни ограничивали ресурсы процессора, GOMAXPROCS всегда устанавливался равным количеству CPU на хосте.

docker run --cpus=4 golang:1.24-alpine go run /app/nproc.go
NumCPU: 8
GOMAXPROCS: 8

Теперь рантайм Go runtime начал учитывать CPU-квоту:

docker run --cpus=4 golang:1.25rc1-alpine go run /app/nproc.go
NumCPU: 8
GOMAXPROCS: 4

Значение по умолчанию для GOMAXPROCS равно либо общему количеству CPU, либо лимиту CPU, заданному через cgroup для процесса — выбирается меньшее из этих двух значений.

Дробные значения лимита округляются в большую сторону:

docker run --cpus=2.3 golang:1.25rc1-alpine go run /app/nproc.go
NumCPU: 8
GOMAXPROCS: 3

На многоядерной машине минимальное умолчательное значение GOMAXPROCS равно 2, даже если лимит на CPU установлен меньше:

docker run --cpus=1 golang:1.25rc1-alpine go run /app/nproc.go
NumCPU: 8
GOMAXPROCS: 2

Если лимит CPU изменяется, рантайм автоматически обновляет значение GOMAXPROCS. Сейчас это происходит не чаще одного раза в секунду (реже, если приложение простаивает).

Ограничение CPU

Cgroups позволяют ограничивать использование процессора двумя способами:

  • Квота CPU — это максимальное время работы процессора, которое можно использовать за определенный период.
  • Доли CPU — это относительный приоритет использования процессора, который задается для планировщика ядра.

В Docker опции --cpus и --cpu-period/--cpu-quota задают квоту, а --cpu-shares задает доли.

В Kubernetes CPU limit задает квоту, а CPU request задает доли.

В Go GOMAXPROCS учитывает только CPU-квоту, но не доли.

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

runtime.GOMAXPROCS(4)
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
GOMAXPROCS: 4

Установленное вручную значение GOMAXPROCS можно отменить. Чтобы вернуть значение по умолчанию, используйте новую функцию runtime.SetDefaultGOMAXPROCS:

GOMAXPROCS=2 go1.25rc1 run nproc.go
// Используем переменную окружения.
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

// Используем ручное значение.
runtime.GOMAXPROCS(4)
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

// Возвращаем значение по умолчанию.
runtime.SetDefaultGOMAXPROCS()
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
GOMAXPROCS: 2
GOMAXPROCS: 4
GOMAXPROCS: 8

Чтобы сохранить обратную совместимость, новое поведение GOMAXPROCS работает только если в go.mod указана версия Go 1.25 или выше. Также его можно отключить вручную с помощью настроек GODEBUG:

  • containermaxprocs=0 — игнорировать CPU-квоту.
  • updatemaxprocs=0 — не обновлять GOMAXPROCS автоматически.

𝗣 73193 • 𝗖𝗟 668638, 670497, 672277, 677037

# Новый сборщик мусора

Некоторые гоферы любят пошутить про Java и ее многочисленные сборщики мусора. Теперь будет не до смеха — в Go 1.25 появился новый сборщик мусора.

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

Старый сборщик мусора сканирует память не по порядку, а скачет туда-сюда. Из-за этого все работает неоптимально, потому что много времени уходит на доступ к памяти. Проблема усугубляется на многоядерных системах с неоднородным доступом к памяти (так называемая NUMA-архитектура, когда у каждого процессора или группы процессоров есть своя «локальная» память).

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

Результаты бенчмарков разнятся, но команда Go ожидает, что в реальных программах с большим количеством GC затраты на сборку мусора снизятся на 10–40%.

Я провел небольшой тест: сделал 1 000 000 операций чтения и записи в Redka (это мой клон Redis, написанный на Go). Общее время на сборку мусора было примерно одинаковым на старом и новом алгоритмах. Но Редька, вероятно, не самый удачный пример — она в основном полагается на SQLite, а действий в Go-коде там относительно немного.

Новый сборщик мусора пока экспериментальный, включается переменной окружения GOEXPERIMENT=greenteagc при сборке. Дизайн и реализация сборщика могут измениться в будущих версиях. Подробности реализации и обратная связь по работе GC — по ссылкам ниже.

𝗣 73581Feedback

# Анти-CSRF

Новый тип http.CrossOriginProtection защищает от CSRF-атак, отклоняя небезопасные кросс-доменные запросы из браузера.

Кросс-доменные запросы определяются так:

  • Проверкой по заголовку Sec-Fetch-Site.
  • Сравнением домена в заголовке Origin с доменом в заголовке Host.

Вот пример, где мы включаем CrossOriginProtection и явно разрешаем несколько дополнительных источников:

// Регистрируем пару обработчиков.
mux := http.NewServeMux()
mux.HandleFunc("GET /get", func(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "ok\n")
})
mux.HandleFunc("POST /post", func(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "ok\n")
})

// Настраиваем защиту от CSRF-атак.
antiCSRF := http.NewCrossOriginProtection()
antiCSRF.AddTrustedOrigin("https://example.com")
antiCSRF.AddTrustedOrigin("https://*.example.com")

// Подключаем защиту ко всем обработчикам.
srv := http.Server{
    Addr:    ":8080",
    Handler: antiCSRF.Handler(mux),
}
log.Fatal(srv.ListenAndServe())

Теперь, если браузер отправит запрос с того же домена, что и сервер, сервер его разрешит:

curl --data "ok" -H "sec-fetch-site:same-origin" localhost:8080/post
ok

Если браузер отправит кросс-доменный запрос, сервер его отклонит:

curl --data "ok" -H "sec-fetch-site:cross-site" localhost:8080/post
cross-origin request detected from Sec-Fetch-Site header

Если заголовок Origin не соответствует заголовку Host, сервер отклонит запрос:

curl --data "ok" \
  -H "origin:https://evil.com" \
  -H "host:antonz.org" \
  localhost:8080/post
cross-origin request detected, and/or browser is out of date:
Sec-Fetch-Site is missing, and Origin does not match Host

Если запрос придет из доверенного источника, сервер его разрешит:

curl --data "ok" \
  -H "origin:https://example.com" \
  -H "host:antonz.org" \
  localhost:8080/post
ok

Сервер всегда разрешает методы GET, HEAD и OPTIONS, поскольку они безопасны:

curl -H "origin:https://evil.com" localhost:8080/get
ok

Сервер всегда разрешает запросы без заголовков Sec-Fetch-Site или Origin (такой запрос не придет из браузера):

curl --data "ok" localhost:8080/post
ok

𝗣 73626 • 𝗖𝗟 674936, 680396

# Go в группе ожидания

Все знают, как дождаться выполнения горутины с помощью группы ожидания:

var wg sync.WaitGroup

wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("go is awesome")
}()

wg.Add(1)
go func() {
    defer wg.Done()
    fmt.Println("cats are cute")
}()

wg.Wait()
fmt.Println("done")
cats are cute
go is awesome
done

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

var wg sync.WaitGroup

wg.Go(func() {
    fmt.Println("go is awesome")
})

wg.Go(func() {
    fmt.Println("cats are cute")
})

wg.Wait()
fmt.Println("done")
cats are cute
go is awesome
done

Реализация именно такая, как вы думаете:

// https://github.com/golang/go/blob/master/src/sync/waitgroup.go
func (wg *WaitGroup) Go(f func()) {
    wg.Add(1)
    go func() {
        defer wg.Done()
        f()
    }()
}

Забавно, что команде Go понадобилось 13 лет, чтобы добавить логичную обертку для Add+Done. Ну да ладно, лучше поздно, чем никогда!

𝗣 63796 • 𝗖𝗟 662635

# Flight recorder

Flight recording — это способ трассировки, который собирает данные о выполнении программы (например, вызовы функций и выделение памяти) со скользящим окном по времени или размеру трейса. Это помогает компактно записывать важные моменты в работе программы, даже если заранее неизвестно, когда они произойдут.

Новый тип trace.FlightRecorder реализует этот подход в Go. Он ведет трассировку в рамках ограниченного скользящего окна, там самым фиксируя только самую свежую информацию.

Вот пример использования.

Сначала настраиваем скользящее окно:

// Сохранять как минимум 5 последних секунд трассировки,
// с размером буфера не более 3 МБ.
// Это только рекомендации, а не строгие ограничения.
cfg := trace.FlightRecorderConfig{
    MinAge:   5 * time.Second,
    MaxBytes: 3 << 20, // 3MB
}

Затем создаем трассировщик и запускаем его:

// Создаем и запускаем трассировщик.
rec := trace.NewFlightRecorder(cfg)
rec.Start()
defer rec.Stop()

И пишем обычный код приложения:

// Имитируем какую-то работу.
done := make(chan struct{})
go func() {
    defer close(done)
    const n = 1 << 20
    var s []int
    for range n {
        s = append(s, rand.IntN(n))
    }
    fmt.Printf("done filling slice of %d elements\n", len(s))
}()
<-done

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

// Сохраняем снепшот трассировки в файл.
file, _ := os.Create("/tmp/trace.out")
defer file.Close()
n, _ := rec.WriteTo(file)
fmt.Printf("wrote %dB to trace file\n", n)
done filling slice of 1048576 elements
wrote 8441B to trace file

Посмотреть трассировку в браузере можно командой go tool:

go1.25rc1 tool trace /tmp/trace.out

𝗣 63185 • 𝗖𝗟 673116

# Больше Root-методов

Тип os.Root ограничивает работу с файловой системой конкретным каталогом. Теперь он поддерживает несколько новых методов, аналогичных функциям пакета os.

Chmod меняет права доступа к файлу:

root, _ := os.OpenRoot("data")
root.Chmod("01.txt", 0600)

finfo, _ := root.Stat("01.txt")
fmt.Println(finfo.Mode().Perm())
-rw-------

Chown меняет идентификатор пользователя (uid) и группы (gid) файла:

root, _ := os.OpenRoot("data")
root.Chown("01.txt", 1000, 1000)

finfo, _ := root.Stat("01.txt")
stat := finfo.Sys().(*syscall.Stat_t)
fmt.Printf("uid=%d, gid=%d\n", stat.Uid, stat.Gid)
uid=1000, gid=1000

Chtimes меняет время последнего доступа и изменения файла:

root, _ := os.OpenRoot("data")
mtime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
atime := time.Now()
root.Chtimes("01.txt", atime, mtime)

finfo, _ := root.Stat("01.txt")
fmt.Println(finfo.ModTime())
2020-01-01 00:00:00 +0000 UTC

Link создает жесткую ссылку на файл:

root, _ := os.OpenRoot("data")
root.Link("01.txt", "hardlink.txt")

finfo, _ := root.Stat("hardlink.txt")
fmt.Println(finfo.Name())
hardlink.txt

MkdirAll создает новый каталог и все родительские каталоги при необходимости:

const dname = "path/to/secret"

root, _ := os.OpenRoot("data")
root.MkdirAll(dname, 0750)

finfo, _ := root.Stat(dname)
fmt.Println(dname, finfo.Mode())
path/to/secret drwxr-x---

RemoveAll удаляет файл или каталог со всем содержимым:

root, _ := os.OpenRoot("data")
root.RemoveAll("01.txt")

finfo, err := root.Stat("01.txt")
fmt.Println(finfo, err)
<nil> statat 01.txt: no such file or directory

Rename переименовывает (перемещает) файл или каталог:

const oldname = "01.txt"
const newname = "go.txt"

root, _ := os.OpenRoot("data")
root.Rename(oldname, newname)

_, err := root.Stat(oldname)
fmt.Println(err)

finfo, _ := root.Stat(newname)
fmt.Println(finfo.Name())
statat 01.txt: no such file or directory
go.txt

Symlink создает символическую ссылку на файл. Readlink возвращает путь, на который указывает эта ссылка:

const lname = "symlink.txt"

root, _ := os.OpenRoot("data")
root.Symlink("01.txt", lname)

lpath, _ := root.Readlink(lname)
fmt.Println(lname, "->", lpath)
symlink.txt -> 01.txt

WriteFile записывает данные в файл, создавая его при необходимости. ReadFile читает файл и возвращает его содержимое:

const fname = "go.txt"

root, _ := os.OpenRoot("data")
root.WriteFile(fname, []byte("go is awesome"), 0644)

content, _ := root.ReadFile(fname)
fmt.Printf("%s: %s\n", fname, content)
go.txt: go is awesome

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

Кстати о файловых системах. os.DirFS() (файловая система с корнем в указанной папке) и os.Root.FS() (файловая система для дерева файлов в корне) теперь реализуют новый интерфейс fs.ReadLinkFS. В этом интерфейсе два метода — ReadLink и Lstat.

ReadLink возвращает путь, на который указывает символическая ссылка:

const lname = "symlink.txt"

root, _ := os.OpenRoot("data")
root.Symlink("01.txt", lname)

fsys := root.FS().(fs.ReadLinkFS)
lpath, _ := fsys.ReadLink(lname)
fmt.Println(lname, "->", lpath)
symlink.txt -> 01.txt

Удивляет, конечно, разница в написании os.Root.Readlink и fs.ReadLinkFS.ReadLink.

Lstat возвращает информацию о файле или символической ссылке:

fsys := os.DirFS("data").(fs.ReadLinkFS)
finfo, _ := fsys.Lstat("01.txt")

fmt.Printf("name:  %s\n", finfo.Name())
fmt.Printf("size:  %dB\n", finfo.Size())
fmt.Printf("mode:  %s\n", finfo.Mode())
fmt.Printf("mtime: %s\n", finfo.ModTime().Format(time.DateOnly))
name:  01.txt
size:  11B
mode:  -rw-r--r--
mtime: 2025-06-22

Вот и все по пакету os!

𝗣 49580, 67002, 73126 • 𝗖𝗟 645718, 648295, 649515, 649536, 658995, 659416, 659757, 660635, 661595, 674116, 674315, 676135

# Рефлексивное приведение типа

Чтобы преобразовать reflect.Value обратно в нужный тип, обычно используют метод Value.Interface() вместе с приведением типа:

alice := &Person{"Alice", 25}

// Взяли рефлексивное Value...
aliceVal := reflect.ValueOf(alice).Elem()
// ...и привели обратно к типу Person.
person, _ := aliceVal.Interface().(Person)

fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age)
Name: Alice, Age: 25

Теперь вместо этого можно использовать новую дженерик-функцию reflect.TypeAssert:

alice := &Person{"Alice", 25}

// Взяли рефлексивное Value...
aliceVal := reflect.ValueOf(alice).Elem()
// ...и привели обратно к типу Person.
person, _ := reflect.TypeAssert[Person](aliceVal)

fmt.Printf("Name: %s, Age: %d\n", person.Name, person.Age)
Name: Alice, Age: 25

Такой способ более идиоматичен и не тратит лишнюю память, потому что значение не упаковывается в интерфейс.

𝗣 62121 • 𝗖𝗟 648056

# Атрибуты и вывод тестов

Новый метод T.Attr добавляет дополнительную информацию к тесту. Например, ссылку на задачу, описание тест-кейса или что-то еще, что поможет анализировать результаты тестов:

func TestAttrs(t *testing.T) {
    t.Attr("issue", "demo-1234")
    t.Attr("description", "Testing for the impossible")

    if 21*2 != 42 {
        t.Fatal("What in the world happened to math?")
    }
}
=== RUN   TestAttrs
=== ATTR  TestAttrs issue demo-1234
=== ATTR  TestAttrs description Testing for the impossible
--- PASS: TestAttrs (0.00s)

Атрибуты особенно полезны в JSON-формате, если вы отправляете результаты тестов в CI или другую систему для автоматической обработки:

go1.25rc1 test -json -run=.
...
{
    "Time":"2025-06-25T20:34:16.831401+00:00",
    "Action":"attr",
    "Package":"sandbox",
    "Test":"TestAttrs",
    "Key":"issue",
    "Value":"demo-1234"
}
...
{
    "Time":"2025-06-25T20:34:16.831415+00:00",
    "Action":"attr",
    "Package":"sandbox",
    "Test":"TestAttrs",
    "Key":"description",
    "Value":"Testing for the impossible"
}
...

Вывод тестов отформатирован для удобства чтения.

Такой же метод Attr есть в testing.B и testing.F.

𝗣 43936 • 𝗖𝗟 662437

Новый метод T.Output предоставляет доступ к потоку вывода (io.Writer), который использует тест. Удобно, если вы хотите отправлять логи приложения прямо в лог теста — так их проще читать или автоматически анализировать:

func TestLog(t *testing.T) {
    t.Log("test message 1")
    t.Log("test message 2")
    appLog := slog.New(slog.NewTextHandler(t.Output(), nil))
    appLog.Info("app message")
}
=== RUN   TestLog
    main_test.go:12: test message 1
    main_test.go:13: test message 2
    time=2025-06-25T16:14:34.085Z level=INFO msg="app message"
--- PASS: TestLog (0.00s)

Такой же метод Output есть в testing.B и testing.F.

𝗣 59928 • 𝗖𝗟 672395, 677875

И последнее: функция testing.AllocsPerRun теперь вызывает панику, если тесты выполняются параллельно.

Сравните поведение версии 1.24:

// go 1.24
func TestAllocs(t *testing.T) {
    t.Parallel()
    allocs := testing.AllocsPerRun(100, func() {
        var s []int
        // Do some allocations.
        for i := range 1024 {
            s = append(s, i)
        }
    })
    t.Log("Allocations per run:", allocs)
}
=== RUN   TestAllocs
=== PAUSE TestAllocs
=== CONT  TestAllocs
    main_test.go:21: Allocations per run: 12
--- PASS: TestAllocs (0.00s)

И 1.25:

// go 1.25
func TestAllocs(t *testing.T) {
    t.Parallel()
    allocs := testing.AllocsPerRun(100, func() {
        var s []int
        // Do some allocations.
        for i := range 1024 {
            s = append(s, i)
        }
    })
    t.Log("Allocations per run:", allocs)
}
=== RUN   TestAllocs
=== PAUSE TestAllocs
=== CONT  TestAllocs
--- FAIL: TestAllocs (0.00s)
panic: testing: AllocsPerRun called during parallel test [recovered, repanicked]

Дело в том, что результат AllocsPerRun не гарантирован, если параллельно идут несколько тестов. Поэтому и добавили новое поведение с паникой — оно должно помочь ловить такие баги.

𝗣 70464 • 𝗖𝗟 630137

# Группировка атрибутов в логах

При структурном логировании связанные атрибуты часто объединяют под одним ключом:

logger.Info("deposit",
    slog.Bool("ok", true),
    slog.Group("amount",
        slog.Int("value", 1000),
        slog.String("currency", "USD"),
    ),
)
msg=deposit ok=true amount.value=1000 amount.currency=USD

Это работает, но только если не пытаться сначала собрать атрибуты, а потом формировать группу:

attrs := []slog.Attr{
    slog.Int("value", 1000),
    slog.String("currency", "USD"),
}
logger.Info("deposit",
	slog.Bool("ok", true),
	slog.Group("amount", attrs...),
)
cannot use attrs (variable of type []slog.Attr)
as []any value in argument to slog.Group
(exit status 1)

slog.Group ожидает срез значений типа any, поэтому он не принимает срез slog.Attr.

Новая функция slog.GroupAttrs решает эту проблему, создавая группу из переданных slog.Attr:

attrs := []slog.Attr{
    slog.Int("value", 1000),
    slog.String("currency", "USD"),
}
logger.Info("deposit",
    slog.Bool("ok", true),
    slog.GroupAttrs("amount", attrs...),
)
msg=deposit ok=true amount.value=1000 amount.currency=USD

Мелочь, но может пригодиться.

𝗣 66365 • 𝗖𝗟 672915

# Клон хеша

Новый интерфейс hash.Cloner позволяет хеш-функции возвращать копию текущего состояния:

// https://github.com/golang/go/blob/master/src/hash/hash.go
type Cloner interface {
    Hash
    Clone() (Cloner, error)
}

Теперь все стандартные реализации hash.Hash поддерживают функцию Clone: MD5, SHA-1, SHA-3, FNV-1, CRC-64 и другие.

h1 := sha3.New256()
h1.Write([]byte("hello"))

clone, _ := h1.Clone()
h2 := clone.(*sha3.SHA3)

// состояние h2 такое же, как h1, поэтому после
// записи одинаковых данных получится одинаковый хеш.
h1.Write([]byte("world"))
h2.Write([]byte("world"))

fmt.Printf("h1: %x\n", h1.Sum(nil))
fmt.Printf("h2: %x\n", h2.Sum(nil))
fmt.Printf("h1 == h2: %t\n", reflect.DeepEqual(h1, h2))
h1: 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
h2: 92dad9443e4dd6d70a7f11872101ebff87e21798e4fbb26fa4bf590eb440e71b
h1 == h2: true

𝗣 69521 • 𝗖𝗟 675197

# Заключение

Go 1.25 облегчает тестирование многозадачного кода, вводит новый экспериментальный пакет для работы с JSON, и улучшает работу рантайма благодаря новой реализации GOMAXPROCS и сборщика мусора. Также появился компактный трассировщик, современная защита от CSRF, долгожданный метод Go для групп ожидания и еще несколько улучшений.

В целом, отличный релиз!

]]>