Библиотека Go-разработчика | Golang
23.9K subscribers
2.74K photos
50 videos
88 files
5.36K links
Все самое полезное для Go-разработчика в одном канале.

Учиться у нас: clc.to/qaSdww

По рекламе: @proglib_adv

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a4a8c24689c2151c752af0

#WXSSA
Download Telegram
📋 Чек-лист из 8 признаков, что вас скоро уволят

Услышали на созвоне слово «оптимизация» больше трех раз? Поздравляем, Вы уже в процессе.

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

➡️ Пройтись по чек-листу

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика
Please open Telegram to view this post
VIEW IN TELEGRAM
🧑‍💻 В Go предлагают лимит на количество HTTP заголовков

В репозитории Go обсуждают новое поле Server.MaxHeaderValueCount в пакете net/http. Статус уже Likely Accept, идёт финальный период комментариев. Если всё пройдёт гладко, поле появится в Go 1.28.

Сейчас сервер net/http ограничивает заголовки только по общему размеру через MaxHeaderBytes. Отдельного лимита на их количество нет. Этим пользуются атаки типа DoS. Злоумышленник шлёт тысячи мелких заголовков, например с именем в один байт без значения. На проводе такой заголовок почти ничего не весит, но во внутренних структурах сервера каждый занимает заметно больше памяти.

Реализация HTTP/2 в Go к DoS атаке устойчива. Сервер закладывает 32 байта накладных расходов на каждое значение заголовка при подсчёте MaxHeaderBytes. Но пользователи всё равно в неудобном положении. Кому нужны большие заголовки, скажем SSO куки на несколько килобайт, приходится поднимать MaxHeaderBytes. Вместе с лимитом растёт и число мелких заголовков, которые сервер готов принять. Слишком большой лимит открывает дорогу DoS, слишком маленький ломает легитимные запросы. Один параметр на два разных риска.

Предлагаемое решение выглядит так:
package http

// DefaultMaxHeaderValueCount is the maximum permitted number of
// header values in an HTTP request.
// This can be overriden by setting [Server.MaxHeaderValueCount]
const DefaultMaxHeaderValueCount = 500

type Server struct {
// Other fields omitted...

// MaxHeaderValueCount controls the maximum number of header
// values that the server is willing to parse from a request.
// If zero, DefaultMaxHeaderValueCount is used.
MaxHeaderValueCount int
}


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

Ждать релиза Go 1.28 необязательно. Автор предложил переходный вариант через переменную окружения, он оформлен отдельным issue 80020. Запуск выглядит так.
GODEBUG=httpmaxheadervalues=500 ./your-server


Значение из GODEBUG задаёт дефолт, а явно выставленное поле в Server будет иметь приоритет.

Если у вас сервис на Go с большим MaxHeaderBytes, стоит следить за этим proposal. Новый параметр позволит держать оба лимита раздельно и защищаться от лавины мусорных заголовков без риска сломать реальный трафик.

➡️ Наша новостная рассылка не имеет лимитов

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoLive
Please open Telegram to view this post
VIEW IN TELEGRAM
👍111
🤖 Дайджест недели

Первый июльский дайджест

GitHub предлагает выслать код на CD-диске

GitHub иронично отреагировал на решение Sony прекратить выпуск дисков для PlayStation. Теперь разработчики могут заполнить специальную форму и получить свой публичный репозиторий записанным на физический CD-ROM.

1 часть цикла постов про сравнение двух структур

Учимся уходить красиво

gokrazy/rsync v0.3.4

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoLive
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻 Шардируйте локи или ускорение in-memory кеша до 8 раз

Конкурентный in-memory кеш в Go часто пишут через один sync.Mutex. Пока нагрузка низкая, всё в порядке. Но под конкурентным доступом единственный лок превращается в узкое место, и добавление ядер не помогает, а иногда даже вредит.

Миша Стребков собрал один и тот же кеш string → string шестью способами на чистой стандартной библиотеке и прогнал бенчмарки под чтением, сбалансированной нагрузкой и записью на 1–8 ядрах. Порядок победителей меняется в зависимости от профиля, а одно «очевидное» решение с ростом числа ядер работает медленнее.

Шесть вариантов

