Глубинный котер
94 subscribers
60 photos
7 videos
4 files
70 links
Download Telegram
Ознакомился со статьей Understanding Real-World Concurrency Bugs in Go о которой узнал из Блог*. Главный вывод: добавление новых семантических конструкций в языке увеличивает количество багов.

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

Из статьи можно сделать вывод, что Go не обеспечивает достаточный уровень проверок на этапе компиляции. Как в том же Rust на уровне теории типов были выявлены и устранены баги в корутинах. Но статья была написана в 2019 году, на текущий момент многие примеры из статьи поднимают ошибки во время исполнения, что уже неплохо.
2👍2
Ох уж этот escape анализ
😁2
package main

import "fmt"

type SelfReferential struct {
r *SelfReferential
field int
}

func make_self_referential() SelfReferential {
s := SelfReferential {
r: nil,
field: 1,
}
s.r = &s
return s
}

func main() {
s := make_self_referential()
s.r.field = 2
fmt.Println(s.field)
fmt.Println(s.r.field)
fmt.Println(s.r.r.field)
}


Ваши догадки, что случится при запуске этой программы:

1. (lawful good) Выведется 2 2 2 без UB (структура аллоцировалась на куче)
2. (lawful evil) Произойдет UB или паника (структура аллоцируется на стеке, в s.r окажется dangling pointer)
3. (chaotic neutral) Выведется 1 2 2 без UB (🤷‍♀️)

#dev #go
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🥰2
Когда глаз дёргается от «функционалов» и «функций» в постановке задачи от бизнеса
🥰3😁21
Книга "Структурированный дизайн" Йордона и Константина, похоже, переживает настоящее переосмысление спустя более 50 лет после первого издания. На неё ссылались Влад Хононов и Кент Бек в своих новых книгах, а теперь на конференции Joker был доклад, посвящённый применению идей из этой книги в современной промышленной разработке. И знаете, что самое интересное? Эти идеи оказались удивительно злободневными, особенно в контексте функционального программирования.

Главная мысль доклада — это важность отделения эффектов от основного потока выполнения. Докладчик прямо говорит об этом и честно признаётся, что похожие идеи распространены в мире функционального программирования. Хотя в докладе рассматриваются только IO-эффекты, это делает идею ещё более наглядной. Например, когда запросы к базе данных смешиваются с бизнес-логикой, получается настоящий "ком грязи", который сложно поддерживать и тестировать. На таких простых примерах становится понятно, почему эффекты — это большая боль для разработчиков: они усложняют формализацию потока выполнения и делают код менее предсказуемым.

Тема эффектов, кстати, сейчас набирает популярность. Недавно вышла книга Effect Oriented Programming с примерами на ZIO, которая тоже поднимает вопросы управления эффектами. Видимо, придётся погрузиться в современные практики приручения эффектов, так что Scala 3, до встречи
👍1🐳1
Глубинный котер
Книга "Структурированный дизайн" Йордона и Константина, похоже, переживает настоящее переосмысление спустя более 50 лет после первого издания. На неё ссылались Влад Хононов и Кент Бек в своих новых книгах, а теперь на конференции Joker был доклад, посвящённый…
В этом докладе я наткнулся на интересный и, казалось бы, очевидный критерий, который помогает оценить, насколько архитектура приложения находится в порядке. Если иерархия вызовов компонентов образует дерево — это хороший знак. Всё работает как надо, компоненты изолированы, и система остаётся поддерживаемой. Но если эта иерархия превращается в граф с множеством связей между компонентами — это явный сигнал, что что-то пошло не так.

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

Этот критерий — отличный способ быстро оценить состояние проекта и понять, где могут скрываться проблемы. Если вы видите "граф" вместо "дерева", возможно, пришло время для рефакторинга
4
Есть API, в который необходимо передавать ключ идемпотентности. На первый взгляд, всё кажется простым: можно использовать любой идентификатор или хэш в качестве ключа. Однако есть нюансы:

1. Нужно сделать N запросов на дочерние ресурсы
2. Ключ идемпотентности должен быть в формате UUID

Возникает вопрос: использовать ли идентификатор дочернего ресурса в качестве ключа или же родительского? Но здесь есть две проблемы:

1. Дочерние ресурсы не имеют UUID-идентификаторов
2. Использование родительского идентификатора не подходит, так как это сделает некорректными смежные запросы для других дочерних ресурсов

Решение сразу приходит на ум: можно склеить идентификаторы дочернего и родительского ресурсов. Но как превратить результат в UUID? Это довольно легко сделать, пример на Go:

