Golang Дайджест
8.23K subscribers
40 photos
1 file
189 links
Самое интересное из мира Go: новости, статьи, проекты, сервисы, изменения в языке и др.

Посты публикуются не часто - только самое важное, с чем я лично ознакомился.

Поэтому можно не мьютить канал =)

Обратная связь: @justskiv
Download Telegram
🐳 Container-aware GOMAXPROCS: Go наконец-то видит лимиты контейнеров

Пост в официальном блоге разработчиков Go, где они подробно рассказывают суть проблемы и как устроено недавнее техническое решение.

Go Team решили давнюю проблему совместимости с контейнерами. До версии 1.25 Go определял все ядра хоста, не учитывая CPU limits контейнера. Теперь GOMAXPROCS выставляется в соответствии с лимитами автоматически.

Суть проблемы:

- Контейнеру выделено 2 CPU на машине с 128 ядрами
- Go видит 128 ядер, создаёт кучу тредов
- Linux throttling тормозит приложение каждые 100ms
- Tail latency растёт

Что изменилось в 1.25:

- Go читает cgroup CPU limits и ставит GOMAXPROCS по ним
- Динамически обновляется при изменении лимитов
- Работает из коробки — просто обновите go.mod
- CPU requests игнорируются (только limits)

————

Наконец-то.. Проблеме много лет, и всё это время приходилось костылить через uber/automaxprocs или ENV-переменные. А теперь оно работает из коробки, как и должно было уже давно.

P.S. В Java эту проблему решили ещё в 2017 году 😩

#go1_25 #go_official #kubernetes #docker
Please open Telegram to view this post
VIEW IN TELEGRAM
20🔥18🤯2🤔1
Николай Тузов
🥂Выпуск про Go 1.25 уже доступен / GoGetPodcast 17 https://youtu.be/fHuJNsZPCJ0 Этого ролика вам точно будет достаточно для полного понимания нововведений, вне зависимости от вашего опыта Подробно обсудили, что нового в новой версии, зачем это нужно, как…
🥂Что нового в Go 1.25 — глубокий разбор изменений / GoGetPodcast

https://youtu.be/fHuJNsZPCJ0

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

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

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

#gogetpodcast #podcast
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥20👍63
Forwarded from Go Update
🏗️ gogrep — инструмент для семантического поиска внутри вашей кодовой базы. 🏗️

Наверняка много кому приходилось сталкиваться с ситуацией «надо по коду найти вызовы по определенному паттерну». Чаще всего для этого мы используем регулярные выражения (grep, ag, rg). Однако у них всех есть один минус — они интерпретируют файлы как текст, а не как код. Из-за этого в результаты поиска попадают как нужные нам места вызова, так и комментарии, участки текста и прочая.

Решение этой проблемы: семантический поиск. Это когда утилита разбивает файл на синтаксическое дерево и производит поиск уже по самому дереву. Приведу простой пример:

~/mws/api > gogrep . 'ptr.Get($_)'


Здесь мы явно говорим: найди все вызовы, у которых слева есть идентификатор ptr а внутри вызова идет только один аргумент (не важно выражение, их сумма, или переменная). Таким образом мы идентифицируем именно конкретные места которые будут частью компилируемого кода.

Документация по gogrep доступна тут. С описанием синтаксиса немного сложнее: большую (и лучшую) часть информации по мэтчингу и по фильтрам можно найти в тестах. Сама тулза является часть куда более мощной тулзы go-ruleguard (которая кстати входит в golangci-lint).

За обе утилиты огромнейшее спасибо Искандеру Шарипову.
🔥11👍1
Go Update
🚀 Расширение функции new для создания указателя на значения 🚀 Отличные новости! Предложение, обсуждение которого которого длится уже больше четырех лет и которое выдвинул сам Роб Пайк, наконец-то подходит к принятию. В чем суть: есть у нас встроенная функция…
Наконец-то можно будет выкинуть из проектов мой "любимый" lo.ToPtr()

Правда остаётся ещё lo.FromPtr(), но надежда уже есть.

