четверг, 22 сентября 2022 г.

Релиз Go 1.19

Релиз Go, версия 1.19, выходит через пять месяцев после Go 1.18. Большинство его изменений касаются реализации цепочки инструментов, среды выполнения и библиотек. Как всегда, выпуск поддерживает обещание совместимости Go 1. Ожидается, что почти все программы Go продолжат компилироваться и работать, как и раньше.

Изменения в языке

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

Модель памяти

Модель памяти Go была пересмотрена, чтобы привести Go в соответствие с моделью памяти, используемой в C, C++, Java, JavaScript, Rust и Swift. Go предоставляет только последовательно согласованные атомарные элементы, а не какие-либо более простые формы, встречающиеся в других языках. Наряду с обновлением модели памяти в Go 1.19 представлены новые типы в пакете sync/atomic, которые упрощают использование атомарных значений, таких как atomic.Int64 и atomic.Pointer[T].

Порты

64-битная версия LoongArch

Go 1.19 добавляет поддержку 64-битной архитектуры Loongson LoongArch для Linux (GOOS=linux, GOARCH=loong64). Реализованный ABI — LP64D. Минимальная поддерживаемая версия ядра — 5.19.

Обратите внимание, что большинство существующих коммерческих дистрибутивов Linux для LoongArch поставляются со старыми ядрами с исторически несовместимым системным вызовом ABI. Скомпилированные двоичные файлы не будут работать в этих системах, даже если они статически связаны. Пользователи таких неподдерживаемых систем ограничены пакетом Go из дистрибутива.

RISC-V

Порт riscv64 теперь поддерживает передачу аргументов функции и результата с использованием регистров. Сравнительный анализ показывает типичное повышение производительности на 10% и более на riscv64.

Инструменты

Комментарии к документу

В Go 1.19 добавлена поддержка ссылок, списков и более четких заголовков в комментариях к документам. В рамках этого изменения gofmt теперь переформатирует комментарии к документам, чтобы сделать их отображаемый смысл более ясным.В качестве еще одной части этого изменения новый пакет go/doc/comment обеспечивает синтаксический анализ и переформатирование комментариев к документам, а также поддержку их преобразования в HTML, Markdown и текст.

Новое ограничение сборки unix

Unix-ограничение сборки теперь распознается в строках //go:build. Ограничение выполняется, если целевая операционная система, также известная как GOOS, является Unix или Unix-подобной системой. Для версии 1.19 допустимо, если GOOS является одним из следующих: aix, android, darwin, dragonfly, freebsd, hurd, illumos, ios, linux, netbsd, openbsd или solaris. В будущих релизах ограничение unix может соответствовать дополнительным вновь поддерживаемым операционным системам.

Команда go

Флаг -trimpath, если он установлен, теперь включается в настройки сборки, проставленные в двоичных файлах Go командой go build, и может быть проверен с помощью go version -m или debug.ReadBuildInfo.

go generate теперь устанавливает переменную среды GOROOT явно в среде генератора, так что генераторы могут найти правильный GOROOT, даже если он собран с -trimpath.

go test и go generate теперь размещает GOROOT/bin в начало PATH, используемого для подпроцесса, поэтому тесты и генераторы, выполняющие команду go, разрешат ее в один и тот же GOROOT.

go env теперь заключает в кавычки записи, содержащие пробелы в отчетных переменных CGO_CFLAGS, CGO_CPPFLAGS, CGO_CXXFLAGS, CGO_FFLAGS, CGO_LDFLAGS и GOGCCFLAGS.

go list -json теперь принимает список полей JSON, разделенных запятыми, для заполнения. Если указан список, выходные данные JSON будут включать только эти поля, и список перехода может избежать работы по вычислению полей, которые не включены. В некоторых случаях это может подавить ошибки, о которых в противном случае сообщалось бы.

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

Ветеринар (Vet)

vet чекер "errorsas" теперь сообщает когда errors.As вызывается со вторым аргументом типа *error, что является распространенной ошибкой.

Среда выполнения (Runtime)

Среда выполнения теперь включает поддержку мягкого ограничения памяти. Это ограничение памяти включает кучу (heap) Go и всю другую память, управляемую средой выполнения, и исключает внешние источники памяти, такие как сопоставления самого двоичного файла, память, управляемую на других языках, и память, удерживаемую операционной системой от имени программы Go. Этим ограничением можно управлять с помощью runtime/debug.SetMemoryLimit или эквивалентной переменной среды GOMEMLIMIT. Ограничение работает в сочетании с runtime/debug.SetGCPercent / GOGC и будет соблюдаться, даже если GOGC=off, позволяя программам Go всегда максимально использовать свой лимит памяти, в некоторых случаях повышая эффективность использования ресурсов. Обратите внимание, что небольшие ограничения памяти, порядка десятков мегабайт или меньше, с меньшей вероятностью будут соблюдаться из-за внешних факторов задержки, таких как планирование ОС. Большие пределы памяти, порядка сотен мегабайт и более, стабильны и готовы к работе.

