Go Update
СтатистикаКанал про новости связанные с языком программирования Go. Эволюция языка, стандартной библиотеки и просто интересные вещи над которыми работает Go Core Team и не только. Админ: @lepage_d
- Последний пост
- 11 авг.
- Последнее чтение
- 17:47
- Постов за неделю
- 2
- Всего постов
- 20
- Тип
- открытый
- Язык
- русский
- Категория
- Технологии
- В каталоге с
- 12 авг.
- 1/24сутки в ленте
- 919
- 1/48двое суток
- 1 053
- 1/72трое суток
- 1 135
Оценка по просмотрам недавних постов: пост набирает почти всё за первые сутки.
Посты
🎂 Настал тот день когда мне исполнилось 33! Да, да тот самый год, в который вас поздравляют используя аналогии к одной из самых ярких личностей в истории человечества 😁️️️️️️. В (не)далеком детстве, когда мне было лет 7-8, я смотрел на взрослых, которым…
🎂 Настал тот день когда мне исполнилось 33! Да, да тот самый год, в который вас поздравляют используя аналогии к одной из самых ярких личностей в истории человечества 😁️️️️️️. В (не)далеком детстве, когда мне было лет 7-8, я смотрел на взрослых, которым…
📝 testing: allow examples with any signature Небольшое «Quality of Life» предложение. Суть: давайте разрешим функциям Example*, которые мы используем для документации, принимать и возвращать любое число аргументов. Запрос связан с тем, что сейчас многие вещи не выразить в примерах. Например примеры для пакетов нацеленных на тестирование. Те в документации станет отображаться следующий код: func Example_Functionname(t *testing.T) { t.Logf("foobar") } Единственным ограничением является то, что в отличии от безаргументных Example* функций, они не смогут содержать // Output: комментарии. func Example_Functionname(t *testing.T) { // will not compile t.Logf("foobar") // Output: foobar } Напомню, что такие комментарии автоматически переводят примеры в тесты — их прогоняет go test и сверяет их вывод с тем, что идёт в блоке комментария после Output. Что было довольно удобно для проверки (и сохранения!) корректности этих самых примеров. Решать сие предлагают через написание тестов, которые будут вызывать эти примеры напрямую — т.е. точно так-же как мы тестируем обычные функции. Текущий статус accepted, и будет сие вероятно частью релиза 1.28.
📝 net/http/httptest: synctest support Я уже писал про пакет synctest и его возможности. Это была одна из главных фишек Go 1.25, здорово облегчившая тестирование конкурентного кода. Главный минус данного пакета, на мой взгляд, это его слабая интегрированность в стандартную библиотеку. И, похоже, эту проблему наконец решено начать исправлять. Большинство из нас работает с HTTP-серверами (включая gRPC, который является сабсетом HTTP/2) в том или ином виде. Для тестирования этих серверов у нас есть пакет net/http/httptest, который позволяет легко поднять тестовый HTTP-сервер, отвечающий на запросы, используя реальный loopback-интерфейс. Запущенный тестовый сервер также легко создаёт подключенного к себе клиента, который, в свою очередь, работает одинаково хорошо с обычным соединением и TLS-режимом без сложных махинаций с корнем сертификатов. Однако у этого подхода есть минус: так как он использует реальный сетевой интерфейс и порт (и, как следствие, сокет), пакет synctest с ним несовместим. Проблемой озаботился Дэмиен Нил (автор самого synctest) и предложил добавить в пакет httptest новую функцию: NewTestServer. Суть заключается в том, что если на полученном сервере не вызывать метод Start, то все запросы пойдут через in-memory-шину, реализованную на каналах. Таким образом уходит IO, и процесс становится контролируемым через synctest. При этом все запросы клиента, полученного от созданного сервера, вне зависимости от целевого адреса, будут уходить на сервер, породивший клиента (в отличие от клиентов для обычных тестовых серверов, где запросы могут уходить и на сторонние адреса). Если же на полученном сервере вызвать метод Start, он будет вести себя так же, как и раньше вёл себя тестовый сервер - поднимет сокет слушающий на loopback-интерфейсе. Вместе с synctest такой подход позволяет создавать серверные юнит-тесты, где можно отслеживать статус сервера, хендлеров, прочих сущностей в любой момент времени без лишних костылей. И ловить утечку горутин после завершения тестов. Текущий статус: accepted, но я не уверен, что фича попадет в 1.27 из-за скорого фича-фриза.
Я редко пишу сюда о вещах которые не относятся к Go, но тут у меня появилась хорошая статья на вечер. Которая про «архитектуру безопастности» и «самом слабом звене» и перекликается с прошлым сообщением. Только в обычном-бытовом значении. Рекомендую. ПС. Все совпадения реальны 😊️️️️️️.
⚠️ CVE-2026-42501 или почему вам нужно обновиться до Go 1.26.3/go1.25.10 ⚠️ Те кто давно работают с Go (или читают меня) знают, что целостность наших модулей гарантируется не только HTTPS транспортом до самой прокси, но и отдельной базой SumDB, которая хранит в себе хеши модулей подписанные приватным ключом. При этом сама база устроена так, что невозможно внести изменения в прошлые записи, без каскада изменений в новые, тк каждый блок «подписан» предыдущим. Но в ИБ сама безопастность это устойчивость самого слабого звена в цепочке. Достаточно одной маленькой ошибки, что-бы вся надежно выстроенная архитектура ИБ превратилась в пыль. Так и случилось в нашем случае. Рассмотрим сценарий: при получении модуля от прокси, Go проверяет его хеш с использованием SumDB, блоки которой, как я уже говорил выше, подписаны приватным ключом. В псевдокоде это выглядит следующим образом: for _, line := range sumdbResponse { if line относится к нужному module@version { if hash не совпадает { return SECURITY ERROR } } } return nil На первый взгляд, всё кажется логичным: если пришел ответ, проверяем хеш нужного модуля и при несовпадении валимся с ошибкой. При совпадении выходим из функции и продолжаем работу. Однако здесь кроется одна маленькая, но критичная деталь: что будет если нам вернут ответ без записей или запись про другой модуль? Скомпроментированная прокся может отдать специально подготовленный ответ, который наш клиент воспримет как правильный — достаточно выдать ответ с корректными хешами для любых модулей или даже выдать ответ без хешей. Поскольку go get пишет записи в go.sum с локально подсчитанного хеша, подмену никто не заметит, и тулинг спокойно продолжит работу, положив модуль от скомпрометированной прокси в go.mod/go.sum. Который, в свою очередь, будет использован компилятором при сборке проекта. Т.е. перед нами классическая уязвимость которая позволяет организовать Supply Chain Attack (атака на зависимости), пример которой можно описать вот так: 1. Прокся отдаёт клиенту изменённый архив модуля, например rsc.io/foo@v1.0.0. 2. На запрос хеша возвращается валидный, криптографически корректный ответ от checksum database, но для другого модуля, например rsc.io/bar@v1.0.0. 3. go get видит: «ответ sumdb валидный, несовпадающего хэша нет» — и продолжает работу. Т.е. вы получаете измененный модуль, у которого хеш оригинала можно узнать только после удаления go.sum и изменения прокси или использования direct в GOPROXY. Проблема бы не была такой большой (ибо у большинства переменную GOSUMDB, отвечающую за источник правды о хешах, никто не трогает, а для загрузки тулчейна её вообще невозможно переопределить) если бы не два но: • Директивы toolchain и go позволяют выбирать нужную версию компилятора автоматически, и при необходимости, скачивать её с той же самой прокси. • go get всегда сначала спрашивает проксю, может ли та проксировать запросы до базы хешей GOSUMDB. Если ответ положительный, то она просто спросит её о хеше, вместо обращения непосредственно к GOSUMDB. В этой ситуации фактически всю цепочку доставки модулей до клиента (и тулчейнов, если явно не запрещен автовыбор) контролирует один узел. Поэтому, если у вас изменена GOPROXY или вы не доверяете сертификатам TLS установленным в системе, то я очень рекомендую вам обновиться. При этом вам недостаточно обновить версию go (или toolchain) в файле go.mod, ведь изначальный тулчейн будет по-прежнему работать по старой логике. Нужно именно установить новую версию компилятора, заменив ту которая стоит «по умолчанию» в системе, а затем прогнать rm go.sum && go mod tidy в проектах которые могут быть скомпрометированы. При всей опасности атаки, фикс у неё самый тривиальный: return nil просто заменили на return module.VersionError(modWithoutSuffix, fmt.Errorf("verifying %s: checksum missing from sumdb response"+sumdbAbsent, noun)) П.С. Именно поэтому я рекомендую использовать приложения типа direnv для установки переменных GOPROXY или GOPRIVATE только для рабочих проектов.
🩳 proposal: spec: type inferred composite literals 🩳 Вы когда-нибудь задумывались, почему можно писать вот такой код: type T struct{ V int } a := []*T{{0}, {1}, {2}} b := map[string]*T{"a": {0}, "b": {1}, "c": {2}} c := [3]T{{0}, {1}, {2}} Но нельзя вот такой: var x []string = {"a", "b", "c"} var y []*T = {{0}, {1}, {2}} var z T = {0} Всё дело в довольно сложных правилах вывода типов для сложных типов. Ещё в первых версиях создатели языка резонно решили, что одного объявления при инициализации достаточно и нет смысла дублировать идентификатор типа для каждого элемента отдельно. Что, в свою очередь, очень помогло в табличных тестах. Но вот сделать ещё один шаг им не хватило смелости уверенности в том, что код сохранит свою читаемость. А это было и остаётся критически важно для языка. Однако всё течёт, всё меняется, и запрос на tuples (кортежи) или их аналог регулярно всплывал в issue-трекере языка. Причину для этого запроса лучше всего описать следующим кодом: ch := make(chan struct { value string err error }) //... ch <- struct { value string err error }{value: "result"} Расписывать описание анонимной структуры и в момент декларации, и в момент использования очень неудобно, что нередко приводило к ситуациям, когда люди создавали локальный тип (или использовали алиас), чтобы не писать описание повторно. И вот теперь, наконец мы стали TypeScript появится (скорее всего) обратный вывод типов для всех вариантов присваивания. Т. е. станет корректным следующий код: ch <- {value: "result"} fnT({0}) var s []*T = {{0}, {1}, {2}} return {}, err Последнее меня особенно радует, так как во многих кодобазах люди специально использовали возврат указателей из функции, так как не хотели писать длинное имя типа с пакетом. А из дополнительных плюсов, данное изменение приведёт к упрощению (!!!) спеки языка, а значит разработчикам компиляторов Go станет чуть легче жить. Текущий статус: likely accept accepted! и потенциальный кандидат на часть релиза 1.28.
✍️ go fix: apply fixes from modernizers ✍️ Есть такая утилита в тулчейне Go — go fix. В задачу которой входила модернизация кода под новые конструкции — как в языке, так и в стандартной библиотеке. Эдакий аналог go fmt который вместо единого стиля, выравнивал использования языковых конструкций по кодобазе. И вроде отличная идея, но с одной проблемой: почти десять лет никто её не поддерживал и не модернизировал. Те сама утилита осталась в далеких временах Go 1.8. Причина подобного довольно тривиальна: нет необходимости в обобщенной модернизации кода, а значит нет и свободных ресурсов под это. Тем более сам go fix был написан с использованием большого числа костылей, которые усложняли любую попытку модернизации. Однако всё изменилось с приходом LLM: модели OpenAI, Anthropic, Alibaba и прочих обучаются на огромных массивах данных в свободном доступе и код генерируют исходя из увиденного. И тут стало понятно: если модели всегда будут учится на старом коде, то и предлагать они будут всегда старые паттерны. Поэтому, недавно вернувшийся в Google, Алан Донован (да-да, тот самый от кого у вас та самая голубая книжка) всерьез озаботился вопросом модернизации кода. Первый шаг — пускаем старый кода под нож. Полностью. Второй шаг — это полная переделка команды на основе различных анализаторов из gopls. Которые, в свою очередь, используют фреймворк analysis. Результат: у нас есть «модернизатор» кода который заменяет «устаревшие» паттерны их современными аналогами. Приведу несколько примеров. Начиная с 1.26 подъехал новый синтаксис new который умеет в значения. А модернизатор умеет распознавать где он нужен: data, err := json.Marshal(&RequestJSON{ URL: url, Attempts: newInt(10), }) func newInt(x int) *int { return &x } Становится data, err := json.Marshal(&RequestJSON{ URL: url, Attempts: new(10), }) Тоже самое касаемо использования конкатенации строк внутри циклов: s := "" for _, b := range bytes { s += fmt.Sprintf("%02x", b) } use(s) Становится var s strings.Builder for _, b := range bytes { s.WriteString(fmt.Sprintf("%02x", b)) } use(s.String()) Те из коробки мы получаем и более краткий и более производительный код. Еще одна фича, которая доступна ещё и «обычным смертным»: новая аннотация //go:fix inline которая позволяет проводить inlining функций. Те если go fix встроен в ваш пайплайн, все перестановки произойдут автоматически, что удобно когда у вас был большой рефакторинг и внешних пользователей надо уведомить о новом расположении (или сигнатуре) функций. Сама команда Go Core опубликовала отличные статьи которые показывают паттерны использования нового функционала и внутрянку того, как это работает. Рекомендую к прочтению.
А ещё внезапно узнал, что у меня новая фамилия.
Запись нашего февральского с Эдгаром подкаста о том как мы дошли до жизни такой.
Новый выпуск трека Golang в партнерстве с GolangConf! 💪 Как применять будут Go в будущем? Чтобы это понять, надо понять то, как его применяли в прошлом, и самое важное, понять, а почему Go таков, какой он есть? В новом выпуске с Дмитрием Матреничевым мы поговорили на тему того, как родился, развивался и развивается Go? Почему наш язык такой бедный в сравнении с Java? Как его развивали, а также кто? И всегда нужно помнить самое важное - какое он место займет в агенто-центричной разработке? Получился очень насыщенный и самое важное - интересный выпуск про то, как же развивался Go и куда он может идти дальше? Смотрим подкаст и заряжаемся гошной энергией: 🎥 YouTube 🎥 VK
AI или не AI Весьма интересное обсуждение - стоит ли использовать AI для разработки Go? Рас Кокс очень обстоятельно отвечает всем интересующимся: засуньте уже себе в ...! На саммо деле, нет конечно, все очень прилично, но суть примерно как я описал выше.…
AI или не AI Весьма интересное обсуждение - стоит ли использовать AI для разработки Go? Рас Кокс очень обстоятельно отвечает всем интересующимся: засуньте уже себе в ...! На саммо деле, нет конечно, все очень прилично, но суть примерно как я описал выше. Он подчёркивает, что, несмотря на все изменения в индустрии, фундаментальные принципы разработки ПО остаются неизменными. Самое важное - сохранять существующие процессы код-ревью и стандарты качества: код, созданный с помощью AI, должен проходить ту же тщательную проверку, что и написанный вручную, а ответственность за качество, поддержку и отсутствие нарушений авторских прав по-прежнему лежит на контрибьюторе. Хотя Google разрешает использование таких инструментов, участники проекта не должны делегировать им критическое мышление или отправлять код, который они сами предварительно не проанализировали и не доработали #golang #ai https://kodikapusta.ru/news/838-ai-ili-ne-ai Поддержать проект на boosty: https://boosty.to/kodikapusta
🎉 Вышел Go 1.26! 🎉 Не получилось написать об этом день в день, поэтому пишу на следующий день. Ключевое из релиза: • Функция new теперь принимает не только типы, но и обычные аргументы. Т.е. раньше можно было писать new(int), а теперь станет возможным ещё и new(42). Использовать в качестве замены &MyStruct{…} (т.е. new(MyStruct{…}) не рекомендую, тк генерирует менее производительный код. Подробно про новую поведение функции я писал тут. • Type-parameters в типах теперь могут ссылаться сами на себя: type Adder[A Adder[A]] interface { Add(A) A } Это довольно техническое изменение, которое позволит авторам библиотек со всякими коллекциями лучше выражать требования к типам от пользователя. Если вы не сталкивались с этой проблемой, значит вы счастливый человек и для вас ничего не поменяется. • go fix полностью переделали: теперь он модернизирует код, заменяя устаревшие конструкции более современными. Перед тем как применять можно глянуть diff через go fix -diff. Из интересного: с помощью управляющего комментария //go:fix inline над функцией можно заменить её вызов на её тело по коду в автоматическом режиме. Обещают в ближайших постах в блоге рассказать больше о новом go fix и как оно работает внутри. • go mod init при создании нового модуля теперь выставляет версию Go 1.N-1 где N это версия, бинарь которой вы вызываете. Т.е. при вызове на компиляторе go 1.26.0 в go.mod будет выставлено go 1.25.0. Говорят, что сделано для улучшения обратной совместимости внутри экосистемы: чтобы больше людей писало код который будет работать на всех поддерживаемых версиях компилятора. • Green Tea сборщик мусора теперь является основным. Обещают снижение затрат на сборщик мусора от 10 до 40 процентов, и еще 10 сверху если используете новые amd64 процессоры типо Ice Lake и Amd Zen 4. Я так и не назрел написать статью про новый сборщик, тк пока я размышлял об этом, ребята сами написали отличный пост в блоге с картинками и видео. Рекомендую, там много интересных деталей. • Оверхед на вызов функций через CGO теперь меньше на 30 процентов. В практических терминах это значит, что sqlite стал ещё быстрее. • Адрес начала хипа теперь случайный на 64 битных платформах. Если вам это не о чем не говорит, даже не заморачивайтесь 😄. • Экспериментальный профилировщик для «утекших» горутин. Мега крутая фича которую завезли ребята из Uber (параллельно написав научную статью). Поиграться можно вот тут. Фича полностью production ready хотя и активируется с помощью флага GOEXPERIMENT=goroutineleakprofile во время сборки. Причина: хотят собрать фидбэк перед финализацией API. • Новый пакет simd/archsimd для тех кто работает с большими массивами числовых данных. Спрятан за флагом GOEXPERIMENT=simd. • Новый пакет runtime/secret для тех кому есть, что скрывать кто работает с криптографией. Подробнее писал тут. Скрыт за флагом GOEXPERIMENT=runtimesecret. • Новая дженерик функция errors.AsType на замену errors.As. Теперь можно писать pathError, ok := errors.AsType[*fs.PathError](err) Подробнее писал тут. • fmt.Errorf("x") теперь аллоцирует столько же, сколько и errors.New("x"), поэтому у вас меньше поводов делать этот рефакторинг. • Новый флаг -artifacts для go test позволяет настроить каталог для хранения артефактов (файлов которые должны пережить конец тестирования). Сам каталог доступен через T.ArtifactDir и компанию. • Разные оптимизации подъехали. Из трендов: язык явно ускоряется в своей эволюции, поэтому на помощь программистам подтягивают тулинг (go fix) который позволит оставаться «в контексте». Но шестимесячного цикла, ребятам отвечающим за компилятор и библиотеку, явно не хватает и поэтому много вещей находятся за экспериментальными флагами. Получается классическая дилемма регулярных релизов: либо тащить условно готовые вещи, либо регулярно срывать сроки. И судя по другим предложениям которые находятся в статусе likely accept следующий релиз вероятно будет не меньше.
😳😳 proposal: spec: generic methods for Go 😳😳 Я не так часто пишу тексты в выходные, но тут случилось исключение, которое я не ожидал увидеть от слова «совсем»: Go Team (а именно Роберт Гризмер, один их трех «столпов» Go) предлагает добавить в язык дженерик методы. Тут надо сделать отступление: просьбы об этом от сообщества шли давно. Невозможность иметь дженерик методы существенно усложняла часть кода и делала невозможными «поточные» типы. Но сами просьбы разбивались о взаимодействие интерфейсов и дженерик методов у типов: настолько часты были эти просьбы и ответное разъяснение, что в Go FAQ есть специальный пункт про это, который я очень рекомендую прочитать что-бы осознать суть (и глубину) проблемы. We do not anticipate that Go will ever add generic methods Так что-же поменялось? Ну во первых «запрос о добавлении дженерик методов в Go» один из самых популярных. 49085 набрало больше 900 положительных emoji, а вопрос «как сделать дженерик методы в Go» остается одним из самых популярных на профильных ресурсах. Во вторых сам Роберт предполагает, что изначальная постановка о дуализме «методы <-> интерфейсы» может быть неверна. В чем суть нового предложения? По сути предлагается разрешить следующую запись: type S struct { ... } func (*S) m[P any](x P) { ... } type G[P any] struct{ ... } func (*G[P]) m[Q any](x Q) { ... } Да-да, теперь можно написать те самые Type[In any].Map[Out any] методы к которым привыкли программисты из С++/Java|C#. При этом эти методы обладают двумя значимыми ограничениями: Они не реализуют интерфейс даже при совпадении имени и сигнатуры. type I interface { m(int) } type H struct{ … } func (H) m[P any](P) { … } var h H var _ I = h // ошибка: сигнатура H.m где m[P any](P) не удовлетворяет m(int) типа I Пример который более приближен к реальности type Reader struct{ … } func (*Reader) Read[E any]([]E) (int, error) { … } Не удовлетворяет io.Reader даже если в коде есть вызовы (*Reader).Read[byte]. Их нельзя «увидеть» с помощью пакета reflection. Так как инстанцирование методов происходит во время компиляции, рантайм банально не знает как «увидеть» этот метод динамически. И вызвать его тоже не может. Сам proposal прописывает довольно много технических подробностей и необходимую работу внутри компилятора (которой, на удивление, относительно немного). Но главное тут другое: основная мотивация это удобство записи: x.a().b().c() писать легче чем c(b(a(x)) и отсутствие дженерик методов существенно усложняет процесс портирования streaming типов из других языков. Насколько хороша эта мотивация покажет время, но тот факт, что автором предложения является один из оригинальных авторов языков, а так-же то, что предложение (на данный момент) позитивно принято сообщество (400+ позитивных emoji) наводит на мысль, что проблема действительно актуальна. P.S: оригинальный proposal про дженерики содержал такой параграф. Or, we could decide that parameterized methods do not, in fact, implement interfaces, but then it's much less clear why we need methods at all. If we disregard interfaces, any parameterized method can be implemented as a parameterized function. Те авторы уже изначально закладывали вариант, что могут и передумать. Ну вот и передумали. 😁️️️️️️
🚀️️️️️️ proposal: spec: direct reference to embedded fields in struct literals 🚀️️️️️️ Embedding структур часто используется как некая «замена наследованию», для того что-бы объединить общий код для нескольких типов внутри одного встраиваемого типа. Это довольно удобно, тк вместе с полями мы получаем еще и методы структуры, при этом обращаться к ним можно как через имя встроенной структуры, так и напрямую. Например type E struct { A int } type T struct { E } var v T v.E.A = 100 println(v.A) Однако «символьная» инициализация подобной структуры всегда требовала полный синтаксис: v := T{E: E{A: 1}} Читается сие, на мой взгляд, довольно сложно. Есть и другая проблема: такую методику «выноса» нельзя использовать как рефакторинг, ведь мы ломаем инициализацию у пользователей нашего кода. Этим озаботились и сами разработчики компилятора Go, и предложили разрешить обращаться к полям встроенной структуры, как к родным в момент инициализации. Т.е. В будущем можно будет писать и так: v := T{A: 1} Правда есть одно но: а что делать если встроена не структура, а указатель? Кидать панику? Делать «неявную» аллокацию для встроенного указателя? А если таких «встроек» несколько? Из-за потенциальной (и множественной) сложности выводов, на данный момент, принято решение разрешить новый синтаксис инициализации только для полей-значений. Т.е. код type F struct { *E } v := F{A: 100} как и раньше, не скомпилируется. На мой взгляд, это хорошее решение. Статус issue: likely accept accepted!, а значит есть все шансы увидеть новый синтаксис в Go 1.27. П.С. Само предложение датируется аж 2015ым. Похоже Go Team основательно взяла курс на разгребание старых проблем.
Антон традиционно публикует список нововведений которые приедут к нам с Go 1.26. Самое интересное это new(…expr…), pprof для ловли утечки горутин и обновленный go fix. Рекомендую ознакомится, тем более всё представлено в интерактивном виде.
Интерактивный тур по Go 1.26 Опубликовал традиционный тур по будущему релизу (на англ). Часть фич мы с вами уже разобрали, а часть еще разберем, но если хотите прочитать все вместе уже сейчас — добро пожаловать. Вот что вошло: — new(expr) — Безопасная проверка ошибок — Новый «чайный» GC — Ускоренный cgo и выделение памяти — SIMD для amd64 — Секретный режим — Криптография без ридеров — Профиль для ловли утекающих горутин — Метрики состояния горутин — Итераторы в reflect — Подсматривание в байтовый буфер — Дескриптор процесса ОС — Сигнал как причина в контексте — Сравнение IP-подсетей — Dialer с контекстом — Фальшивый example.com — Оптимизированные fmt.Errorf и io.ReadAll — Множественные хендлеры в логах — Артефакты тестов — Обновленный go fix Это жесть сколько всего они в релиз запихнули 😅 https://antonz.org/go-1-26
Про go mod tidy | verify | download (Part 2) Получается, что на практике подделать запись (или случайно закараптить модуль) довольно сложно, тк проверка идет из нескольких мест. Однако потенциальная атака существует: • Атакующий может переписать модуль, его архив и ziphash файлы. • Затем атакующий переписывает хеш внутри go.sum ваших проектов. • Тогда, даже если вы перед сборкой делаете go mod download и go mod verify, то компилятор не заподозрит подмену. Проблема этой атаки заключается в том, что стоит вам вычистить ziphash файлы из кеша и вся эта схема разваливается. Одна сложность — отдельной команды я под это не нашел, но в качестве замены можно взять следующую команду find $GOPATH/pkg/mod/cache/ -type f -name "*.ziphash" | xargs -rn1 rm
Про go mod tidy | verify | download Задумывались ли вы о том, как именно Go проверяет целостность ваших скаченных модулей? Как наш тулчейн проверяет, что зависимости которые вы видите в своем проекте, соответствуют зависимостям которые видят другие разработчики? Я думаю, что нет, ибо работает простой принцип: работает — не трогай. Однако мне, по роду деятельности, пришлось залезть внутрь и прочитать (несколько раз) спеку и посмотреть реализацию. И для того, чтоб структурировать свои изыскания я пишу сей пост. Все начинается с попытки получить модуль. После первого скачивания модуля (через вызов go mod tidy или go mod download на проекте) в формате zip архива с прокси для модулей, Go делает следующее • Сортирует имена файлов в архиве и пробегает sha256 по каждому файлу. Получившийся набор пар "hash filepath" он прогоняет через sha256 еще раз, кодирует его в base64, добавляет префикс h1: и запоминает результат. В общем виде это выглядит как вызов вот такой команды: sha256sum $(find . -type f | sort) | sha256sum • Далее он сравнивает получившийся хеш с записью о зависимости которая хранится в go.sum внутри вашего проекта. Если её нет (или нет go.sum, т.е. вы только начали проект) то он идет в GOSUMDB и спрашивает хеш у него. В обоих случаях, после совпадения хешей, zip файл модуля пишется в локальные загрузки ($GOPATH/pkg/mod/cache/download/<url>) и распаковывается в локальный кеш ($GOPATH/pkg/mod/cache/<url>). Если не совпало, ничего не пишем и кидаем ошибку. Побочный вывод — нельзя называть модуль cache или sumdb, о чем ниже. - Если модуль приватный (выставлены переменные окружения GOPRIVATE и/или GONOSUMDB) или вообще отключена связь с сервером хешсумм через GOSUMDB=off, то проверять хешсумму он будет исключительно с тем, что есть в go.sum внутри проекта. Если там такого модуля нет, то мы доверяем полученным данным и добавляем полученную хешсумму в go.sum. - Если нет прокси для модулей (например вы берёте файлы напрямую с гита) то Go сам создает zip архив и помещает его в скаченное. - Подсчет хеша архива модуля недешевая операция в общем смысле, особенно если её постоянно вызывать. Поэтому, кроме zip файла модуля мы рядом храним полученный хеш в файле c суффиксом .ziphash. • Во время сборки бинаря наш компилятор, чтобы не терять время, не проверяет целостность архива и распакованных данных, а только сравнивает, что строчки в go.sum проекта совпадают с тем, что у нас лежит в ziphash файлах. И тут наступает вопрос: а как проверить, что никто не трогал наш кеш модулей? В дело вступает go mod verify, который пробегаясь по всем модулям из go.mod делает следующее: • Сначала он читает хеш из ziphash файла. • Затем он читает и хеширует распакованные файлы модуля, сравнивая их итоговый хеш с полученным на прошлом этапе. Затем тоже самое повторяется для zip архива модуля. Операция аналогична тому, что происходит при скачивании. • Если не совпало, то verify кидает ошибку на модуль и выходит. Обратите внимание, что здесь go.sum проекта нигде не участвует. Проверяется целостность именно кеша. А что если мне подменили ziphash вместе с файлами модуля и zip архивом? На это есть ряд ответов: • Во время скачивания предполагается, что между нами и БД хешсумм установленно защищенное соединение. • Сама БД хешей, это даже не БД в общем смысле, а дерево, где каждый новый элемент зависит от прошлых вставок. Поэтому при попытке поменять запись развалятся записи о всех хешах модулей которые были вставлены после. Кому интересны подробности рекомендую вот эту статью https://research.swtch.com/tlog и погуглить Merkle Tree. • Когда мы скачиваем хеш, мы получаем не отдельное значение, а некое «поддерево» хешей которое необходимо нам для валидации нашего модуля. Это дерево мы помещаем в $GOPATH/pkg/mod/cache/download/sumdb/ (тот самый) и используем для проверки локального модуля. И тут происходит интересное: если удалить ziphash файл то даже без соединения с интернетом, go mod download сможет восстановить хеши из частей большого дерева лежащих в $GOPATH/pkg/mod/cache/download/sumdb/ и записать их обратно в ziphash.