Если что, мне жутко не нравится идея тащить в проект samber/lo, но не всегда удаётся с этим бороться. При этом, я даже не знаю что хуже — тащить этого монстра ради пары строк кода, или же желание использовать ещё больше его функционала 😩

К слову, в предыдущем выпуске подкаста тема `lo` очень подробно обсуждалась
в самом конце. Советую послушать, если нужны аргументы.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥32
Forwarded from Go Update
✔️ errors.AsType — типобезопастная замена errors.As ✔️

Тем временем, в 1.26 нас (вероятно) ждет еще одно приятное изменение: дженерики наконец доберутся до пакета errors.

Все изменение проще описать тремя строчками кода. В 1.25 у нас вот так:


var pe *fs.PathError
if errors.As(err, &pe) {
fmt.Println("Failed at path:", pe.Path)
}


А в 1.26 можно будет вот так:


if pe, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println("Failed at path:", pe.Path)
}


Вроде и небольшое изменение, но оно ведет, как минимум, к двум положительным вещам:
• Зона видимости типизированной ошибки во многих участках у нас теперь будет меньше, а значит меньше захламляется пространство имен и снижается необходимость думать над правильным именем для ошибки.
• В отличии от errors.As, который вторым аргументом принимал any, новая функция принимает только тех, кто реализует интерфейс error. Несмотря на то, что у нас есть проверка внутри go vet проверяющая второй аргумент у As, всегда приятнее когда компилятор может самостоятельно поймать ошибку на этапе сборки приложения.

Кстати, причина по которой сигнатура текущей функции выглядит как As(err error, target any) bool заключается в том, что указатель на интерфейс и указатель на тип реализующий интерфейс для компилятора две несовместимые конструкции. Иначе говоря, вот такой код


func As(err error, target *error) bool {
panic("unimplemented")
}



pe *fs.PathError
if As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
}





компиляцию не пройдет. А причина в том, что интерфейсы у нас это отдельная сущность которая существует не только во время компиляции, но и во время выполнения.
🔥303
Forwarded from go-with-me
⚡️ Concurrency Patterns. Fan In
В предыдущем посте мы немного напутали с определением такого многопоточного паттерна как Fan Out

На деле мы показывали Tee, который распространяет одно и то же значение V из канала-источника на N каналов-потребителей

Отличие Fan Out от Tee в том, что на N каналов распространяются разные значения из одного канала-источника. Тобишь, воркеры тянут значения из одного канала, борясь за них насмерть

Лирическое отступление закончено, наша совесть чиста, а сегодняшняя тема будет посвящена Fan In

Этот паттерн является обратным для Fan-Out. Мы собираем данные из нескольких каналов-источников и направляем их в один общий канал-потребитель


1. Default
Итак, что мы имеем?
— Есть воркеры — они кладут значения в N каналов и являются продьюсерами
— Каждый из этих N каналов будет получать значения от своего продьюсера. Назовем такие каналы "стоковыми"
— Есть один общий канал out, туда будет нужно отправить все значения из стоковых каналов
— Для этого мы запускаем N потоков, каждый из которых слушает свой стоковый канал, куда кладет значения продьюсер и редиректит все значения в out

Playground пример

Но что же будет, если какой-то наш продьюсер потух и больше не шлет никаких значений, а передаггый контекст не отменен? Как бы нам понять, что воркер не является активным и перезапустить его? — в этом нам поможет такой механизм как "Heartbeats"

Heartbeat — это регулярное сообщение от продьюсера/воркера, подтверждающее, что он жив и работает


2. Heartbeats
Приступим к рассмотрению этого чуда!

Основная идея проста:
— Имеем структуру, которая хранит в себе стоковый канал, используемый как пайп между воркером и стоком, и канал "сердцебиений"
— Функция Supervise ответственна за отслеживание "сердцебиений" и перезапуск воркера при их отсутствии по TTL
— Функция FanIn принимает на вход стоковые каналы и возвращает результирующий канал, из которого можно читать данные

Всмотримся в наши функции поподробнее

2.1. FanIn
— Не отклоняемся от цели: выкачиваем данные из стоковых каналов и перекладываем в out, реагируя на контекст и неблокирующе отправляя "сердцебиение" нашему супервизору, который пристально наблюдает за нашими воркерами
WaitGroup здесь так же используется для того, чтобы дождаться конца работы наших стоков и отдать управление основному потоку
после дренажа всех "живых" значений

2.2 Supervise
— Создаем стоковый канал и канал "сердцебиений", агрегируем эти значения в структуре Source и возвращаем ее
— В отдельном потоке запускаем нашу рутину по отслеживанию и перезапуску воркеров

2.2.1 Смотрим на внутренности запущенного потока внутри Supervise
— Изначально происходит создание дочернего контекста с отменой для нашего воркера. Этот контекст будет рулить в тот момент, когда наш TTL пройдет и надо будет потушить воркера
— Создаем ticker, который будет слать ивенты, семантически значащие следующее: "в нашего воркера стреляли и он упал в лужу на..."
После получения ивента мы отменяем контекст и воркер окончательно "задыхается в луже"
— Первично запускаем работягу в отдельном потоке
— Если ловим ивент от тикера: производим отмену контекста, переназначаем этот же контекст и функцию отмены, сбрасываем таймер, и запускаем нового воркера в отдельном потоке
— В случае, когда из стокового потока нам пришло "сердцебиение" мы просто сбрасываем таймер и движемся дальше!

Стоит отметить, что воркеры должны "реагировать" на переданный контекст. Без этого мы получим утечку потоков и черт его знает, чем нам это грозит (профилированием и
устранением проблемы, которой можно было бы и избежать)

Таким образом, мы получаем более надежный Fan In, где все источники данных контролируемы и восстанавливаемы при зависаниях

Playground пример


Статью писали с Дашей: @dariasroom

Stay tuned 😏
Please open Telegram to view this post
VIEW IN TELEGRAM
2👍106🔥4🤯1
conc — удобные примитивы для конкурентного кода

https://github.com/sourcegraph/conc

⭐️ ~10.1k

Небольшая библиотека от Sourcegraph, которая предлагает более безопасные и удобные обёртки над стандартными инструментами конкурентности в Go.

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

Что в ней есть интересного:

- conc.WaitGroup — альтернатива стандартному sync.WaitGroup: ловит паники в горутинах и возвращает их в Wait(), а также не даёт забыть вызвать Wait()

- pool — конкурентный пул задач с ограничением числа горутин (WithMaxGoroutines), сбором ошибок (WithErrors, WithFirstError) и отменой задач при ошибках (WithContext)

- stream — параллельное выполнение задач с сохранением порядка результатов (удобно, когда порядок важен, но хочется параллелизма)

- iter.Map / ForEach — упрощённые хелперы для конкурентной обработки слайсов

🟠 Библиотека пока pre-1.0 (последний релиз в феврале 2023), API может меняться.

#library #concurrency #goroutines
Please open Telegram to view this post
VIEW IN TELEGRAM
15👍11🤯2
Как я пишу HTTP сервисы на Go спустя 13 лет

How I write HTTP services in Go after 13 years

Мэт Райер (Grafana, Go Time podcast) делится опытом, как он пишет HTTP-сервисы после 13 лет работы с Go.

Ключевая мысль статьи: не нужен ни фреймворк, ни DI-магия, всё решается стандартной библиотекой и явными зависимостями.

TL;DR:

- NewServer(...) принимает все зависимости конструктором и возвращает http.Handler. Да, список аргументов может быть длинным, и это нормально — зато всё явно.

- Все роуты в одном месте (routes.go), никакой путаницы.

- main() тонкий, реальная логика в run(ctx, ...) — удобно и для тестов, и для graceful shutdown.

- Тесты поднимают сервис целиком (с /readyz и прочим), а окружение передаётся как параметры функции, без обращения к глобальному состоянию (os.Getenv, flag, os.Stdin/Stdout)

- Хэндлеры собираются фабриками, middleware пишутся обычными функциями func(h http.Handler) http.Handler — всё просто и прозрачно.

————

🟢Что я могу сказать.. Такое ощущение, будто статью писал я сам. Хочу, чтобы каждый мой коллега прочитал её дважды! Я устал регулярно объяснять и защищать все те простые вещи, о которых пишет автор.

Мне даже 13 лет на это не понадобилось, просто учителя были хорошие 👴

Единственное, что вызывает вопросы — это «nil-зависимости» в тестах (автор иногда передаёт nil, если зависимость не используется). Я бы всё же предпочёл простые no-op фейки, чтобы не ловить паники внезапно, пусть даже в тестах — поверьте, фиксить тесты без должной гигиены та ещё морока. Пусть сегодня в конкретном кейсе дело не доходит до какой-то зависимости, но завтра дойдёт.

В остальном подход отличный: код простой и читаемый, тесты пишутся легко, нет магии. Отличная статья, особенно для тех, кто только ещё не набил руку в архитектуре сервисов на Go ❤️

Для новичков must read, опытным товарищам тоже лишним не будет ознакомиться.

#article #http #architecture #english
Please open Telegram to view this post
VIEW IN TELEGRAM
1🔥448🤔7👍4
🔍 Утечка горутин в продакшене — разбор реального кейса

- Оригинальный пост
- Обсуждение на Hacker News

Автор рассказывает про классическую production-проблему: out of memory в k8s-подах посреди во время работы сервиса.
Спойлер: виноваты горутины, которые живут вечно.

Как искали проблему:

- Grafana показала растущее потребление памяти горутинами
- Подключили pprof и какое-то время мониторили результаты
- Нашли фабрику бесконечных горутин 🙃

Виновник — функция конвертации канала:

func ToDoneInterface(done <-chan struct{}) <-chan interface{} {
interfaceStream := make(chan interface{})
go func() {
defer close(interfaceStream)
select {
case <-done: return
}
}()
return interfaceStream
}


Проблема: select{} ждёт сигнала из done, но если контекст никогда не отменяется — горутина блокируется навсегда. defer close() там есть, но он не поможет, если select никогда не вернётся.

Инструменты для поиска утечек:

- Grafana + Pyroscope — flame-графы памяти
- pprof с diff_base — сравнение снапшотов до/после
- goleak — для автоматического обнаружения в тестах

Главная мысль от автора: не конвертируйте каналы!
Используйте обычные паттерны работы с context.Context и не забывайте вызывать .Done() там, где нужно.

————

Что ж, вот вам ещё одно напоминание о том, что Go хоть и простой язык, но конкурентность в нём кусается как везде. И go func() — это не fire-and-forget, как кажется новичкам, а обязательство следить за временем жизни горутины. Опытный разработчик 10 раз подумает перед тем как использовать эту конструкцию где бы то ни было.

#article #concurrency #english
Please open Telegram to view this post
VIEW IN TELEGRAM
👍259🔥4
Насколько быстр Go? Симуляция миллионов частиц на смарт-ТВ

https://habr.com/ru/articles/953434/

Автор проверил производительность Go на практике — написал симуляцию миллионов частиц с мультиплеером, которая работает только на CPU, причём настолько легковесно, что запускается даже на смарт-телевизоре.

Задача: симулировать миллионы частиц, синхронизировать их между сотнями клиентов через WebSocket, но при этом не заставлять клиент ничего симулировать — только рисовать.

Подход как из GPU-программирования:

- Используется g-buffer из deferred shading: как там отвязывают количество полигонов от расчёта освещения, так и здесь количество частиц отвязывается от объёма передаваемых данных
- Сервер рендерит всё сам и отправляет клиентам готовые кадры-буферы
- Клиент просто рисует пиксели в canvas — работает везде, где есть браузер
- Стоимость передачи фиксирована разрешением экрана, а не числом частиц

Математика:

Для разрешения 1920×1080 при 1 бите на пиксель получается примерно 260 КБ на кадр. При 30 fps это около 7.8 МБ/с (≈ 62 Мбит/с) на одного клиента. На сервере с пропускной способностью 2.5 Гбит/с можно теоретически обслужить порядка 40 клиентов при Full HD, или 300+ клиентов при мобильных разрешениях (например, 640×360).

Оптимизации:

- Воркер-пулы вместо создания горутин в цикле — pprof показал, что создание горутин не бесплатное
- Lookup-таблицы для побитовых операций вместо прямой упаковки/распаковки — ускорило в разы
- Простой трюк с p := &particles[i] вместо particles[i].x дал +30% за счёт меньшего числа bounds checks
- Двойная буферизация кадров, каналы для неблокирующей отправки, пул буферов

Результат: 2.5 миллиона частиц на 60 fps симуляции + 30 fps отправка клиентам. Работает на смарт-ТВ.

Попробовать можно тут: howfastisgo.dev

————

Статья очень крутая, рекомендую.

Автор честно признаётся в разочаровании: Go без SIMD в чистых вычислениях проигрывает даже JavaScript, не говоря уж про Rust (который на одном ядре обошёл Go на восьми).

Но Go и не позиционируется как язык для тяжёлых вычислений — он про сетевые сервисы и конкурентность.

Мне понравился подход с g-buffer'ом — изящное решение проблемы масштабирования. Дельта-кодирование с зигзаг-кодированием в итоге забросили из-за сложности кода, но это правильный выбор — иногда простота важнее оптимальности.

#article #performance #websocket
1👍27🔥75
if err != nil: почему мы (не) любим обработку ошибок в Go? Обзор предложений по её улучшению

https://habr.com/ru/companies/avito/articles/944824/

Павел Агалецкий из Авито в очередной раз поднимает вечную холиварную тему — обработку ошибок в Go.

Суть проблемы (а вдруг кто-то не в курсе? 🙃):

Go не использует исключения (exceptions) — ошибки это просто значения, которые функции возвращают наравне с другими. Код выглядит многословно:

func doSomething() error {
if err := one(); err != nil {
return err
}
if err := two(); err != nil {
return err
}
if err := three(); err != nil {
return err
}
return nil
}


Сравнение с другими языками:

- Python — исключения есть, но из сигнатуры непонятно, выбрасывает метод исключение или нет
- Java — есть checked exceptions, но большинство функций в языке и библиотеках их не декларируют, т.к. это необязательно
- Rust — есть тип Result<T, E> и оператор ? для проброса ошибок (более компактно)

Что было предложено за годы существования Go:

На GitHub 195 закрытых и 14 открытых proposal по улучшению обработки ошибок. Автор группирует их:

1. check/handle — специальные ключевые слова, чтобы обрабатывать ошибки единообразно
2. try/catch — как в других языках
3. Спецсимволы (? или !) — вместо ключевых слов
4. Упрощение if — тернарные операторы и подобное

Все отклонялись по схожим причинам:
- Можно частично реализовать в userland (через panic/recover + defer)
- Ломается при оборачивании ошибок (fmt.Errorf)
- Появляется неявный control flow
- Код становится менее читаемым

Последний proposal от Ian Lance Taylor:

Ян предложил писать знак вопроса после функций, возвращающих ошибки:

func doSomething(in Dto) (err error) {
validate(in) ?
one() ?
two() ?
three() ?
return nil
}


С возможностью обработки:

validate(in) ? {
return fmt.Errorf("validation failed: %w", err)
}


Реакция сообщества неоднозначная, больше против. Дизлайков в полтора раза больше лайков.

Финал истории:

Robert Griesemer опубликовал статью в блоге Go, где команда объявила, что они больше не будут рассматривать предложения по изменению обработки ошибок. Решили, что на этапе создания языка стоило подумать лучше, но сейчас менять поздно — будет только хуже.

————

Автор делает правильные выводы:
Ошибки как обычные значения — это нормально, явная сигнатура — это хорошо, отсутствие исключений — это прекрасно. Да, многословно, но явно. Да, занимает много места, но зато всё понятно. А современные IDE с autocomplete (особенно с LLM) и code folding помогают справляться с многословностью (у меня в IDE вообще давно настроен хоткей для создания error handling блока: CMD + J).

Я полностью согласен. Лучше писать чуть больше, но понимать что происходит, чем получить "волшебный" синтаксис, который будет работать неявно. Go Team приняли мудрое решение — оставить всё как есть. Язык не должен становиться зоопарком из разных способов сделать одно и то же.

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

P.S. Мне кажется, что все эти годы сообщество пытается запихнуть в Go механики из других языков, вместо того чтобы просто принять философию Go 🤡

#article #error_handling #proposals
Please open Telegram to view this post
VIEW IN TELEGRAM
35👍23
🕵️‍♂️ Как Cloudflare нашли баг в компиляторе Go

- Источник
- Перевод

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

Заваривайте кофе или чаёк и устраивайтесь поудобнее, мы начинаем.. ☕️

Начало истории:

🟠Cloudflare обрабатывает 84 миллиона HTTP-запросов в секунду через 330 дата-центров. При таком масштабе даже самые редкие баги проявляются регулярно.

Один из сервисов начал спорадически паниковать на arm64-машинах с ошибкой "traceback did not unwind completely" — признак повреждения стека. Решили, что это редкая проблема с памятью и не стали копать глубже.

Но паники продолжились.

Первая теория:

- Все критические паники происходили при раскрутке стека
- Коррелировали с recovered panic
- В коде было старое использование panic / recover для обработки ошибок
- Есть похожий баг на GitHub

Убрали panic / recover — паники прекратились. Вздохнули с облегчением.

Но через месяц паники вернулись:

Теперь до 30 в день. Без recovered panics вообще. Никакой корреляции с релизами, инфраструктурой, или положением Марса 🪐

🕵️‍♂️ Расследование:

Все крэши происходили в (*unwinder).next — при раскрутке стека. Два типа:
1. Явная критическая ошибка от среды выполнения
2. SIGSEGV при разыменовании указателя

Заметили паттерн: все segfault'ы происходили при асинхронном вытеснении функции NetlinkSocket.Receive из библиотеки go-netlink.

Что такое асинхронное вытеснение:

До Go 1.14 планировщик был кооперативным — горутины сами решали, когда отдать управление. С 1.14 появилось асинхронное вытеснение: если горутина работает больше 10ms, среда выполнения отправляет SIGURG и принудительно вызывает asyncPreempt.

➡️ Так, для понимания этих деталей коротким ликбезом не отделаться, но у меня есть подробнейшие ролик и статья на эту тему. После них будете кристаллически ясно понимать суть, касательно планировщика, обещаю.

Прорыв:

Удалось получить дамп ядра и посмотреть в отладчике. Горутина была остановлена между двумя инструкциями в эпилоге функции:

nl_linux.go:779 0x555577cb287c ADD $80, RSP, RSP
nl_linux.go:779 0x555577cb2880 ADD $(16<<12), RSP, RSP
nl_linux.go:779 0x555577cb2884 RET


Вытеснение произошло между двумя ADD, которые корректируют указатель стека. Стек оказался в несогласованном состоянии!

Почему две инструкции ADD:

На arm64 непосредственный операнд в инструкции ADD — это 12 бит. Для больших значений компилятор разбивает на две операции: ADD $x, RSP и ADD $(y<<12), RSP. Если прерывание происходит между ними, RSP указывает в середину стека, а не на его вершину.

Простейшее воспроизведение:

//go:noinline
func big_stack(val int) int {
var big_buffer = make([]byte, 1 << 16)
// ... работа с буфером
}

func main() {
go func() {
for { runtime.GC() }
}()
for { _ = big_stack(1000) }
}


Функция со стековым кадром >64KB, GC в цикле для раскрутки стека, и бесконечные вызовы. Через пару минут — segfault!

Суть бага:

1. Асинхронное вытеснение между ADD x, RSP и ADD (y<<12), RSP
2. GC запускает раскрутку стека
3. Раскрутчик пытается прочитать родительский кадр по невалидному RSP
4. Крэш

Условие гонки в одну инструкцию. Невероятно редкий баг! 💎

Исправление:

Для стеков >4KB компилятор теперь сначала строит смещение во временном регистре, потом делает одну атомарную операцию ADD. Вытеснение может произойти до или после, но никогда не во время.

Исправлено в go1.23.12, go1.24.6 и go1.25.0.

————

Очень люблю такие истории!
Месяцы расследования, работа с дампами ядра, изучение внутренностей среды выполнения и ассемблера. И в итоге — баг на уровне компилятора, который проявляется только при стечении обстоятельств: arm64 + большой стек + асинхронное вытеснение + GC + луна в правильной фазе

#compiler #debugging
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥389👍6