Чтобы ограничить последствия перегрузки GC, когда размер динамической кучи программы приближается к пределу программной памяти, среда выполнения Go также пытается ограничить общее использование ЦП GC до 50%, исключая время простоя, предпочитая использовать больше памяти, а не предотвращая выполнение приложения. На практике ожидается, что это ограничение будет играть роль только в исключительных случаях, и новая метрика среды выполнения /gc/limiter/last-enabled:gc-cycle сообщает, когда это произошло в последний раз.

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

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

В операционных системах Unix программы Go, которые импортируют пакет os, теперь автоматически увеличивают лимит открытых файлов (RLIMIT_NOFILE) до максимально допустимого значения; то есть они изменяют мягкое ограничение, чтобы оно соответствовало жесткому ограничению. Это исправляет искусственно заниженные ограничения, установленные в некоторых системах для совместимости с очень старыми программами на C, использующими системный вызов select. Это ограничение не помогает программам Go, и вместо этого даже простые программы, такие как gofmt, часто не имеют файловых дескрипторов в таких системах при параллельной обработке большого количества файлов. Одним из последствий этого изменения является то, что программы Go, которые, в свою очередь, выполняют очень старые программы C в дочерних процессах, могут запускать эти программы со слишком высоким ограничением. Это можно исправить, установив жесткое ограничение перед вызовом программы Go.

Неисправимые фатальные ошибки (такие как одновременная запись карты или разблокировка разблокированных мьютексов) теперь печатают более простую трассировку, исключая метаданные времени выполнения (эквивалент фатальной паники), если только GOTRACEBACK не установлено значение system или crash. Внутренние трассировки неустранимых ошибок во время выполнения всегда включают полные метаданные независимо от значения GOTRACEBACK.

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

Поддержка очистки адресов, добавленная в Go 1.18, теперь более точно обрабатывает аргументы функций и глобальные переменные.

Компилятор

Теперь компилятор использует таблицу переходов для реализации больших целочисленных и строковых операторов переключения. Улучшения производительности оператора switch различаются, но могут быть на порядка 20% быстрее. (только GOARCH=amd64 и GOARCH=arm64)

Компилятору Go теперь требуется флаг -p=importpath для создания связываемого объектного файла. Это уже предоставлено командой go и Bazel. Любые другие системы сборки, которые напрямую вызывают компилятор Go, также должны убедиться, что они передают этот флаг.

Компилятор Go больше не поддерживает флаг -importmap. Системы сборки, которые напрямую вызывают компилятор Go, должны вместо этого использовать флаг -importcfg.

Ассемблер

Как и компилятору, ассемблеру теперь требуется флаг -p=importpath для создания компоновочного объектного файла. Это уже предоставлено командой go. Любые другие системы сборки, которые напрямую вызывают ассемблер Go, также должны убедиться, что они передают этот флаг.

Линкер

На платформах ELF компоновщик теперь создает сжатые разделы DWARF в стандартном формате gABI (SHF_COMPRESSED) вместо устаревшего формата .zdebug.

Основная библиотека

Новые атомарные типы

Пакет sync/atomic определяет новые атомарные типы Bool, Int32, Int64, Uint32, Uint64, Uintptr и Pointer. Эти типы скрывают базовые значения, поэтому все обращения вынуждены использовать атомарные API. Pointer также избавляет от необходимости конвертировать в unsafe.Pointer в местах вызовов. Int64 и Uint64 автоматически выравниваются по 64-битным границам в структурах и выделенных данных даже в 32-битных системах.

PATH поиск

Command и LookPath больше не позволяют находить результаты поиска PATH относительно текущего каталога. Это устраняет общий источник проблем с безопасностью, но может также нарушить работу существующих программ, которые зависят от использования, скажем, exec.Command("prog") для запуска двоичного файла с именем prog (или, в Windows, prog.exe) в текущем каталоге.

В Windows Command и LookPath теперь учитывают переменную среды NoDefaultCurrentDirectoryInExePath, что позволяет отключить неявный поиск “.” по умолчанию в поиске PATH в системах Windows.

Мелкие изменения в библиотеке

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

archive/zip

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

crypto/elliptic

