Открытый канал ID 70451103686846

Библиотека Go разработчика | Golang

Реклама: t.me/tproger_sales_bot

Каналов в рубрике
1 736
Место этого канала
№ 292
Открыть рубрику →
Каналов в рубрике
17 377
Место этого канала
№ 2 781
Открыть рубрику →

Публикации за период

По реакциям

Поиск и вся лента — после бесплатной регистрации. Свои даты — по подписке; владелец канала видит их бесплатно. Сортировка по реакциям — по подписке. Зарегистрироваться

Загрузка…

🔍 LeetCode Daily: считаем подстроки за один проход

Задача дня на LeetCode — Number of Strings That Appear as Substrings in Word. Дан массив строк patterns и строка word. Нужно вернуть количество строк из patterns, которые входят в word как подстроки.

Подстрока — это непрерывная последовательность символов внутри строки. Если patterns = ["a","abc","bc","d"] и word = "abc", ответ будет 3: входят "a", "abc" и "bc", а "d" нет.

➡️ Решение
 
В Go для этого есть готовый инструмент — strings.Contains. Проходим по каждому паттерну и проверяем вхождение. Если вошло, увеличиваем счётчик.

import "strings"
 
func numOfStrings(patterns []string, word string) int {
count := 0
for _, p := range patterns {
if strings.Contains(word, p) {
count++
}
}
return count
}

➡️ Сложность
 
strings.Contains внутри использует алгоритм поиска подстроки. При длине word = n и pattern = m это O(n·m) в худшем случае. С учётом ограничений задачи в 100 символов это несущественно.

➡️ Решить
Просмотры: 232
Перейти в пост Открыть в MAX Сообщить о неточности
🤩 Фаззинг находит то, что вы не догадались проверить

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

➡️ Идея в одном сдвиге

Вы пишете не пример, а свойство, которое обязано держаться всегда. Машина сама ищет вход, который его ломает. Вместо «на входе X жду Y» вы описываете инвариант:

func FuzzParse(f *testing.F) {
f.Add("platform=5000") // сиды это ваши готовые кейсы
f.Fuzz(func(t *testing.T, rule string) {
team, limit, err := ParseBudgetRule(rule)
if err != nil {
return // ошибка на кривом входе это норма
}
// раз ошибки нет, результат обязан быть валидным
if limit <= 0 || team == "" {
t.Errorf("bad result without error for %q", rule)
}
})
}

Частое заблуждение, что фаззер сыплет случайные байты. Современные фаззеры coverage‑guided. Стартуют с ваших сидов и мутируют их, отслеживая, какие мутации открывают новые ветки. Попал в новый путь, копает оттуда. Поэтому к багам он сходится в разы быстрее слепого рандома. Юнит‑тесты проверяют дороги, которые вы построили, фаззер ищет те, о которых вы не знали.

❓ Что ловит

Класс ошибок на стыке кода и реальности. Паника на входе, которого «не бывает».

parts := strings.SplitN(rule, "=", 2)
limit, _ := strconv.Atoi(parts[1]) // "platform" без = и parts[1] не существует
// panic: index out of range [1] with length 1

Нарушения инвариантов, когда функция вернула мусор без ошибки, что хуже явной паники. Расхождения round‑trip, когда decode(encode(x)) не равно x. Их объединяет одно. Все появляются, когда снаружи приходит то, чего код не ждал. Обычные тесты это скипают, потому что их пишут под ожидаемые сценарии, а вы рассуждаете как автор кода, который знает, как им пользоваться. Фаззеру эта рамка незнакома.

Найденный падающий вход не исчезает. Go сам кладёт минимальный контрпример в testdata/fuzz/ и делает из него регрессию:

go test -fuzz=FuzzParse -fuzztime=30s # ищем баг
go test ./... # корпус гоняется всегда, даже без -fuzz

Один раз пойманный баг вернуться уже не может.

➡️ Куда прикладывать

Туда, где код разбирает или валидирует внешний вход. Парсеры, декодеры, заголовки, query, конфиги, всё, что ветвится по содержимому. Чем больше условной логики, тем больше путей фаззеру. Не стоит фаззить чистую бизнес‑логику без зависимости от входа и функции, которые ходят в сеть или базу, их фаззер дёрнет тысячи раз. Правило короткое. Зависит поведение от того, что прислал внешний мир, заведите фаззинг.