1. naive это обычная map без блокировок, не потокобезопасна и падает на конкурентной записи.
2. mutex использует один sync.Mutex, прост и корректен, но не масштабируется.
3. rwmutex даёт параллельные чтения и эксклюзивную запись через sync.RWMutex.
4. syncmap это встроенная sync.Map.
5. sharded разбивает данные на 256 частей, у каждой свой мьютекс, ключ выбирает часть по хешу.
6. cow реализует copy-on-write через atomic.Pointer, чтения без блокировок, но каждая запись копирует всю мапу.

➡️Что показали замеры

sharded и cow наращивают пропускную способность с ростом ядер, а mutex остаётся плоским. Более того, у mutex на 8 ядрах пропускная способность падает до 0.66 от одноядерной. Чтения не идут параллельно, а кеш-линия с локом гоняется между ядрами. Вы добавили железо и потеряли производительность.

rwmutex упирается в потолок около 2× и перестаёт расти после 4 ядер. Счётчик читателей сам становится точкой конкуренции. На записи он оказывается хуже обычного мьютекса.

cow выигрывает на чистом чтении с огромным отрывом, 87 миллионов операций в секунду на 8 ядрах. Но как только появляется запись, он проваливается почти в ноль, потому что каждый Set копирует мапу на миллион записей.

sharded единственный дизайн, который держится у вершины во всех профилях сразу.

Победитель в пятнадцати строках

sharded это просто N независимых мап, каждая под своим локом. Хеш ключа выбирает часть, поэтому операции над разными ключами почти никогда не трогают один и тот же лок. Конкуренция падает примерно в N раз:
const shards = 256 // степень двойки, чтобы маскировать вместо modulo

type part struct {
mu sync.Mutex
m map[string]string
}
type Sharded struct{ parts [shards]*part }

func (c *Sharded) at(key string) *part {
h := uint64(14695981039346656037) // FNV-1a
for i := 0; i < len(key); i++ {
h = (h ^ uint64(key[i])) * 1099511628211
}
return c.parts[h&(shards-1)]
}

func (c *Sharded) Get(key string) (string, bool) {
p := c.at(key)
p.mu.Lock()
v, ok := p.m[key]
p.mu.Unlock()
return v, ok
}

func (c *Sharded) Set(key, value string) {
p := c.at(key)
p.mu.Lock()
p.m[key] = value
p.mu.Unlock()
}


Одна деталь про явный Unlock. На Go 1.26 замер показал, что defer p.mu.Unlock() здесь стоит около +8% (примерно 1 нс) поверх явного анлока. На горячем пути это заметно, поэтому горячие методы разблокируют лок явно.

Почему 256 частей

Достаточно, чтобы убить конкуренцию, и не так много, чтобы жечь память. Свип количества частей на 8 ядрах и сбалансированной нагрузке растёт круто до 256, а дальше выходит на плато. Переход от одного лока к 256 даёт скачок в 9 раз (с 4.6 до 43 миллионов операций в секунду). После 256 отдача падает, 1024 добавляют +13%, 4096 всего +18% при кратно большем расходе памяти.

Цифры на 8 ядрах, ns/op, меньше лучше, равномерное распределение

mix          mutex  rwmutex  syncmap  sharded  cow
read-only 168 53 30 21 11.5
read-heavy 168 259 37 22 12000000
balanced 190 282 57 24 46500000
write-heavy 208 222 73 25 82500000


Восьмизначные значения cow в столбце записи реальные и указаны в наносекундах. 82 500 000 нс это 82 миллисекунды на один Set. Такова цена чтений без блокировок.

➡️ Что использовать

По умолчанию берите sharded. Он лучший или почти лучший везде, масштабируется с ядрами и пишется тривиально. Это ответ для большинства конкурентных мап.

cow подходит для данных, которые почти всегда только читают. Это снапшоты конфигов, таблицы маршрутизации, feature-флаги. Чтения непобедимы, но только если записи редкие и батчатся. Для записи он не годится.

sync.Map берите в её нише, ключи пишутся один раз и читаются вечно, либо горутины работают с непересекающимися наборами ключей. За пределами этого она посредственна и аллоцирует (40–72 B/op).

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

Обычный mutex нормален при низкой конкуренции или малом числе ядер. Не тяните за сложность, потребность в которой не можете измерить.

Рост числа ядер сделал кеш на одном мьютексе медленнее, и это чистая конкуренция за кеш-линию самого лока. RWMutex оказался полумерой, которая бьёт по записи. Перекос нагрузки (Zipf) это палка о двух концах, а не равномерный штраф, он ускоряет чтения за счёт кеш-локальности и одновременно концентрирует конкуренцию на записи.