Работа с недопустимыми точками кривой (теми, для которых метод IsOnCurve возвращает false и которые никогда не возвращаются Unmarshal или методом Curve, работающим с допустимой точкой) всегда было неопределенным поведением и может привести к атакам восстановления ключа. Если для Marshal, MarshalCompressed, Add, Double или ScalarMult указана недопустимая точка, они теперь будут паниковать.

Операции ScalarBaseMult с кривыми P224, P384 и P521 теперь выполняются до трех раз быстрее, что приводит к аналогичному ускорению некоторых операций ECDSA. Общая (не оптимизированная для платформы) реализация P256 была заменена реализацией, полученной из официально проверенной модели; это может привести к значительному замедлению работы на 32-разрядных платформах.

crypto/rand

Read больше не буферизует случайные данные, полученные от операционной системы, между вызовами. Приложения, которые выполняют много небольших операций чтения с высокой частотой, могут по соображениям производительности обернуть Reader в bufio.Reader, стараясь использовать io.ReadFull, чтобы гарантировать отсутствие частичных чтений.

В Plan 9 функция Read была повторно реализована, заменив алгоритм ANSI X9.31 на генератор быстрого стирания ключей.

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

crypto/tls

Опция GODEBUG tls10default=1 удалена. По-прежнему можно включить TLS 1.0 на стороне клиента, установив Config.MinVersion.

Сервер и клиент TLS теперь отклоняют повторяющиеся расширения в рукопожатиях TLS, как того требуют RFC 5246, раздел 7.4.1.4 и RFC 8446, раздел 4.2.

crypto/x509

CreateCertificate больше не поддерживает создание сертификатов с SignatureAlgorithm, для которого задано значение MD5WithRSA.

CreateCertificate больше не принимает отрицательные серийные номера.

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

Удаление GODEBUG optionx509sha1=1, первоначально запланированное для Go 1.19, было перенесено на будущий релиз. Приложения, использующие его, должны работать при миграции. Практические атаки на SHA-1 были продемонстрированы с 2017 года, а общедоступные доверенные центры сертификации не выпускали сертификаты SHA-1 с 2015 года.

ParseCertificate и ParseCertificateRequest теперь отклоняют сертификаты и CSR, которые содержат повторяющиеся расширения.

Новые методы CertPool.Clone и CertPool.Equal позволяют клонировать CertPool и проверять эквивалентность двух CertPools соответственно.

Новая функция ParseRevocationList обеспечивает более быстрый и безопасный анализатор CRL, который возвращает RevocationList. При разборе CRL также заполняются новые поля RevocationList RawIssuer, Signature, AuthorityKeyId и Extensions, которые игнорируются CreateRevocationList.

Новый метод RevocationList.CheckSignatureFrom проверяет, является ли подпись в CRL действительной подписью сертификата.

Функции ParseCRL и ParseDERCRL теперь объявлены устаревшими в пользу ParseRevocationList. Метод Certificate.CheckCRLSignature устарел в пользу RevocationList.CheckSignatureFrom.

Построитель пути Certificate.Verify был переработан и теперь должен создавать более качественные цепочки и/или быть более эффективным в сложных сценариях. Ограничения имен теперь также применяются к неконечным сертификатам.

crypto/x509/pkix

Типы CertificateList и TBSCertificateList устарели. Вместо этого следует использовать новую функциональность crypto/x509 CRL.

debug/elf

Новые константы EM_LOONGARCH и R_LARCH_* поддерживают порт loong64.

debug/pe

Новый метод File.COFFSymbolReadSectionDefAux, возвращающий COFFSymbolAuxFormat5, обеспечивает доступ к информации COMDAT в разделах PE-файла. Они поддерживаются новыми константами IMAGE_COMDAT_* и IMAGE_SCN_*.

encoding/binary

Новый интерфейс AppendByteOrder предоставляет эффективные методы добавления uint16, uint32 или uint64 к срезу байтов. BigEndian и LittleEndian теперь реализуют этот интерфейс.

Точно так же новые функции AppendUvarint и AppendVarint являются эффективными версиями добавления функций PutUvarint и PutVarint.

encoding/csv

Новый метод Reader.InputOffset сообщает о текущей позиции ввода средства чтения в виде смещения в байтах, аналогично Decoder.InputOffset в encoding/json.

encoding/xml

Новый метод Decoder.InputPos сообщает текущую позицию ввода считывателя в виде строки и столбца, аналогично Decoder.FieldPos из encoding/csv.

flag

Новая функция TextVar определяет флаг со значением, реализующим encoding.TextUnmarshaler, что позволяет переменным флага командной строки иметь такие типы, как big.Int, netip.Addr и time.Time.

fmt