concatenated := []byte("<ParentID>/<DaughterID>")
result := uuid.NewSHA1(uuid.Nil, concatenated)


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

Альтернативное решение — честно генерировать UUID, сохранять его в базу данных и использовать до победного конца в ретраях запроса. Но это будет в разы сложней реализовать и тяжело поддерживать в будущем
👍31
Уже как вторую неделю продолжаю изучать Scala 3 и ZIO. Изначально я заинтересовался этим стеком после прочтения книги Effect-Oriented Programming, где примеры были приведены на этих технологиях. По завершению чтения у меня сложилось впечатление (возможно, ошибочное), что основная идея ZIO — это решение проблемы эффектов через инъекцию зависимостей и модульность системы. Каждый компонент с эффектами изолирован, что упрощает его замену и тестирование. Но к тем же выводом приходят и другие направления в промышленной разработке, тот же test first подход.

Из-за этого я не увидел особых преимуществ ZIO (не судите строго, Scala-разработчики). Похожие подходы можно найти во многих других практиках, а тесты в официальных примерах ZIO кажутся недостаточно лаконичными. Для сравнения я взял Kotlin с библиотеками Arrow и Ktor.

Рассмотрим реализацию эндпоинта для создания пользователя на основе данных из запроса. Сначала пример на ZIO:


Method.POST / "users" -> handler { (req: Request) =>
for {
u <- req.body.to[User].orElseFail(Response.badRequest)
r <- UserRepo
.register(u)
.mapBoth(
_ => Response.internalServerError(s"Failed to register the user: $u"),
user => Response(body = Body.from(user))
)
} yield r
}


А теперь аналогичный пример на Kotlin:


post<UsersResource> {
either {
val user = receiveCatching<User>().bind().user
UserRepo
.register(user)
.bind().user
}
.respond(HttpStatusCode.Created)
}


В целом, код на ZIO выглядит менее лаконичным, хотя выразительнее на подобии конструкций вроде body.to[User]. При этом UserRepo в Scala выводится из типа функции Routes[UserRepo, Response], тогда как в Kotlin репозиторий передаётся через параметры функций. Оба подхода работают с ошибками как с монадами, но в Arrow, на мой взгляд, код получился минималистичней.

Теперь посмотрим на тесты. Пример на Scala:


test("Can register user") {
for {
client <- ZIO.service[Client]
_ <- TestServer.addRoutes(UserRoutes())
port <- ZIO.serviceWith[Server](_.port)
url = URL.root.port(port)
createResponse <- client(
Request.post(url / "users", Body.from[User](testUser))
)
result <- getResponse.body.to[User]
} yield assertTrue(result == testUser)
}.provideSome[Client with Driver with UserRepo](
TestServer.layer,
Scope.default,
InmemoryUserRepo.layer
)


Аналогичный тест на Kotlin:


"Can register user" {
withServer {
val response =
post(UsersResource()) {
contentType(ContentType.Application.Json)
setBody(testUser))
}

with(response.body<User>().user) {
assert(username == testUser.Name)
assert(email == testUser.Email)
}
}
}


Субъективно, Scala с ZIO действительно использует систему типов на полную, но, к моему удивлению, конечный код получается более многословным, чем на Kotlin с Arrow и Ktor. Это особенно заметно на примере тестов, где в Kotlin вся "шелуха" скрыта за withServer. Кроме того, работа с результатом через bind() в Arrow позволяет избежать дублирования обработки ошибок (вспомним err != nil из Go). В Rust также используется bind() для работы с ошибками, и это действительно удобно.

При этом работа с эффектами в ZIO будто бы не сильно отличается от Arrow c Ktor, скорее всего просто не до конца понял фишку. Но пока вывод один: эффекты это зло от которого нет серебряной пули в виде фреймворка.
4👍2
Есть мнение, что одна из ключевых особенностей Go — это кодогенерация. В Go многие проблемы решаются генерацией нового кода, начиная от работы с Protobuf и заканчивая созданием моков. Долгое время в языке не было дженериков, потому что считалось, что кодогенерация может их заменить.

Если копнуть глубже в другие стеки, то окажется, что кодогенерация там используется так же часто, как и в Go, но она скрыта от глаз разработчиков. В языках без сборщика мусора кодогенерация часто ассоциируется с макросами. Например, в Rust макросы активно используются для генерации кода на этапе компиляции. Возьмем макрос vec!, который генерирует код для создания и инициализации вектора:

let v = vec![1, 2, 3];


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

Spark генерирует Java-код на лету для выполнения map-reduce операций. Разработчик пишет высокоуровневые трансформации, а Spark самостоятельно генерирует и оптимизирует низкоуровневый код.

SQLDelight генерирует Kotlin-классы на основе SQL-запросов, описанных в .sq файлах. Разработчик пишет SQL, а SQLDelight создает типобезопасные методы для работы с базой данных. Есть похожее решения под Java — JOOQ, а под Scala — Quill.

Отдельного внимания заслуживают плагины компилятора Kotlin, которые занимаются кодогенерацией на этапе компиляции. Например, компилятор автоматически генерирует методы equals, hashCode, toString и copy для data-классов. Через плагины компилятора также работает библиотека kotlinx.serialization, которая генерирует код для сериализации и десериализации данных.

Кодогенерация — это мощный инструмент, который используется во многих стеках, но в Go она явная, а в других скрыта от конечного пользователя.
6👍1😱1
В Go существует интересный нюанс, связанный с компиляцией функций, которые формально возвращают значение, но фактически ведут себя как функции, возвращающие Unit (т.е. ничего не возвращают). Рассмотрим пример:

func unitInFact() bool {
for {} // Бесконечный цикл
}


Несмотря на то, что функция unitInFact() объявлена как возвращающая bool, она успешно компилируется, хотя и не содержит явного return. Это поведение объясняется правилами проверки terminating statements на этапе семантического анализа.

При компиляции проверка тела функции происходит в методе funcBody, где ключевым является следующий фрагмент:

if sig.results.Len() > 0 && !check.isTerminating(body, "") {
check.error(atPos(body.Rbrace), MissingReturn, "missing return")
}


В нашем случае:
- sig.results.Len() возвращает 1 (функция объявлена с возвращаемым значением)
- check.isTerminating(body, "") возвращает true (тело функции считается завершающим)

Проверка цикла на принадлежность к terminating statements происходит в методе isTerminating:

case *ast.ForStmt:
if s.Cond == nil && !hasBreak(s.Body, label, true) {
return true
}
}


Условие выполняется, если:
- Цикл не имеет условия (for {})
- Тело цикла не содержит операторов break (включая неявные)

Это поведение не просто особенность реализации, а часть официальной спецификации Go:

Если сигнатура функции объявляет возвращаемые параметры, список операторов в теле функции должен заканчиваться terminating statements


Такой прагматичный подход к системе типов заметно отличается от более строгих проверок в языках вроде Kotlin или Haskell, где подобные случаи обычно считаются ошибками типизации
51
Глубинный котер
В Go существует интересный нюанс, связанный с компиляцией функций, которые формально возвращают значение, но фактически ведут себя как функции, возвращающие Unit (т.е. ничего не возвращают). Рассмотрим пример: func unitInFact() bool { for {} // Бесконечный…
Вообще такой ход конем вызван тем, что в Go нет типа суммы. А на практике может пригодиться функция с бесконечным циклом, которая возвращает ошибку, исключений же нет. Поэтому дизайнеры языка пошли на этот компромисс, чтобы не усложнять семантику, а значит замедлять время компиляции.

Причем это не единственный хак в компиляторе Go. Один из самых знаменитых, это запрет открытия фигурной скобки на новой строке при объявлении структуры или функции. За счет этого разработчики языка убили двух зайцев: единый формат (привет С/С++) и упростили парсер
🔥62
Всегда было интересно — откуда такая уверенность в гарантиях Rust? Как компилятор может быть абсолютно уверен, что в программе не будет:

- Гонок данных
- Use-after-free
- Висячих ссылок

Тесты проверяют лишь конечное число случаев. Математические доказательства — вот что дает стопроцентную гарантию. И здесь на сцену выходит Coq — мощный инструмент формальной верификации. Coq позволяет:

- Формально описывать поведение программ
- Доказывать их свойства математическими методами
- Верифицировать всё — от алгоритмов до целых языков программирования

Простой пример — доказательство чётности числа:


Definition is_even (n : nat) := exists k, n = 2 * k.

Lemma two_is_even : is_even 2.
Proof. exists 1. reflexivity. Qed.


Но нас интересует не арифметика, а безопасность памяти в Rust. Проект RustBelt формально верифицирует ключевые механизмы Rust:

- Систему владения (ownership)
- Заимствование (borrowing)
- Времена жизни (lifetimes)

Рассмотрим, как в Coq моделируются правила заимствования. Сначала определим типы:


Inductive permission :=
| Unique (* Эксклюзивный доступ (аналог &mut T) *)
| SharedReadWrite (* Чтение и запись *)
| SharedReadOnly (* Только чтение (аналог &T) *)
| Disabled. (* Доступ запрещен *)

Inductive access_kind :=
| AccessRead (* Запрос на чтение *)
| AccessWrite. (* Запрос на запись *)


Затем реализуем логику проверки прав:


Definition grants (perm: permission) (access: access_kind) : bool :=
match perm, access with
| Disabled, _ => false (* Все операции запрещены *)
| SharedReadOnly, AccessWrite => false (* Запись запрещена *)
| _, _ => true (* Во всех остальных случаях разрешено *)
end.


В этой модели:

- SharedReadOnly соответствует &T в Rust
- Unique соответствует &mut T
- Disabled — ситуация, когда доступ к данным невозможен

Интересно? Недавно вышел курс лекций на русском по основам Coq от блокчейн-инженеров. Да, в криптовалютных проектах Coq тоже используют — для верификации смарт-контрактов
🔥42
Маркетинг в опен-сурс, который мы заслужили

github.com/mark3labs/mcp-go
😁3
Команда Go разрабатывает новый алгоритм сборки мусора — Green Tea 🍵, который радикально меняет подход к разметке памяти. Вместо обработки отдельных объектов он работает с целыми блоками по 8 КБ, но только для маленьких объектов (≤512 байт). Уже есть экспериментальная реализация, и в Go 1.25 её можно будет протестировать.

Сейчас Go использует классический алгоритм трёхцветной разметки, который:

- Не учитывает расположение объектов в памяти, сканирует указатели вразнобой
- Тратит 85% времени на цикл сканирования, из которых 35% CPU-циклов уходят просто на чтение памяти (из-за плохой локальности)
- Плохо масштабируется на системах с десятками ядер и сложной топологией памяти (NUMA, HBM)

Вместо работы с отдельными объектами, Green Tea оперирует целыми 8 КБ (для уменьшения промахов L1/L2) блоками называемыми интервалами, но только для маленьких объектов. Такой фокус на маленьких объектах обусловлен тем, что сборка их новым алгоритмом дает наибольший выигрыш:

- Дешевле сканировать блоками, чем по одному
- Они чаще создают фрагментацию и нагрузку на GC

Крупные объекты обрабатываются старым алгоритмом, чтобы не усложнять логику. Новый алгоритм Green Tea работает следующим образом:

- Каждый 8 КБ интервал хранит битовую маску (серый/чёрный биты) для своих объектов
- Если в блоке появляется новый указатель, он помечается серым, а весь блок ставится в очередь на сканирование
- Когда блок извлекается из очереди, все помеченные объекты в нём сканируются разом

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

Бенчи в целом хорошие:

- CPU-затраты GC снизились на 10–50% в CPU-bound сценариях
- В 2 раза меньше промахов кэша (L1/L2)
- Тесты компилятора Go показывают незначительный регресс (~0.5%)

Oригинал
3🔥2
На мой взгляд, одна из самых красивых идей в программировании — соответствие Карри-Ховарда. Это концепция, которая связывает программирование с логикой, показывая, что программы можно рассматривать как доказательства логических утверждений.

Обычно мы думаем о программах как о способах обработки данных: чисел, списков, функций. Но что, если взглянуть на это с другой стороны? Что, если программа — это доказательство того, что ее тип обитаем? То есть, если программа компилируется, она доказывает, что ее тип не пуст.

Например, рассмотрим простую функцию, которая создает пару:


let pair x y = (x, y)


Тип этой функции:


val pair : 'a -> 'b -> 'a * 'b


Теперь давайте переведем это на язык логики. Тип 'a * 'b соответствует логической формуле A /\ B, где /\ — это конъюнкция (логическое "и"). Таким образом, функция pair доказывает, что если у нас есть доказательства для A и B, мы можем создать доказательство для A /\ B.

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


let swap (x, y) = (y, x)


Тип этой функции:


val swap : 'a * 'b -> 'b * 'a


В логике это соответствует доказательству того, что если у нас есть доказательство для A /\ B, мы можем получить доказательство для B /\ A. Процесс вычисления функции swap можно рассматривать как упрощение этого доказательства.

Если тема показалось интересной, то в одном из блоков курса cs3110 ждут упражнения на OCaml 🐫
3
Недавно я наткнулся на интересную поделку — Dockerfile Kotlin DSL. Да, вы не ослышались, это DSL на Kotlin для написания Dockerfile. Сомнительно, но представьте, как это удобно: вместо того чтобы писать Dockerfile в текстовом формате, вы можете использовать Kotlin для создания Dockerfile прямо в вашем Gradle-конфигурационном файле. Вот как выглядит простенький DSL для JVM-приложения:


dockerfile {
from("openjdk:21-jdk-slim")
workdir("/app")

+"Копируем JAR-файл в Docker-образ"
copy {
source = "app.jar"
destination = "/app/app.jar"
}

cmd("java", "-jar", "app.jar")
}


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

Главная мотивация автора — избежать дублирования Dockerfile для разных сред (staging/production). Вместо того чтобы создавать отдельные Dockerfile для каждой среды, вы можете использовать один DSL-файл, который будет генерировать Dockerfile в зависимости от переменных окружения. Например, под разные окружения можно загрузить разные артефакты в образ:


+"Загружаем активы"
val assetPath =
when (System.getenv("MODE")) {
"local" -> "/first_100_assets.zip"
"staging" -> "/compressed_assets.zip"
else -> "/full_assets.zip"
}


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

Для меня, Dockerfile Kotlin DSL — это еще один пример того, насколько мощным может быть Kotlin для создания DSL. Он позволяет писать Dockerfile как код, что делает его более гибким и удобным для управления. И это не единственный пример. Есть ещё Exposed — ORM для Kotlin от JetBrains. В ней также удалось минимизировать семантический разрыв между DSL и SQL. Сравните SQL-запрос:


select * from user
where vibe = 'cheel'
and status = 'active'


с его аналогом на Exposed DSL:


User
.select { (User.vibe eq "cheel")
and (User.status eq "active") }


И это потрясающе. Восхищаюсь командой разработки языка, потому что до этого мир видел подобные DSL на динамически типизированных языках, как Ruby и Groovy. Но чтобы на статически типизированном и без необходимости получать PhD по теории категорий, это впервые 🔥
🔥3👍1👎1💩1
Мимо меня чуть не прошла одна из главных новостей этого года — поддержка асинхронного ввода-вывода через io_uring в PostgreSQL 18. Напомню, что io_uring — это новая подсистема асинхронного ввода-вывода, разработанная для Linux, позволяющая асинхронно управлять операциями чтения-записи на диск. До недавнего времени операции ввода-вывода с файловой системой были синхронными и блокирующими.

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

Основные компоненты PostgreSQL, затронутые изменениями:

- Sequential Scans: Последовательные сканирования таблиц теперь выполняются быстрее, поскольку данные считываются параллельно и обработка выполняется независимо друг от друга.

- Bitmap Heap Scans: Этот метод поиска данных стал более эффективным благодаря параллельному доступу к индексированным данным.

- Vacuum Operations: Очистка таблиц от устаревших записей также получила ускорение благодаря возможности параллельного исполнения операций удаления.

Эти улучшения были достигнуты именно за счёт внедрения асинхронного механизма, основанного на io_uring. Тестирование показало впечатляющие результаты: улучшение производительности вплоть до 2–3 раз 🔥

Главная причина прироста производительности заключается в устранении задержки при операциях ввода-вывода. Когда PostgreSQL отправляет запрос на чтение файла, процесс блокируется до завершения операции. Но с использованием io_uring база данных продолжает обрабатывать другие запросы, пока идёт операция ввода-вывода. Это значит, что даже самые тяжелые нагрузки на базу данных обрабатываются эффективнее.

Release notes
🔥11
Friendly error message наше всё. Да, JetBrain?
🤡3👍2
This media is not supported in your browser
VIEW IN TELEGRAM
На днях наткнулся на апрельский маркетинговый ролик от JetBrains, где собрали максимум хайпа: MCP, AI Coding Assistant, IoT приправленный Kotlin, только RAG не хватало. В видео автор вместе со зрителями пишет наык для Алисы MCP-сервер для управления лампочкой через LLM. Понятно, что ролик рекламный, но есть пара моментов, которые меня зацепили, о которых расскажу ниже.

Похоже, JetBrains первыми среди разработчиков IDE догадались сделать интерактивную работу ассистента: LLM сам пишет код, пытается его запустить, пока тесты не станут зелёными. Насладиться можно на отрывке видео.

Похожая фича появилась недавно у Jules от Google. Хотя такая работа ассистента кажется очевидной, реализовать её начали только сейчас — раньше разработчики самостоятельно создавали такие тулзы, как тут.

MCP только набирает популярность, а в JVM-экосистеме уже всё готово:

- Официальные SDK для MCP, в отличие от модных Rust и Go, для которых ещё нет SDK.
- Spring AI — для упрощения работы со всем циклом разработки LLM-приложений.
- Официальный плагин MCP в IntelliJ для обеспечения связи между LLM и средой разработки.

В целом здорово, что развивается вся экосистема, а разработчики языка не ограничиваются одним лишь туториалом, как их коллеги. Хотя в том же Go есть родная версия LangChain, похожего проекта для других языков кроме Python не встречал.
👍42