Весь код, сырой вывод benchstat и однокомандный свип для воспроизведения лежат в репозитории.

➡️ Источник

Нашу новостную рассылку не нужно ускорять, она и так быстрая

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥76👍4
🔥 Открытое занятие по AgentOps — курс стартовал!

Сегодня в 19:00 по МСК пройдет первое занятие нового потока, на которое может прийти каждый. Оцените пользу нашего подхода на ретрансляции урока в VK!

👨‍💻 Спикер: Андрей Носов

Тема: Архитектура управления: state machine для AI-агентов

Будем разбираться, как использовать State machine в качестве главного оружия против стохастики (непредсказуемости) LLM.

Что в программе:

● State machine: инварианты и терминальные состояния;
● Паттерны маршрутизации: Supervisor, ReAct, Plan-and-Solve;
● Детекция циклов и настройка аварийных выходов;
● Абстракция от модели: как сделать каркас, который переживет смену LLM/провайдера;
● Адаптация графов под ограничения локальных моделей;
● Версионирование графов и миграции стейта.

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

👉 Подписывайтесь на нашу группу ВКонтакте, чтобы не пропустить старт трансляции!
💻 Фреймворк, где файловая система это карта маршрутов

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

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

goldr
стандартизирует этот слой и при этом оставляет приложение явным.

➡️ Что делает goldr

goldr это server-first, HTML-first, HTMX-native фреймворк на Go. Он не вытаскивает центр тяжести из Go. Файловая система становится картой маршрутов,

.templ
файлы владеют HTML, HTMX остаётся видимым прямо в разметке, хендлеры остаются обычным Go, а сгенерированный код берёт на себя повторяющуюся работу вокруг маршрутов.

Вы получаете полный рабочий цикл приложения на Go и HTMX. Это маршрутные страницы, вложенные лейауты, HTMX-фрагменты, экшены на мутации, сгенерированные хелперы для URL, live reload, отпечатанные и встроенные статические ресурсы, команды инспекции маршрутов и визуальный инспектор в браузере, который умеет подсвечивать области отрендеренной страницы.

При этом приложение по-прежнему владеет своим net/http сервером, миддлварями, статикой, авторизацией, сессиями, парсингом запросов, валидацией и деплоем. goldr не компилирует CSS, не бандлит JavaScript, не регистрирует хендлеры за вас и не выбирает CDN.

➡️ Как устроены маршруты

В goldr файловая система и есть карта маршрутов. Директория маршрута это единица локального поведения:
app/routes/
layout.go -> логика лейаута для / и ниже
layout.templ -> HTML лейаута
route.go -> GET /
page.templ -> HTML страницы
users/
route.go -> GET /users, GET /users/table, POST /users/create
page.templ -> HTML страницы users
by_id/
route.go -> GET /users/{id}
page.templ -> HTML детальной страницы
frag_table.templ -> HTML фрагмента


Один route.go объявляет страницу, HTMX-фрагменты и POST-экшены для этой части приложения:
var Route = goldr.RouteDef{
Page: page,
Fragments: goldr.Fragments{
goldr.FragmentRoute("/table", table),
},
Actions: goldr.Actions{
goldr.Action(http.MethodPost, "/create", postCreate),
},
}


Из файловой системы и этих объявлений goldr генерирует диспатч и типобезопасные хелперы URL. Вместо захардкоженных путей шаблоны ссылаются на сгенерированные хелперы:
urls.Users.Path()
urls.Users.Table.Path()
urls.Users.Create.Path()
urls.Users.ByID.Bind(id).Path()


Важно, что HTMX остаётся видимым на месте вызова. Сгенерированный путь фрагмента подставляется прямо в атрибут hx-get, а не прячется за проприетарными компонентами:
templ UsersView() {
<button
hx-get={ urls.Users.Table.Path() }
hx-target="#users-table"
hx-swap="innerHTML"
>
Refresh users
</button>
<div id="users-table"></div>
}


Директория by_id/ маппится в динамический сегмент {id}. Подчёркивания в статических именах превращаются в дефисы в URL, поэтому Go-безопасное имя build_info/ отдаёт путь /build-info.

➡️ Команды разработки

goldr dev держит локальный цикл в движении. Он генерирует templ и маршруты, отпечатывает ассеты, перезапускает приложение и перезагружает браузер. goldr generate обновляет обвязку маршрутов, хелперы URL, вывод templ и отпечатки ассетов одной командой. goldr check проверяет, что сгенерированные файлы актуальны, ничего при этом не записывая.

Команды routes list, routes explain и routes layouts показывают дерево маршрутов из терминала, а routes refs собирает прямые HTMX-ссылки из .templ файлов.

Если у вас растёт Go-приложение с HTMX, которое должно оставаться читаемым, goldr стоит посмотреть.

➡️ Репозиторий

Иногда маршруты ведут сюда.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
😁32
📎 Клонирование, которое возвращает тот же тип

В Go 1.26 самоссылающееся ограничение дженериков вида [T Constraint[T]] заработало не только для функций, но и для объявлений обобщённых типов. Один из приёмов, которые это открывает, это клонирование с сохранением конкретного типа.

Что это решает

Представьте пайплайн, стадии которого форкаются под A/B эксперименты, и каждому форку нужно своё независимое состояние. Стадии имеют разные конкретные типы.

Если написать обобщённое копирование, которое возвращает интерфейс Stage, вызывающий код получит абстракцию и не сможет использовать результат как *TransformStage без приведения типа. Приведение прячется где-то в глубине кода, компилятор перестаёт вам помогать, а в рантайме появляется место для паники.

До 1.26 выразить «операция над T возвращает T» на уровне объявления обобщённого типа было нельзя. Компилятор ругался на рекурсивный тип.

Как это работает

Идея в том, что параметр типа T появляется в ограничении на сам T. Тип обязан уметь клонировать именно себя:
type Cloneable[T any] interface {
Clone() T
}

// DeepCopy возвращает T, а не Cloneable[T].
func DeepCopy[T Cloneable[T]](v T) T {
return v.Clone()
}


Тот же приём на реальном пайплайне выглядит так:
type Stage[T any] interface {
Clone() T
Execute(ctx context.Context, event Event) (Event, error)
Name() string
}

// ForkStage возвращает T, а не Stage[T].
// Вызывающий получает *TransformStage, без приведения.
func ForkStage[T Stage[T]](s T) T {
return s.Clone()
}


На вызове конкретный тип остаётся живым:
original := &TransformStage{ /* ... */ }
fork := ForkStage(original) // fork имеет тип *TransformStage
fork.SomeSpecificMethod() // доступно без приведения


Тип пронесён через операцию целиком. Ни одного скрытого .(TransformStage) в коде.

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

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
4
💸 ROI на джуна в первый год — отрицательный

Результат джуна близок к нулю, время сеньоров он сжигает колоссально, а зарплата с учётом overhead — всё равно деньги. Бизнес просто взял и пересчитал стоимость сотрудника — и джуны не прошли по цифрам.

👉 Разбираемся, что пошло не так

Новостная рассылка, с которой всегда всё так

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика
Please open Telegram to view this post
VIEW IN TELEGRAM
2
🧑‍💻 Билдеры, которые не ломаются при встраивании

Продолжаем разбирать [T Constraint[T]] в объявлениях обобщённых типов. Это чинит старую проблему флюентных билдеров, где цепочку вызовов ломало встраивание.

Какую боль это решает

У билдера в Go давнее противоречие. Чтобы работала цепочка .WithX().WithY().Build(), каждый метод возвращает *Builder. Пока билдер один, всё в порядке. Но как только вы расширяете его через встраивание, цепочка рвётся. Метод встроенного типа возвращает встроенный тип, а не внешний, поэтому после первого же вызова вы теряете доступ к методам расширения. Приходится либо дублировать методы, либо приводить типы вручную.

Как это работает

Ограничение задаёт набор методов, а параметр типа B несёт конкретный тип билдера через всю цепочку:
type Builder[B any] interface {
WithTimeout(d time.Duration) B
WithRetry(attempts int, backoff time.Duration) B
Build() (Config, error)
}

// ApplyDefaults возвращает B, а не Builder[B].
func ApplyDefaults[B Builder[B]](b B, env Environment) B {
b = b.WithTimeout(env.DefaultTimeout())
b = b.WithRetry(env.MaxRetries(), env.RetryBackoff())
return b
}