Новые функции Append, Appendf и Appendln добавляют отформатированные данные в байтовые срезы.

go/parser

Синтаксический анализатор теперь распознает ~x как унарное выражение с оператором token.TILDE, что позволяет лучше устранять ошибки, когда ограничение типа, такое как ~int, используется в неправильном контексте.

go/types

Новые методы Func.Origin и Var.Origin возвращают соответствующий Object универсального типа для синтетических объектов Func и Var, созданных во время создания экземпляра типа.

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

hash/maphash

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

html/template

Тип FuncMap теперь является псевдонимом для FuncMap пакета text/template вместо собственного именованного типа. Это позволяет писать код, который работает с FuncMap при любых настройках.

image/draw

Рисование с оператором Src сохраняет цвета без предварительного умножения альфа-канала, когда целевое и исходное изображения оба имеют формат image.NRGBA или оба изображения image.NRGBA64. Это отменяет изменение поведения, случайно внесенное оптимизацией библиотеки Go 1.18; код теперь соответствует поведению в Go 1.17 и более ранних версиях.

io

Результат NopCloser теперь реализует WriterTo всякий раз, когда это происходит на входе.

Результат MultiReader теперь безоговорочно реализует WriterTo. Если какое-либо базовое средство чтения не реализует WriterTo, оно моделируется соответствующим образом.

mime

Только в Windows пакет mime теперь игнорирует запись реестра о том, что расширение .js должно иметь тип MIME text/plain. Это распространенная непреднамеренная неправильная настройка в системах Windows. В результате .js будет иметь тип MIME text/javascript по умолчанию; кодировка=utf-8. Приложения, которые ожидают text/plain в Windows, теперь должны явно вызывать AddExtensionType.

net

Чистый преобразователь Go теперь будет использовать EDNS(0) для включения рекомендуемой максимальной длины ответного пакета, что позволяет ответным пакетам содержать до 1232 байт (ранее максимальное значение составляло 512). В том маловероятном случае, если это вызовет проблемы с локальным распознавателем DNS, установка переменной среды GODEBUG=netdns=cgo для использования распознавателя на основе cgo должна работать.

Когда функция или метод пакета net возвращает ошибку "I/O timeout", эта ошибка теперь будет удовлетворять errors.Is(err, context.DeadlineExceeded). Когда функция пакета net возвращает ошибку "operation was canceled" ("операция была отменена"), эта ошибка теперь удовлетворяет errors.Is(err, context.Canceled). Эти изменения предназначены для того, чтобы упростить тестирование кода в случаях, когда отмена контекста или таймаут приводят к тому, что функция или метод пакета net возвращает ошибку, сохраняя при этом обратную совместимость для сообщений об ошибках.

Resolver.PreferGo теперь реализован в Windows и Plan 9. Ранее он работал только на платформах Unix. В сочетании с Dialer.Resolver и Resolver.Dial теперь можно писать переносимые программы и контролировать все поиски DNS-имен при подключении.

Пакет net теперь имеет начальную поддержку тега сборки netgo в Windows. При использовании пакет использует DNS-клиент Go (используемый Resolver.PreferGo) вместо того, чтобы запрашивать у Windows результаты DNS. Однако вышестоящий DNS-сервер, который он обнаруживает из Windows, может быть еще неправильным со сложными системными сетевыми конфигурациями.

net/http

ResponseWriter.WriteHeader теперь поддерживает отправку определяемых пользователем информационных заголовков 1xx.

io.ReadCloser, возвращаемый MaxBytesReader, теперь будет возвращать определенный тип ошибки MaxBytesError при превышении предела чтения.

HTTP-клиент будет обрабатывать ответ 3xx без заголовка Location, возвращая его вызывающей стороне, а не рассматривая его как ошибку.

net/url

Новая функция JoinPath и метод URL.JoinPath создают новый URL-адрес путем присоединения к списку элементов пути.

Тип URL-адреса теперь различает URL-адреса без полномочий и URL-адреса с пустыми полномочиями. Например, http:///path имеет пустые права доступа (хост), а в http:/path их нет.

Новое поле URL-адреса OmitHost имеет значение true, если URL-адрес имеет пустые полномочия.

os/exec

Cmd с непустым полем Dir и nil Env теперь неявно устанавливает переменную среды PWD для подпроцесса в соответствие с Dir.

Новый метод Cmd.Environ сообщает о среде, которая будет использоваться для запуска команды, включая неявно заданную переменную PWD.

reflect

Метод Value.Bytes теперь принимает адресные массивы в дополнение к срезам.