❓ Почему окупается

Барьер почти нулевой. В Go фаззер встроен в тулчейн с версии 1.18, свойство пишется в считаные строки. Цена это пара минут на разработке. Альтернатива это ждать, пока вход за вас подберёт прод. Боевыми данными, под нагрузкой, в три часа ночи, в виде алерта. Прод пофаззит ваш код в любом случае, вопрос лишь в том, узнаете вы о баге от теста или от дежурного.
Просмотры: 205
Перейти в пост Открыть в MAX Сообщить о неточности
📎 Главная проблема в микросервисах

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

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

Папки вместо архитектуры
Спросите десять команд про структуру сервиса, и почти все опишут что‑то такое.

/cmd
/internal
/handlers
/services
/repository
/models
/pkg

Выглядит разумно и проходит ревью. Беда в том, что папки создают иллюзию границ, но не держат их. Ничто не мешает хендлеру импортировать репозиторий напрямую. Ничто не мешает модели обрасти бизнес‑логикой. Границы косметические, и код деградирует предсказуемо. Логика переползает туда, где удобнее, обычно в хендлеры, потому что там контекст.

Типы репозитория протекают в API, потому что заводить отдельный DTO лень. Пакет models превращается в общий мешок структур, от которого зависят все, и поменять в нём что‑либо без каскада уже нельзя. Получается слоёная на вид архитектура, ведущая себя как монолит с накладными расходами на HTTP.

В чём контракт слоёв

Контракт про направление зависимостей. Они текут внутрь. Внешний слой (HTTP, gRPC, CLI) знает про слой приложения. Слой приложения знает про домен. Домен не знает ни про что.
На практике это значит вот что. Хендлер принимает запрос, зовёт метод сервиса и сериализует ответ. Никакого SQL и никаких бизнес‑правил в нём нет.

Сервис кодирует, что приложение делает. Он оркестрирует, валидирует, делегирует репозиторию и ничего не знает про HTTP. Репозиторий говорит с базой и возвращает доменные типы или ошибки, но не pgx.Rows.

Доменные типы несут инварианты. Структура Member знает, что делает участника валидным, но не знает, как сериализовать себя в JSON. Когда контракт держится, слои тестируются независимо, реализацию репозитория можно подменить, не трогая хендлер, а gRPC добавить без дублирования логики.

Анти‑паттерн god service

Чаще всего контракт ломает god service. Сервис‑структура, которая всасывает всё:

type MemberService struct {
db *pgxpool.Pool
}

func (s *MemberService) CreateMember(ctx context.Context, req CreateMemberRequest) (*Member, error) {
// валидация, хеш, вставка строки, welcome‑письмо, событие в Kafka
}

Через полгода у MemberService сорок методов, пул базы, почтовый клиент, продюсер Kafka, клиент S3 и конфиг. Любой тест требует поднять все зависимости, даже когда проверяешь одну ветку валидации. Это уже не слой, а ящик для хлама. И дробить его на MemberCreationService и MemberUpdateService бесполезно, получите ящики поменьше.

Лечит инверсия зависимостей

Интерфейсы в Go удовлетворяются неявно, и это недоиспользуют. Вместо *pgxpool.Pool опишите то, что сервису реально нужно:

type MemberRepository interface {
CreateMember(ctx context.Context, params CreateMemberParams) (*Member, error)
GetMemberByID(ctx context.Context, id uuid.UUID) (*Member, error)
}

type EventPublisher interface {
Publish(ctx context.Context, event DomainEvent) error
}

type MemberService struct {
repo MemberRepository
publisher EventPublisher
}

Теперь сервис ничего не знает про PostgreSQL и Kafka. Он зависит от поведения, а не от реализации. Чтобы протестировать метод, достаточно мока под интерфейс, без базы, брокера и сети. Про pgx знает реализация репозитория, про franz-go знает реализация publisher, а сервис не знает ни про то, ни про другое. В этом и есть контракт.
Просмотры: 143
Перейти в пост Открыть в MAX Сообщить о неточности
👨‍💻 Контекст неизменяемый, а память нет

Правило знают все. context.WithValue ничего не меняет, а создаёт новую обёртку, поэтому контекст безопасно шарить между горутинами. Учебники забывают сказать главное.