На вызове конкретный тип остаётся живым, поэтому специфичный метод вроде WithHeader доступен прямо в цепочке:
client, err := ApplyDefaults(NewHTTPClientBuilder("https://api.internal"), prodEnv).
WithHeader("X-Service-ID", serviceID). // всё ещё доступно
Build()


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

Самоссылка снимает старый компромисс между цепочкой вызовов и расширением билдера. Общие настройки выносятся в одну обобщённую функцию, а конкретный тип переживает всю цепочку, поэтому доменные методы остаются под рукой без приведений. Для конфигураторов, клиентов и любых флюентных API это делает код короче и безопаснее на этапе компиляции.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
👍42
👀 Инфографика по горутинам

➡️ Сохраняйте к себе, чтобы не потерять

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoVisual
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1152
🧑‍💻 Четыре года разработки без документации

Код собирался. Тесты проходили. Но никто в команде не мог объяснить, зачем существуют три пакета. Инженер получил сервис на 47 000 строк Go. Зелёный CI, четыре года в проде, последние 14 месяцев без серьёзных инцидентов.

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

Долг был не в качестве кода, а в понимании

Здесь ломается привычное определение. Обычно техдолг это плохой код. Спагетти, нет тестов, устаревшие зависимости. Но в этом проекте покрытие было 71%, стиль ровный, зависимости под контролем, сборка чистая. Метрики говорили, что всё в порядке.

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

Работать с кодом, чью логику структуры вы не можете восстановить, это не то же самое, что работать с плохим кодом. Вы не отличите «так сделано по хорошей причине, которую я пока не понял» от «так сделано из-за ограничения, которого давно нет». Каждое изменение несёт эту неопределённость.

➡️ Три пакета, которые никто не смог объяснить

sync2 повторял куски x/sync из стандартной библиотеки. Семафоры, errgroup, пул. Из 11 мест импорта семь тянули только Pool, у которого в стандартной библиотеке есть почти идентичный sync.Pool. Скорее всего, когда сервис писали, стандартных примитивов не хватало, sync2 был затычкой. Потом стандартная библиотека догнала, а затычка осталась.

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

transport определял кастомный HTTP-транспорт с ретраями и настройкой пула. Исходящих вызовов было три, два использовали transport, один обычный http.DefaultClient. Никакой причины для разницы. Это было не решение, а накопление. Разные инженеры добавляли вызовы, кто-то не знал про transport, кто-то не посчитал, что он подходит.

➡️ Зелёные тесты врут

Тесты проверяют поведение. Они не проверяют, что поведение вообще нужно, что абстракция под ним правильная, что это тот код, который должен решать задачу. У transport было 23 теста, все проходили. Они проверяли, что ретраи работают и пул настраивается. Они не проверяли, нужен ли этот транспорт вообще.

Вот показательный кусок. У этой функции было четыре теста, все зелёные, но никто не знал, что значит «legacy» в контексте:
func (c *Client) fetchWithLegacyFallback(ctx context.Context, req Request) (*Response, error) {
resp, err := c.primary.Fetch(ctx, req)
if err == nil {
return resp, nil
}

// If primary fails, try legacy endpoint
// TODO: remove when migration complete
return c.legacy.Fetch(ctx, req)
}


Комментарию TODO было два года. Миграция, на которую он ссылался, нигде не задокументирована. legacy-клиент всё ещё вызывался при каждой ошибке. Никто не знал, завершилась миграция или идёт до сих пор.

Налог на онбординг

Время на эту археологию не бесплатное. По заметкам автора счёт вышел такой:

• 11 часов на три загадочных пакета

• 9 часов на разбор кэша против Redis

• 7 часов на цепочку middleware, которая протаскивала значение сквозь четыре слоя ради трейсинга. Библиотеку трейсинга заменили годами раньше, а значение осталось
• 4 часа на разбор utils/

Итого около 31 часа, три четверти рабочей недели. И этот счёт оплатит заново каждый следующий инженер, который придёт в проект.

➡️ Оригинал

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4😁4🤩1👾1
⭐️ Самоупорядочивающиеся типы в Go для доменных значений

Снова поговорим про [T Constraint[T]], ведь оно работает и для объявлений обобщённых типов. Это удобно для коллекций, которые сортируют доменные типы по их собственному правилу сравнения.

Стандартный constraints.Ordered покрывает числа и строки, но не богатые доменные типы вроде Priority, SemanticVersion или Money. Для них порядок задаётся своей логикой, а не оператором <. Обычно это решают через sort.Slice с замыканием на каждый вызов или через хранение значений как interface{} с приведением при каждом сравнении. Первый вариант рассыпает логику сравнения по коду, второй теряет типобезопасность и добавляет работу рантайму.

➡️ Как это работает

Ограничение требует, чтобы тип умел сравнивать себя с себе подобным:
type Ordered[T any] interface {
Less(other T) bool
}

// T обязан реализовать Less(T) bool.
type SortedSlice[T Ordered[T]] struct {
items []T
}

func (s *SortedSlice[T]) Insert(v T) {
i := sort.Search(len(s.items), func(i int) bool {
return !s.items[i].Less(v)
})
s.items = append(s.items, v)
copy(s.items[i+1:], s.items[i:])
s.items[i] = v
}


Доменный тип реализует Less для себя:
type Priority int

func (p Priority) Less(other Priority) bool {
return p < other
}

set := &SortedSlice[Priority]{}
set.Insert(High)
set.Insert(Low)


SortedSlice[Priority]
на этапе компиляции знает, что хранит именно Priority. Вызов .Less прямой, без интерфейсной прослойки в рантайме. Если попытаться вставить тип, который не реализует Ordered[T] для себя, компилятор отклонит его прямо на месте вызова, а не оставит падать под нагрузкой.

Паттерн переносит правило сравнения внутрь самого типа и даёт коллекцию, которая держит порядок для любого доменного значения. Логика Less живёт в одном месте, типобезопасность сохраняется, приведений нет. Для приоритетных очередей, версий и денежных сумм это чище, чем замыкания на каждый вызов сортировки.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
18👍3🔥2
💰 Бонусы-призраки и грейды из параллельной вселенной

Широкая вилка 250–450, а на руки 270? Бонусы до 100%, которых никто в глаза не видел? Оффер, который «горит до конца дня»? Всё это не совпадения, а рабочие схемы, на которых компании экономят прямо сейчас.

👉 Читайте, пока вам снова не пообещали «пересмотрим через полгода»

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика
Please open Telegram to view this post
VIEW IN TELEGRAM
1
👨‍💻 Типизированные подписки на события в Go без приведений

Снова про [T Constraint[T]]. На этом можно построить событийную шину, где обработчик получает конкретный тип события, а не interface{}.

Событийные системы в Go обычно строят одним из двух способов. Либо типизированные каналы, и тогда нужен отдельный канал на каждый тип события, что быстро разрастается. Либо диспатч через interface{} с приведением типа в каждом обработчике. Второй путь гибче, но каждое приведение это место, где код упадёт в рантайме, если тип не совпал. Компилятор при этом молчит.

Как это работает

Событие несёт свой тип полезной нагрузки через самоссылку, а обработчик и шина параметризуются этим же типом:
type Event[T any] interface {
EventType() string
Payload() T
}

type Handler[T Event[T]] func(ctx context.Context, event T) error

type TypedBus[T Event[T]] struct {
mu sync.RWMutex
handlers []Handler[T]
}

func (b *TypedBus[T]) Subscribe(h Handler[T]) {
b.mu.Lock()
b.handlers = append(b.handlers, h)
b.mu.Unlock()
}

func (b *TypedBus[T]) Publish(ctx context.Context, event T) error {
b.mu.RLock()
handlers := b.handlers
b.mu.RUnlock()
for _, h := range handlers {
if err := h(ctx, event); err != nil {
return err
}
}
return nil
}


На стороне вызова обработчик получает конкретный тип, а не пустой интерфейс:
bus := &TypedBus[*UserEvent]{}
bus.Subscribe(func(ctx context.Context, e *UserEvent) error {
log.Println(e.UserID) // реальные поля, без приведения
return nil
})


Добавить новый тип события значит создать новый TypedBus[NewEventType]. Если типы обработчика и шины не сойдутся, компилятор сообщит об этом до выката, текстом вроде *UserEvent does not implement Event[*UserEvent]. Сообщение разбирается не с первого взгляда, но оно появляется на сборке, а не в проде.

Самоссылка даёт третий путь между отдельным каналом на каждый тип и универсальным диспатчем через interface{}. Обработчик работает с настоящими полями события, а несоответствие типов ловится компилятором. Для событийных систем это убирает целую категорию рантайм-багов с приведением типов и делает добавление нового события предсказуемым.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
🔥 ИИ-выходные: Собери своего AI-агента за 2 дня

Хватит просто читать про ИИ, пора собирать автономные системы руками! 1–2 августа пройдет хардкорный онлайн-интенсив для junior-middle разработчиков.

Формат: теория ➡️ сразу практика. Вы будете управлять AI-инструментами.

🛠️ Как всё пройдет:

День 1 (Суббота): разбираем анатомию агента, подключаем инструменты (shell, GitHub, Postgres) и дебажим трейсы.
День 2 (Воскресенье): собираем собственного агента под вашу задачу, настраиваем Eval + Guardrails (чтобы бот не галлюцинировал) и проводим демо.


Для кого: Вы пишете на Python, работаете с Git и терминалом. (С нуля не подойдет, темп очень быстрый!)

Артефакт на выходе: Рабочий репозиторий с вашим агентом, который не стыдно показать команде.
👨‍💻 Спикер: Алексей Жиряков (Сбер, GenAI).

Места ограничены!

👉 Изучить программу и занять место
🧑‍💻 Как не оставить археологию после себя

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

Что автор сделал с унаследованным кодом

Он не стал чистить всё подряд, потому что 47 000 строк за пару недель не разберёшь параллельно с обычными задачами. Подход был такой.

Middleware с мёртвым значением. Удалил сразу. ID запроса протаскивался сквозь четыре слоя ради библиотеки трейсинга, которой давно нет. Правка маленькая и однозначная, поведение не меняется.

Несогласованность transport. Задокументировал, не трогал. Чтобы починить, нужно понять исходные настройки ретраев и актуальны ли они. Вместо правки он написал в пакете подробный комментарий. Что это, когда сделано, под что настроено, какой вопрос открыт.

Избыточность sync2. Убрал частично. Четыре из семи мест с Pool спокойно заменялись на sync.Pool. Три остальных имели тонкие отличия, в которых он не был уверен. Убрал те, где был уверен, остальные пометил.

Разделение cache и Redis. Оставил как есть. Самый запутанный кусок с наименьшим пониманием. Менять поведение, не зная, зачем оно, слишком рискованно.
Это честный подход к чужому долгу. Чините то, в чём уверены. Документируйте то, в чём нет. И не поддавайтесь желанию вычистить всё, потому что чистка без понимания создаёт новый долг быстрее, чем гасит старый.

Пишите комментарии про «почему», а не про «что»

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

Вот как выглядел бы тот самый legacy-фолбэк, если бы автор оставил после себя контекст:
// fetchWithLegacyFallback существует, потому что основной эндпоинт
// был нестабилен во время миграции БД в 2021 году.
// Legacy-эндпоинт только на чтение и медленнее, но стабилен.
//
// Статус (2023-06). Миграция завершена, но снятие фолбэка требует
// проверки, что стабильность основного эндпоинта закрепилась
// на новой инфраструктуре. Отслеживается в issue #1847.
func (c *Client) fetchWithLegacyFallback(ctx context.Context, req Request) (*Response, error) {


Такой комментарий пишется три минуты и экономит полчаса тому, кто откроет функцию следующим.

Ставьте дату у технических решений

Комментарий // TODO: remove when migration complete почти бесполезен, если не знать, когда он написан. Вариант // TODO (2021-03): убрать после миграции, tracked in #1204 говорит, когда решение принято и где искать контекст. Через два года вы просто проверяете, закрыт ли issue #1204.

Трение на онбординге это сигнал долга

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

Убирайте абстракции, которые не можете объяснить

Если вы не можете в одном-двух предложениях сказать, зачем существует абстракция и какую проблему она решает, это пассив. Либо разберитесь и задокументируйте, либо удалите. Абстракция без объяснения это цена, которую платят при каждом следующем контакте с кодом.

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

➡️ Оригинал

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
4
🧠 Невидимые 10 MB

Представьте: есть слайс на 10 MB, а нужны только первые 5 байт. Вы пишете bigSlice = bigSlice[:5] и идёте дальше. Освободит ли сборщик мусора оставшуюся память после этой операции?

Подсказка срез — это всего лишь тройка из указателя на массив, длины и ёмкости. Операция [:5] трогает только заголовок, но underlying array остаётся прежним. А теперь подумайте, куда всё ещё указывает capacity нового слайса и что из-за этого видит GC.

➡️ Правильный ответ

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека Go-разработчика

#ReadySetGo
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🥱1