Методы Value.Len и Value.Cap теперь успешно работают с указателем на массив и возвращают длину этого массива, чтобы соответствовать тому, что делают встроенные функции len и cap.

regexp/syntax

Go 1.18 релиз-кандидат 1, Go 1.17.8 и Go 1.16.15 включали исправление безопасности для синтаксического анализатора регулярных выражений, заставляющее его отклонять очень глубоко вложенные выражения. Поскольку релизы исправлений Go не вводят новый API, синтаксический анализатор в этом случае вернул синтаксис.ErrInternalError. В Go 1.19 добавлена ​​более конкретная ошибка синтаксис.ErrNestingDepth, которую теперь возвращает синтаксический анализатор.

runtime

Функция GOROOT теперь возвращает пустую строку (вместо "go"), когда бинарный файл был собран с установленным флагом -trimpath и переменная GOROOT не установлена в среде процесса.

runtime/metrics

Новая метрика /sched/gomaxprocs:threads сообщает о текущем значении runtime.GOMAXPROCS.

Новая метрика /cgo/go-to-c-calls:calls сообщает об общем количестве вызовов, сделанных с Go на C. Эта метрика идентична функции runtime.NumCgoCall.

Новая метрика /gc/limiter/last-enabled:gc-cycle сообщает о последнем цикле сборки мусора, когда был включен ограничитель ЦП сборщика мусора.

runtime/pprof

Время пауз Stop-the-world было значительно сокращено при сборе профилей goroutine, что уменьшило общее влияние задержки на приложение.

MaxRSS теперь сообщается в профилях кучи для всех операционных систем Unix (ранее сообщалось только для GOOS=android, darwin, ios и linux).

runtime/race

Детектор гонок был обновлен для использования санитайзера потоков версии v3 на всех поддерживаемых платформах, кроме windows/amd64 и openbsd/amd64, которые остаются на версии v2. По сравнению с v2, теперь он обычно в 1,5–2 раза быстрее, использует вдвое меньше памяти и поддерживает неограниченное количество горутин. В Linux детектор гонки теперь требует как минимум glibc версии 2.17 и GNU binutils 2.26.

Детектор гонки теперь поддерживается на GOARCH=s390x.

Поддержка детектора гонки для openbsd/amd64 была удалена из вышестоящего санитайзера потоков, поэтому вряд ли оно когда-либо будет обновлено с версии v2.

runtime/trace

Если трассировка и профилировщик ЦП включены одновременно, трассировка выполнения включает образцы профиля ЦП как мгновенные события.

sort

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

Новая функция Find похожа на Search, но часто проще в использовании: она возвращает дополнительное логическое значение, сообщающее, найдено ли равное значение.

strconv

Quote и связанные с ней функции теперь указывают руну U+007F как \x7f, а не \u007f, для совместимости с другими значениями ASCII.

syscall

На PowerPC (GOARCH=ppc64, ppc64le) Syscall, Syscall6, RawSyscall и RawSyscall6 теперь всегда возвращают 0 для возвращаемого значения r2 вместо неопределенного значения.

В AIX и Solaris теперь определен Getrusage.

time

Новый метод Duration.Abs предоставляет удобный и безопасный способ получения абсолютного значения длительности путем преобразования −2⁶³ в 2⁶³−1. (Этот пограничный случай может произойти в результате вычитания недавнего времени из нулевого времени.)

Новый метод Time.ZoneBounds возвращает время начала и окончания часового пояса, действующего в данный момент времени. Его можно использовать в цикле для перечисления всех известных переходов часовых поясов в заданном месте.


Читайте также:


понедельник, 12 сентября 2022 г.

Начало работы с фаззингом в Golang

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

В этом руководстве вы напишете fuzz тест для простой функции, запустите команду go, отладите и исправите проблемы в коде.

Предпосылки

  • Установленный Go 1.18 или более поздней версии.
  • Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально.
  • Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.
  • Среда, поддерживающая фаззинг. В настоящее время фаззинг с инструментами покрытия доступен только для архитектур AMD64 и ARM64.

Создайте папку для своего кода

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

1. Откройте командную строку и перейдите в свой домашний каталог.

В Linux или Mac:

$ cd

В Windows:

C:\> cd %HOMEPATH%

В остальной части руководства в качестве подсказки будет отображаться символ $. Используемые вами команды будут работать и в Windows.

2. В командной строке создайте каталог для своего кода с именем fuzz.

$ mkdir fuzz
$ cd fuzz

3. Создайте модуль для хранения вашего кода.

Запустите команду go mod init, указав путь к модулю вашего нового кода.

$ go mod init example/fuzz
go: creating new go.mod: module example/fuzz

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

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

Добавьте код для тестирования