Иммутабельны обёртки, а объект в корне цепочки может переиспользоваться фреймворком через sync.Pool. Тогда горутина, пережившая запрос, держит указатель на память, которая вот‑вот достанется другому запросу.

Хендлер запускает фоновую задачу и сразу возвращает ответ. Линтер ругается на отмену контекста, и со времён Go 1.21 его успокаивают одной строкой:

go s.processAsync(context.WithoutCancel(ctx), id)

Для net/http это правильно. Там контекст это обычная аллокация в куче. Но линтер слеп к архитектуре и не знает, что под context.Context может стоять пулящаяся структура.

❓ Где можно споткнуться

В Fiber и всём на fasthttp c.Context() возвращает *fasthttp.RequestCtx. Пулящийся объект и есть ваш контекст, и после отправки ответа fasthttp сбрасывает его и кладёт обратно в пул. Gin пулит *gin.Context, поэтому передавать его в горутину нельзя (для снимка есть c.Copy()), а c.Request.Context() безопасен. Echo свой контекст пулит, но context.Context он не реализует, так что задеть гонку сложнее.

Дальше воркер просыпается после I/O, зовёт ctx.Value("user_id") и читает память, которую перезаписывают данными другого пользователя. И вот что коварно. WithoutCancel снимает только отмену, а Value() по‑прежнему делегирует пулящемуся родителю. То есть фикс, который позеленил линтер, ровно и удерживает протухший указатель.

➡️ Как правильно

Если задаче ничего не нужно из запроса, постройте свежий корень:

ctx := context.Background()

Если нужны trace‑id или user‑id, вытащите их как простые значения, пока горутина запроса ещё жива, и положите в свежий контекст:

userID := c.Locals("user_id").(string)
traceID := c.Locals("trace_id").(string)
go func(userID, traceID string) {
ctx := context.WithValue(context.Background(), userCtxKey, userID)
ctx = context.WithValue(ctx, traceCtxKey, traceID)
h.svc.ProcessAsync(ctx, userID)
}(userID, traceID)

Вся безопасность держится на том, что значения сняты до возврата из хендлера. Логику стоит вынести в хелпер вроде ctxutil.Detach(ctx). Из штатных лазеек помогают c.Copy() у Gin и c.UserContext() у Fiber.

Линтер видит синтаксис, но не жизненный цикл памяти. Если горутина переживает запрос, не давайте ей контекст запроса ни в какой обёртке. Снимайте значения как простые данные, стройте свежий корень и держите -race в CI.
Просмотры: 145
Перейти в пост Открыть в MAX Сообщить о неточности
💡 Предложение добавить slog.TestHandler в стандартную библиотеку Go

В трекере Go появилось предложение
добавить в пакет log/slog готовый хендлер, который пишет логи в testing.TB. Тогда логи приложения и логи теста окажутся в одном выводе и привяжутся к нужному тесту или подтесту.

❓ Зачем это

Сопоставлять логи приложения с тестом, который их породил, приходится постоянно. Кирпичик для этого уже есть. Метод testing.TB.Output() добавили в Go 1.25 для T, B и F, а в Go 1.26 расширили на сам интерфейс TB. Но готового хендлера в slog нет, поэтому каждый проект собирает его заново.

Обычно так:

logger := slog.New(slog.NewTextHandler(t.Output(), nil))

Этот вариант работает, но у него два минуса. Удобства тест ориентированного хендлера приходится дописывать руками, а наивная форма имеет реальную гонку при логировании после завершения теста.

➡️ Что предлагают сделать

В log/slog появится тип TestHandler, реализующий интерфейс Handler, и конструктор к нему. Чтобы не тянуть testing внутрь slog и не словить циклический импорт, вводят узкий интерфейс TestLogger. Ему удовлетворяют *testing.T, *testing.B и *testing.F.

package slog

// TestLogger — подмножество testing.TB, нужное хендлеру.
type TestLogger interface {
Output() io.Writer
}

// NewTestHandler возвращает Handler, который пишет записи в tb.Output().
// Вывод форматируется как у NewTextHandler. opts может быть nil.
func NewTestHandler(tb TestLogger, opts *HandlerOptions) *TestHandler

type TestHandler struct {
tb TestLogger // куда писать
// ...
}

func (h *TestHandler) Enabled(ctx context.Context, level Level) bool { ... }
func (h *TestHandler) Handle(ctx context.Context, record Record) error { ... }
func (h *TestHandler) WithAttrs(attrs []Attr) Handler { ... }
func (h *TestHandler) WithGroup(name string) Handler { ... }

На стороне теста всё сводится к одной строке:

func TestThing(t *testing.T) {
logger := slog.New(slog.NewTestHandler(t, nil))

// Логи лягут под TestThing с отступом от раннера.
// Для прошедших тестов вывод скрыт, пока не передан флаг -v,
// как у t.Log() и t.Logf().
doWork(logger)
}

Пока это обсуждение, а не принятая фича. Следить за судьбой можно в самом issue #80138.
Просмотры: 146
Перейти в пост Открыть в MAX Сообщить о неточности
⚡️ Быстрый бэкап с шифрованием и дедупликацией из коробки

restic — это открытая программа бэкапа на Go, которая работает на Linux, macOS, Windows, а также FreeBSD и OpenBSD. На GitHub у проекта около 34 тысяч звёзд.

Что делает

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

➡️ Принципы, на которых построен

Просто
. Бэкап и восстановление не должны быть сложными, иначе их пропускают.

Быстро. Скорость упирается только в сеть или диск, при восстановлении переносятся лишь те данные, что нужны для нужных файлов.

Проверяемо. Восстановление важнее самого бэкапа, поэтому restic даёт легко убедиться, что данные реально восстановимы.

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

Эффективно. Рост данных не означает рост хранилища в разы за счёт дедупликации.

➡️ Как пользоваться

Сначала создаёте репозиторий под бэкапы. Пароль обязателен, без него данные не восстановить:

restic init --repo /tmp/backup

Затем кладёте данные. restic сам сканирует и показывает прогресс со скоростью и ETA:

restic --repo /tmp/backup backup ~/work

Восстановить можно командой restic restore, либо смонтировать репозиторий через fuse командой restic mount и ходить по файлам прошлых снапшотов как по обычной папке.

➡️ Куда складывать бэкапы

Хранить копию на той же машине это не стратегия. restic нативно поддерживает локальную папку, sftp по SSH, собственный REST сервер, Amazon S3 и совместимый Minio, OpenStack Swift, Backblaze B2, Azure Blob Storage, Google Cloud Storage. Через rclone подключается ещё множество сервисов.

Приятная деталь

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

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

➡️ Репозиторий
Просмотры: 154
Перейти в пост Открыть в MAX Сообщить о неточности
🤨 Не прокидывайте context в чистые функции. Он стоит дороже, чем кажется

Проброс контекста штука верная и важная. Но совет «всегда прокидывай context» превращается в карго культ, когда ctx добавляют в сигнатуру каждой функции, включая чистые функции без всякого ввода вывода, просто потому что так сказал линтер или комментарий в PR:

// Этой функции context не нужен
func calculateInstallments(principal float64, rate float64, months int) []Installment {
// чистая математика, нет I/O, нет горутин
}

У контекста есть цена. Это аргумент, который приходится тянуть повсюду, он зашумляет сигнатуры и провоцирует пихать зависимости через context.WithValue, чего делать не стоит.

➡️ Где он нужен

Прокидывайте контекст туда, где он реально используется. Это вызовы базы, HTTP клиенты, gRPC стабы, всё, что блокируется на I/O или нуждается в отмене. Не прокидывайте его в чистые функции, доменную логику и всё, что возвращается за микросекунды независимо от отмены.

Контекст нужен там, где есть что отменять или ждать. В остальных местах он только зашумляет код и создаёт соблазн использовать его не по назначению. Доверяйте языку и добавляйте структуру там, где она окупается.
Просмотры: 192
Перейти в пост Открыть в MAX Сообщить о неточности
🚀 Java догнала Go, а на больших запросах обогнала

В 2020 Марк Нельсон проверял, может ли Java-микросервис держать скорость как у Go. Тогда для маленького сервиса вышла ничья. В июне 2026 замер повторился на свежих рантаймах, и расклад поменялся.

Оба сервиса гоняли по очереди на одной машине, без конкуренции за CPU. Go на стандартном net/http, без фреймворка. Java на Helidon SE с виртуальными потоками, в двух вариантах. Обычный JVM от Oracle JDK 26 и тот же JDK с AOT-кэшем Leyden.

Сервис делал немного работы на запрос. Регистр, разворот строки, CRC32 и JSON в ответ.

❓ Что показали цифры

На одном воркере и 7 байтах все шли рядом. Go около 3 200 запросов в секунду, JVM около 2 722, Leyden около 3 561. Классическая картинка из старых споров, где Go стартует резво.

А потом подняли конкуренцию до 192 воркеров, и кривая разъехалась. На 7 байтах Go выдал около 59 тысяч запросов в секунду, JVM около 74 тысяч, Leyden около 99 тысяч.

На 2 КБ пик Go около 17 тысяч против 40 с лишним тысяч у обоих вариантов Java. На 8 КБ Go около 6,8 тысячи, Java около 15 тысяч. Отказов не было нигде. В финальной матрице Go не выиграл ни одной ячейки.

❓Какие выводы

Старый тезис «Java тяжёлая, для мелких сервисов берите Go» этот прогон не подтверждает. Go по-прежнему хорош, компактный код и поставка одним бинарником никуда не делись. Но виртуальные потоки и зрелый JVM делают блокирующий код Java дешёвым, а Helidon SE держит сервис маленьким, так что сравнение не «Go против монстра-фреймворка».

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

➡️ Код и сырые результаты
Просмотры: 187
Перейти в пост Открыть в MAX Сообщить о неточности
🔍 Почему у горутин нет ID

Разработчики, пришедшие из Java или Python удивляются: горутина запущена, но получить её идентификатор нельзя. Нет метода, нет структуры, нет ничего, что можно было бы сохранить и использовать потом. В Go это сделано намеренно.

❓ Как это устроено

Горутина — анонимный воркер. Она не имеет имени, не возвращает дескриптор при запуске, не регистрируется нигде в рантайме с точки зрения программиста.

Это не техническое ограничение. Рантайм Go знает о каждой горутине всё что нужно — стек, статус, планировщик. Но эта информация намеренно скрыта от прикладного кода.

Причина в том, что именованные горутины провоцируют плохой дизайн. Когда у потока есть ID, программисты начинают привязывать к нему состояние — «этот поток обрабатывает этот запрос, значит туда и кладём контекст». Именно так работает thread-local storage, и именно это создаёт проблемы в конкурентных системах.

Пример из реальной жизни: если бы net/http хранил состояние запроса в горутине по ID, сервер не мог бы распараллелить обработку одного запроса между несколькими горутинами. Вся архитектура стала бы жёстче.

➡️ Подводные камни

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

Похожая история возникает с goroutine-local storage — его периодически пытаются реализовать через runtime.Stack и парсинг вывода. Это работает, но Go-команда считает такой подход антипаттерном: он хрупкий, медленный и ломается при изменении формата стека.

➡️ Пример кода

Правильный способ передать состояние между горутинами — канал или context.Context:

func worker(ctx context.Context, jobs <-chan int, results chan<- int) {
for {
select {
case j, ok := <-jobs:
if !ok {
return
}
results <- process(ctx, j)
case <-ctx.Done():
return
}
}
}

Горутина не знает своего ID. Но она знает, что делать, через аргументы и каналы. Этого достаточно.

Отсутствие goroutine ID — осознанное решение, которое держит архитектуру чистой. Вместо того чтобы привязывать состояние к конкретной горутине, Go предлагает передавать его явно — через каналы и контекст. Это делает код проще для тестирования и масштабирования.

У нашей рассылки есть ID. Вот он — https://clc.to/mKZg2A
Просмотры: 186
Перейти в пост Открыть в MAX Сообщить о неточности
Вся лента — после бесплатной регистрации
ВКонтакте Телеграм Одноклассники
Избранное в datool.ru

Следите за каналом в личном кабинете

Зарегистрируйтесь в datool.ru, чтобы добавить канал «Библиотека Go разработчика | Golang» в избранное и отслеживать его показатели.

Зарегистрироваться
Для владельцев каналов

Подтвердите свой канал

Войдите или зарегистрируйтесь в datool.ru, затем добавьте нашего бота в канал по инструкции в личном кабинете. После проверки владения замок исчезнет и вам станет доступно управление каналом.

Войти или зарегистрироваться

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти