Skip to content

Ouroboros-Logger 0.4.0

Choose a tag to compare

@the-homeless-god the-homeless-god released this 30 Aug 01:54
· 106 commits to main since this release

Шестой язык — Go

Уроборос умел пять языков; теперь шесть. Разборщик Go лежит в самой поставке
языка
, поэтому доставлять нечего: нужна только команда go — и, в отличие от
Node с Elixir, нужна она и на обмазке, а не только на сборке.

ouroboros wrap-file main.go
go build -o app main.go ouroboros_runtime.go
OUROBOROS_DEBUG_INFO=./debug.info ./app
{"p":"in","t":"2026-08-30T00:59:13.333","id":"6fd9c942-…","ci":-1,"th":"3787200.1","fn":"(*Счётчик).Добавить","a":"2","k":""}
{"p":"out","id":"6fd9c942-…","fn":"(*Счётчик).Добавить","r":"2","d":0.000001}

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

Как это сделано

Отложенное замыкание вместо try/finally. У Go нет ни надстроек над
функцией, ни try/finally, зато есть то, ради чего оба и нужны: defer.
Замыкание, поставленное первым же действием в теле, отрабатывает на любом
выходе — на каждом return, на панике, проходящей сквозь кадр, на достижении
конца тела.

Возвраты не переписываются вовсе. Вместо этого всем возвращаемым значениям
даётся имя прямо в подписи, и замыкание читает их уже после того, как return
их присвоил:

func add(a, b int) (__ouro_r0 int) {
	__ouro_ctx := _ouroEnter("add", a, b)
	defer func() {
		if __ouro_p := recover(); __ouro_p != nil {
			_ouroPanicked(__ouro_ctx, __ouro_p)
			panic(__ouro_p)
		}
		_ouroReturned(__ouro_ctx, __ouro_r0)
	}()
	return a + b
}

Отсюда return f(), пробрасывающий сразу несколько значений чужого вызова,
работает без единого исключения из правила — тогда как C и JavaScript приходится
переписывать каждое место возврата. Тип подписи от именования не меняется, так
что вызывающие, интерфейсы и функции-значения ничего не замечают.

Строки ввоза нет ни одной. Помощник — файл того же пакета, а не подключаемая
библиотека: Go не умеет ввозить соседний файл. Поэтому над заголовком файла
ничего не вставляется, и //go:build вместе с описанием пакета остаются на
своих местах, как того требует язык. Плата за это — помощник обязан объявлять
пакет обмазанного файла, иначе рядом с файлом библиотеки ляжет package main,
который не собирается.

Границы — прочтите до того, как включать

Функции-значения не обмазываются. func(x int) int { … }, записанная в
переменную или переданная доводом, пропускается, и её вызовов в записях
не будет вовсе. Решение то же, что и пропуск lambda у Python, но разница
для читателя настоящая: у Python вложенная def обмазывается и в записи видна
(outer.<locals>.inner), а в Go такой же по смыслу код молчит. Обмазываются
только объявленные функции и методы. Отличить «не звали» от «звали, но не
записано» по записи нельзя.

Непойманная паника печатает не то же самое. Это единственное место во всех
шести языках, где обмазка меняет вывод программы. Чтобы записать вид и текст
паники, замыкание вызывает recover(), а затем panic() заново — иначе поле
x осталось бы без вида и без текста. Поэтому программа, чью панику никто не
ловит, печатает в поток ошибок

panic: bad [recovered, repanicked]

вместо panic: bad. Проверено прогоном: код возврата тот же (2), обычный вывод
тот же, и всякая паника, пойманная самой программой или вызывающей, приходит тем
же значением
— меняется ровно текст сообщения о необработанном падении. Он и
без обмазки был бы другим: в след вызовов входят номера строк, а обмазка их
сдвигает. Убрать это, не потеряв поле x, нельзя: узнать значение паники, не
перехватив её, Go не даёт.

Ещё три мелочи, снятые прогонами: получателя метода в доводах нет (как у C++ с
this; Python, наоборот, пишет self); go vet находит на одну придирку
больше, если через обмазанную функцию идёт по значению структура с замком
внутри — на go build и go test это не влияет; изображение значения идёт
через %v, а он зовёт у значения его собственный String()/Error().

Чем это подтверждено

  • 102 программы равенства вместо 79: каждая собирается и запускается дважды,
    без обмазки и с обмазкой, и сверяются три исхода — код возврата, обычный вывод,
    вывод ошибок. Из них 23 на Go: несколько возвратов, наказ defer, меняющий
    именованный возврат, паника пойманная вызывающим и пойманная собой, горутины,
    os.Exit(3), обобщённые функции, //go:build, кириллица в именах.
  • 835 проверок вместо 742, покрытие с ветвями 99,55 %; у обработчика Go
    оно стопроцентное вместе с ветвями.
  • 20 нарочных порч внесены в код Go и все 20 пойманы. Две из них с первого
    раза прошли — и это были настоящие дыры: не проверялся предел длины одного
    значения и монотонность часов. Обе закрыты новыми проверками.
  • Схема сходится с остальными пятью: tests/test_schema_parity.py гоняет
    один и тот же вызов на всех шести языках и сверяет поля.
  • Инструмент обмазывает сам себя: 21 файл собственного исходника, 227
    функций, обмазанная копия проходит 834 проверки из 835. Единственная упавшая
    упала не от поломки — проверка велит обмазанному инструменту писать записи в
    тот самый файл, который даёт ему читать, и он честно дописывает туда два своих
    вызова.

Чего это стоит

Замеры пересняты целиком, все шесть языков одним прогоном на одной машине
(scripts/measure/run.sh), 20 002 вызова:

язык добавка на вызов байт на вызов
C 19,8 мкс 250
JavaScript 20,3 мкс 238
C++ 22,0 мкс 270
Go 30,0 мкс 244
Python 63,5 мкс 255
Elixir 179,7 мкс 255

Go не попал в тесную полосу 20–22 микросекунд, и разница разобрана: 3,3
микросекунды из тридцати стоит поле th.
Узнать номер горутины стандартная
библиотека даёт единственным способом — разобрать runtime.Stack, — а он обходит
весь стек: 2,655 мкс на пустом и 87,562 на глубине 300. Платится это один раз на
вызов, на строке входа, и до того, как запускаются часы длительности, поэтому
в поле d не попадает.

Установка

brew install digitable-lol/tap/ouroboros
# или
asdf plugin add ouroboros https://github.com/digitable-lol/ouroboros.git
asdf install ouroboros 0.4.0

Оба пути проверены не «команда не выругалась», а работой, и с пустого места:
хранилище формул отцеплено и 0.3.1 снята, затем короткая строка подключила
хранилище сама и поставила 0.4.0; brew test прошёл. Тем же поставленным
инструментом обмазан настоящий файл на Go, собран, запущен — вывод совпал с
необмазанным, — и записи прочитаны им же. Для asdf то же на чистом каталоге
данных.

Если в вашем ~/.gitconfig есть правило insteadOf, переписывающее
https://github.com/ на git@github.com:, то и brew tap, и
asdf plugin add упадут на «Permission denied (publickey)»: клонируют они
сами, и правило переписывает адрес до того, как он до нас доходит. Обход —
снять правило на время либо клонировать вручную.

Образ packaging/Dockerfile собран с golang-go и проверен изнутри: обмазка,
сборка и запуск программы на Go прошли на Debian-овском Go 1.19.8, то есть
помощник собирается не только свежим Go.

Страницы

Языки ·
Границы ·
Замеры