На этом шаге вы добавите функцию для обращения строки.

Напишите код

1. Используя текстовый редактор, создайте файл с именем main.go в каталоге fuzz.

2. В файл main.go в верхней части файла вставьте следующее объявление пакета.

package main

Автономная программа (в отличие от библиотеки) всегда находится в пакете main.

3. Под объявлением пакета вставьте следующее объявление функции.

func Reverse(s string) string {
    b := []byte(s)
    for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}

Эта функция примет строку, обработает ее побайтно и в конце вернет перевернутую строку.

Примечание. Этот код основан на функции stringutil.Reverse из golang.org/x/example.

4. В верхней части main.go, под объявлением пакета, вставьте следующую main функцию, чтобы инициализировать строку, перевернуть ее, распечатать вывод и повторить.

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev := Reverse(input)
    doubleRev := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q\n", rev)
    fmt.Printf("reversed again: %q\n", doubleRev)
}

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

5. main функция использует пакет fmt, поэтому вам нужно будет его импортировать.

Первые строки кода должны выглядеть так:

package main

import "fmt"

Запустите код

Из командной строки в каталоге, содержащем main.go, запустите код.

$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"

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

Теперь, когда код работает, пришло время его протестировать.

Добавить модульный (юнит) тест

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

Напишите код

1. Используя текстовый редактор, создайте файл с именем reverse_test.go в каталоге fuzz.

2. Вставьте следующий код в reverse_test.go.

package main

import (
    "testing"
)

func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev := Reverse(tc.in)
        if rev != tc.want {
                t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}

Этот простой тест подтвердит, что перечисленные входные строки будут правильно инвертированы.

Запустите код

Запустите модульный тест, используя go test

$ go test
PASS
ok      example/fuzz  0.013s

Далее вы измените модульный тест на fuzz тест.

Добавить fuzz тест

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

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

Обратите внимание, что вы можете хранить модульные тесты, бенчмарки и fuzz тесты в одном и том же файле *_test.go, но в этом примере вы преобразуете модульный тест в fuzz тест.

Напишите код

В текстовом редакторе замените модульный тест в reverse_test.go следующим fuzz тестом.

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Используйте f.Add для предоставления начального корпуса (seed corpus)
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

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

Например, в тестовом случае Reverse("Hello, world") модульный тест определяет результат как "dlrow,olleH".

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

Однако есть несколько свойств функции Reverse, которые вы можете проверить с помощью fuzz теста. В этом fuzz тесте проверяются два свойства:

  • Двойное обращение строки сохраняет исходное значение
  • Перевернутая строка сохраняет свое состояние как действительное UTF-8.

Обратите внимание на синтаксические различия между модульным тестом и fuzz тестом:

  • Функция начинается с FuzzXxx вместо TestXxx и принимает *testing.F вместо *testing.T.
  • Там, где вы ожидаете увидеть выполнение t.Run, вместо этого вы видите f.Fuzz, который принимает целевую функцию фаззинга с параметрами *testing.T и типами, подлежащими фаззингу. Входные данные из вашего модульного теста предоставляются как входные данные начального корпуса с помощью f.Add.

Убедитесь, что новый пакет unicode/utf8 импортирован.

package main

import (
    "testing"
    "unicode/utf8"
)

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

Запустите код

1. Запустите fuzz тест без фаззинга, чтобы убедиться, что исходные входные данные пройдены.

$ go test
PASS
ok      example/fuzz  0.013s

Вы также можете запустить go test -run=FuzzReverse, если у вас есть другие тесты в этом файле, и вы хотите запустить только fuzz тест.

2. Запустите FuzzReverse с фаззингом, чтобы увидеть, не приведут ли какие-либо случайно сгенерированные вводы строк к сбою. Это выполняется с помощью go test с новым флагом -fuzz.

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
fuzz: minimizing 38-byte failing input file...
--- FAIL: FuzzReverse (0.01s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"

    Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    To re-run:
    go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
FAIL
exit status 1
FAIL    example/fuzz  0.030s

Во время фаззинга произошел сбой, и ввод, вызвавший проблему, записывается в файл начального корпуса (seed corpus), который будет запущен при следующем вызове go test, даже без флага -fuzz. Чтобы просмотреть ввод, вызвавший сбой, откройте файл корпуса, записанный в каталог testdata/fuzz/FuzzReverse, в текстовом редакторе. Ваш файл исходного корпуса может содержать другую строку, но формат будет таким же.

go test fuzz v1
string("泃")

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

3. Снова запустите go test без флага -fuzz; будет использоваться новая ошибочная запись корпуса исходных данных:

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
        reverse_test.go:20: Reverse produced invalid string
FAIL
exit status 1
FAIL    example/fuzz  0.016s

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

Исправить ошибку неверной строки

В этом разделе вы отладите сбой и исправите ошибку.

Диагностируйте ошибку

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

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

Во-первых, рассмотрим документацию для utf8.ValidString.

ValidString сообщает, состоит ли s полностью из допустимых рун в кодировке UTF-8.

Текущая функция Reverse переворачивает строку байт за байтом, и в этом наша проблема. Чтобы сохранить руны исходной строки в кодировке UTF-8, мы должны вместо этого перевернуть строку руна за руной.

Чтобы выяснить, почему ввод (в данном случае китайский иероглиф 泃) приводит к тому, что Reverse создает недопустимую строку при переворачивании, вы можете проверить количество рун в перевернутой строке.

Напишите код

В текстовом редакторе замените цель fuzz в FuzzReverse следующим.

f.Fuzz(func(t *testing.T, orig string) {
    rev := Reverse(orig)
    doubleRev := Reverse(rev)
    t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))
    if orig != doubleRev {
        t.Errorf("Before: %q, after: %q", orig, doubleRev)
    }
    if utf8.ValidString(orig) && !utf8.ValidString(rev) {
        t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
    }
})

Здесь строка t.Logf будет выводиться в командную строку в случае возникновения ошибки или при выполнении теста с ключом -v, что может помочь вам отладить эту конкретную проблему.

Запустите код

Запустите тест, используя go test

$ go test
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=3, doubleRev=1
        reverse_test.go:21: Reverse produced invalid UTF-8 string "\x83\xb3\xe6"
FAIL
exit status 1
FAIL    example/fuzz    0.598s

Во всем начальном корпусе (seed corpus) использовались строки, в которых каждый символ представлял собой один байт. Однако для таких символов, как 泃, может потребоваться несколько байтов. Таким образом, обращение строки байт за байтом сделает недействительными многобайтовые символы.

Примечание. Если вам интересно, как Go работает со строками, прочтите запись в блоге Строки, байты, руны и символы в Go для более глубокого понимания.

Лучше поняв ошибку, исправьте ошибку в функции Reverse.

Исправьте ошибку

Чтобы исправить функцию Reverse, пройдемся по строке по рунам, а не по байтам.

Напишите код

В текстовом редакторе замените существующую функцию Reverse() следующей.

func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

Ключевое отличие состоит в том, что Reverse теперь перебирает каждую руну в строке, а не каждый байт.

Запустите код

Запустите тест, используя go test

$ go test
PASS
ok      example/fuzz  0.016s

Теперь тест проходит!

Еще раз проверьте его с помощью go test -fuzz, чтобы увидеть, есть ли какие-либо новые ошибки.

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/37 completed
fuzz: minimizing 506-byte failing input file...
fuzz: elapsed: 0s, gathering baseline coverage: 5/37 completed
--- FAIL: FuzzReverse (0.02s)
    --- FAIL: FuzzReverse (0.00s)
        reverse_test.go:33: Before: "\x91", after: "�"

    Failing input written to testdata/fuzz/FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
    To re-run:
    go test -run=FuzzReverse/1ffc28f7538e29d79fce69fef20ce5ea72648529a9ca10bea392bcff28cd015c
FAIL
exit status 1
FAIL    example/fuzz  0.032s

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

Давайте снова отладим.

Исправить ошибку двойного реверса

В этом разделе вы отладите ошибку двойного реверса и исправите ее.

Диагностируйте ошибку

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

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

Посмотрите внимательно на перевернутую строку, чтобы найти ошибку. В Go строка представляет собой срез байтов, доступный только для чтения, и может содержать байты, которые не являются допустимыми UTF-8. Исходная строка представляет собой срез байтов с одним байтом, '\x91'. Когда входная строка имеет значение []rune, Go кодирует байтовый срез в UTF-8 и заменяет байт символом UTF-8 �. Когда мы сравниваем замещающий символ UTF-8 с входным байтовым срезом, они явно не равны.

Напишите код

В текстовом редакторе замените функцию Reverse на следующую.

func Reverse(s string) string {
    fmt.Printf("input: %q\n", s)
    r := []rune(s)
    fmt.Printf("runes: %q\n", r)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

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

Запустите код

На этот раз мы хотим запустить только неудачный тест, чтобы проверить логи. Для этого мы будем использовать go test -run.

$ go test -run=FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0
input: "\x91"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)
    --- FAIL: FuzzReverse/28f36ef487f23e6c7a81ebdaa9feffe2f2b02b4cddaa6252e87f69863046a5e0 (0.00s)
        reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1
        reverse_test.go:18: Before: "\x91", after: "�"
FAIL
exit status 1
FAIL    example/fuzz    0.145s

Чтобы запустить конкретную запись корпуса в FuzzXxx/testdata, вы можете указать {FuzzTestName}/{filename} для -run. Это может быть полезно при отладке.

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

Исправьте ошибку

Чтобы решить эту проблему, давайте вернем ошибку, если ввод в Reverse недействителен в кодировке UTF-8.

Напишите код

В текстовом редакторе замените существующую функцию Reverse на следующую.

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

Это изменение вернет ошибку, если входная строка содержит символы, недопустимые в кодировке UTF-8.

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

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

Эти вызовы Reverse должны возвращать нулевую ошибку, поскольку входная строка является допустимой UTF-8.

Вам нужно будет импортировать пакеты errors и unicode/utf8. Оператор импорта в main.go должен выглядеть следующим образом.

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

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

func FuzzReverse(f *testing.F) {
    testcases := []string {"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc)  //  Используйте f.Add для предоставления начального корпуса (seed corpus)
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
             return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

Вместо возврата вы также можете вызвать t.Skip(), чтобы остановить выполнение этого fuzz ввода.

Запустите код

Запустите тест, используя go test

$ go test
PASS
ok      example/fuzz  0.019s

Фаззите с помощью go test -fuzz=Fuzz, затем через несколько секунд прекратите фаззинг, нажав Ctrl-C.

$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/38 completed
fuzz: elapsed: 0s, gathering baseline coverage: 38/38 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)
fuzz: elapsed: 6s, execs: 193490 (35714/sec), new interesting: 4 (total: 37)
fuzz: elapsed: 9s, execs: 304390 (36961/sec), new interesting: 4 (total: 37)
...
fuzz: elapsed: 3m45s, execs: 7246222 (32357/sec), new interesting: 8 (total: 41)
^Cfuzz: elapsed: 3m48s, execs: 7335316 (31648/sec), new interesting: 8 (total: 41)
PASS
ok      example/fuzz  228.000s

fuzz тест будет выполняться до тех пор, пока не обнаружит ошибочный ввод, если только вы не передадите флаг -fuzztime. По умолчанию выполняется вечно, если не происходит сбоев, и процесс можно прервать с помощью Ctrl-C.

Проведите фаззинг с помощью go test -fuzz=Fuzz -fuzztime 30s, который будет фаззить в течение 30 секунд перед выходом, если сбоев не обнаружено.

$ go test -fuzz=Fuzz -fuzztime 30s
fuzz: elapsed: 0s, gathering baseline coverage: 0/5 completed
fuzz: elapsed: 0s, gathering baseline coverage: 5/5 completed, now fuzzing with 4 workers
fuzz: elapsed: 3s, execs: 80290 (26763/sec), new interesting: 12 (total: 12)
fuzz: elapsed: 6s, execs: 210803 (43501/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 9s, execs: 292882 (27360/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 12s, execs: 371872 (26329/sec), new interesting: 14 (total: 14)
fuzz: elapsed: 15s, execs: 517169 (48433/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 18s, execs: 663276 (48699/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 21s, execs: 771698 (36143/sec), new interesting: 15 (total: 15)
fuzz: elapsed: 24s, execs: 924768 (50990/sec), new interesting: 16 (total: 16)
fuzz: elapsed: 27s, execs: 1082025 (52427/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 30s, execs: 1172817 (30281/sec), new interesting: 17 (total: 17)
fuzz: elapsed: 31s, execs: 1172817 (0/sec), new interesting: 17 (total: 17)
PASS
ok      example/fuzz  31.025s

Фаззинг пройден!

Завершенный код — main.go —

package main

import (
    "errors"
    "fmt"
    "unicode/utf8"
)

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev, revErr := Reverse(input)
    doubleRev, doubleRevErr := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q, err: %v\n", rev, revErr)
    fmt.Printf("reversed again: %q, err: %v\n", doubleRev, doubleRevErr)
}

func Reverse(s string) (string, error) {
    if !utf8.ValidString(s) {
        return s, errors.New("input is not valid UTF-8")
    }
    r := []rune(s)
    for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r), nil
}

— reverse_test.go —

package main

import (
    "testing"
    "unicode/utf8"
)

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world", " ", "!12345"}
    for _, tc := range testcases {
        f.Add(tc) // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev, err1 := Reverse(orig)
        if err1 != nil {
            return
        }
        doubleRev, err2 := Reverse(rev)
        if err2 != nil {
            return
        }
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}


Читайте также: