Библиотека 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
👨‍💻 Контекст неизменяемый, а память нет

Правило знают все. 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.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
6👍1🥱1
🎮 Субботний оффтоп

Кто-то собрал веб-версию Half-Life 2 и запустил её без установки и скачивания. Уровни и ресурсы грузятся быстро, игра работает бодро. У многих сразу включается русский язык — Сити-17 и Рейвенхольм уже вовсю тестируют первые игроки.

➡️ Поиграть

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

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

#GoLive
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10
📎 Главная проблема в микросервисах

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

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

Папки вместо архитектуры
Спросите десять команд про структуру сервиса, и почти все опишут что‑то такое.
/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, а сервис не знает ни про то, ни про другое. В этом и есть контракт.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
13🌚1
🤩 Фаззинг находит то, что вы не догадались проверить

У обычных тестов есть слепое пятно. Они проверяют случаи, которые вы придумали, а баги живут в тех, что не придумали. Покрытие в 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, свойство пишется в считаные строки. Цена это пара минут на разработке. Альтернатива это ждать, пока вход за вас подберёт прод. Боевыми данными, под нагрузкой, в три часа ночи, в виде алерта. Прод пофаззит ваш код в любом случае, вопрос лишь в том, узнаете вы о баге от теста или от дежурного.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍5
🧑‍💻 Сравнение двух Go-структур без хардкода полей. Часть 1, проблема

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

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

Он отдаёт маленькую структуру на три поля:
type DriverDriver struct {
Name string `json:"name"`
Number string `json:"number"`
ProfilePic *string `json:"profile_pic"`
}


А ваша модель базы выглядит куда менее мило, в ней около восьмидесяти колонок:
type User struct {
ID int64 `gorm:"column:id"`
Name string `gorm:"column:name"`
Email string `gorm:"column:email"`
Number string `gorm:"column:number"`
ProfilePic *string `gorm:"column:profile_pic"`
// ... и ещё около восьмидесяти колонок
}


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

Первый вариант, который пишут все, выглядит так:
updates := map[string]any{}
if driver.Name != user.Name {
updates["name"] = driver.Name
}
if driver.Number != user.Number {
updates["number"] = user.Number // упс, опечатка, скопировали не ту сторону
}
if !ptrEqual(driver.ProfilePic, user.ProfilePic) {
updates["profile_pic"] = driver.ProfilePic
}


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

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

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10🔥21
🤝 Уйти грамотно — это тоже навык
 
В IT репутация передается быстрее, чем резюме. Бывший тимлид может ответить на звонок рекрутера через год, бывший коллега — написать в личку вашему потенциальному работодателю.

Один неаккуратный уход способен закрыть двери туда, где вы еще даже не пытались открыть.

➡️ Как уйти красиво

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

🐸 Библиотека Go-разработчика
Please open Telegram to view this post
VIEW IN TELEGRAM
😁41
🔍 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 символов это несущественно.

➡️ Решить

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

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

#ReadySetGo
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱43
📎 Сравнение двух Go-структур без хардкода полей. Часть 2, идея с тегами

В первой части мы упёрлись в стену из почти одинаковых блоков if, которая растёт с каждым новым полем. Теперь зададим другой вопрос. Что если логика сравнения вообще не будет упоминать ни одного имени поля?

У полей Go-структуры можно хранить метаданные через теги, те самые строки в обратных кавычках. GORM по тегу gorm:"column:name" знает, в какую колонку базы ложится поле. JSON по тегу json:"name" знает имя в JSON. Это просто строки, которые рантайм умеет читать обратно через рефлексию.

Обе стороны сравнения уже размечены. Модель базы знает свои колонки. Нужно лишь, чтобы входящий DTO объявил, какой колонке соответствует каждое его поле. Поэтому добавляем ещё один тег:
type DriverDriver struct {
Name string `json:"name" match:"name"`
Number string `json:"number" match:"number"`
ProfilePic *string `json:"profile_pic" match:"profile_pic"`
}


Тег match:"name" это весь контракт. Он говорит, что это поле надо сравнивать с колонкой базы по имени name. Правило маппера получается таким. Для каждого поля DTO с тегом match найди поле базы, у которого колонка gorm равна этому тегу, сравни их, и если различаются, запиши новое значение под именем колонки.

Ни одно имя поля не зашито в логику. Определения структур сами становятся конфигурацией. Добавили поле и тег, оно автоматически попало в сравнение. Удалили, оно выпало. Сам маппер при этом не меняется. Вызов выглядит как одна строка на модель:
driverUpdates, err := mapper.CompareAndMap(driver.DriverDriver, dbUser)
metaUpdates, err := mapper.CompareAndMap(driver.DriverMeta, dbUser)
vehicleUpdates, _ := mapper.CompareAndMap(driver.Vehicle, dbVehicle)


Каждый вызов возвращает map[string]any только из изменившихся колонок, и это ровно та форма, которую ждёт Updates() у GORM. Обратите внимание, второй и третий вызовы сравнивают совсем разные типы структур с разными таблицами одной и той же функцией.

В следующей части разберём, как этот маппер устроен внутри.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
🌚1
🚀 Не уверены, стоит ли переходить на зрелую ИИ-инженерию? Начните с демо-урока!

Вот-вот стартует наш курс AgentOps. Если вы сомневаетесь в формате, просто оставьте заявку и получите бесплатный демо-урок «AI-инструменты в разработке: как писать код быстрее с помощью ассистентов».

Для тех, кто готов мощно прокачать портфолио, прямо сейчас действует предложение «3 любых курса по цене 1»:
— При покупке VIP-тарифа (осталось 4 места) нового потока «ИИ-агенты» вы получаете в подарок доступ к курсу «AgentOps» + ещё один любой курс Академии на выбор

— В деньгах это два топовых курса по автоматизации и контролю ИИ всего за 134.000 ₽ вместо 263.000 ₽ 🔥 А за счет третьего курса (например, можно выбрать «Математику») вы соберете мощный стек и освоите целое востребованное направление.

— Платеж можно разбить на несколько частей с помощью беспроцентной рассрочки.


👉 Получить демо-урок и зафиксировать спецпредложение 3 в 1
👍1
📎 Разбираемся, как os.ReadFile() работает с памятью

Во многих туториалах по Go встречается фраза «os.ReadFile() читает весь файл в память». Мы привыкли принимать такие утверждения на веру, хотя проверить их легко.

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

➡️ Эксперимент

Берём простую программу. Она читает файл, путь к которому передан аргументом, выводит его размер и затем ждёт нажатия Enter, чтобы у нас было время посмотреть на потребление памяти процесса:
package main

import (
"fmt"
"os"
)

func main() {
if len(os.Args) != 2 {
fmt.Println("Please specify a path")
return
}

b, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
fmt.Println("File has been read into memory.")
fmt.Println("Size:", len(b), "bytes")

fmt.Println("Press Enter to exit...")
fmt.Scanln()
}


Сначала создаём файл на 500 мегабайт:
fallocate -l 500M bigfile.txt


Запускаем программу через go run:
go run readingFile_bad.go bigfile.txt


Пока программа стоит на паузе, смотрим на её RSS, то есть на объём физической памяти, реально занятой процессом:
ps -o pid,rss,vsz,cmd -p 228159
PID RSS VSZ CMD
228159 3116 1750908 /tmp/go-build2199976311/b001/exe/readingFile_bad bigfile.txt


RSS показывает примерно 3 мегабайта, хотя файл весит 500. Логично ожидать, что вся эта память должна быть занята. Разберёмся, почему этого не происходит.

Почему память не занята

Когда os.ReadFile() вызывается, Go выделяет на куче backing array размером с файл, в нашем случае 500 мегабайт, и операционная система копирует туда содержимое файла с диска.

Переменная b []byte, которую мы получаем, это не сами данные. Это всего лишь заголовок слайса, маленькая структура размером около 24 байт на 64-битной системе, которая хранит три поля: указатель на первый байт backing array, текущую длину слайса, и полную ёмкость backing array.

Сам слайс маленький, а массив с данными файла занимает реальные 500 мегабайт где-то в куче:
b │
├── pointer ───────────────┐
├── length = 524288000 |
└── capacity = 524288000 │

+-----------------------+
| 500 MB backing array |
+-----------------------+


Дело в том, что в нашей программе после fmt.Scanln() переменная b больше нигде не используется. Компилятор и сборщик мусора видят, что b мертва после этой точки, поэтому backing array становится недоступным для использования и сборщик мусора может его освободить ещё до того, как мы посмотрели на RSS.

Чтобы проверить эту гипотезу, раскомментируем строку с обращением к b[0] в конце программы:
fmt.Println("FirstByte: ", b[0])


Теперь компилятор знает, что переменная понадобится после Scanln(), и не может позволить сборщику мусора забрать её раньше времени. Запускаем программу заново и смотрим на RSS:
ps -o pid,rss,vsz,cmd -p 233685
PID RSS VSZ CMD
233685 514680 1750844 /tmp/go-build1663734078/b001/exe/readingFile_bad bigfile.txt


На этот раз RSS равен 514680 килобайт, то есть около 514 мегабайт. Это уже соответствует ожиданиям.

Стоит добавить ещё один нюанс. Команда go run сама по себе не запускает наш код напрямую, она сначала компилирует временный исполняемый файл во временную директорию и уже его запускает отдельным процессом. Поэтому при анализе памяти нужно смотреть не на процесс go run, а на дочерний процесс из /tmp/go-build.../exe/, именно он реально читает файл.

os.ReadFile() действительно выделяет память под весь файл целиком, утверждение из туториалов верное. Но []byte, который мы получаем, это лёгкий заголовок слайса, а не сами данные, и поведение сборщика мусора влияет на то, когда именно эта память физически занята процессом. Если переменная с данными становится недостижимой раньше, чем вы успели посмотреть на RSS, может показаться, что файл вообще не загружался в память, хотя на деле это просто сборка мусора отработала раньше, чем мы ожидали.

Полезный практический вывод из этого расследования простой. Если вам нужно работать с большими файлами, лучше избегать os.ReadFile() и использовать потоковое чтение через bufio.Reader или io.Copy, чтобы не держать весь файл в памяти разом.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍124🔥1
⚠️ Уже завтра стартует курс AgentOps!

Мы собрали на потоке сборную из мастеров IT-рынка. Практики из BigTech научат вас контролировать и отлаживать ИИ-агентов, чтобы они работали предсказуемо и не сливали бюджет на API.

🔥 Заберите 3 курса по цене 1:
● При покупке VIP-тарифа (осталось 4 места) нового потока «Разработка ИИ-агентов» получаете в подарок курс «AgentOps» + ещё один любой курс Академии (например, «Математика для разработки AI», чтобы глубже освоить направление).

● Три курса обойдутся вам всего в 134.000 ₽ вместо 263.000 ₽.

● Доступна удобная беспроцентная рассрочка, платеж можно разбить на несколько комфортных частей.


Хотите прокачать свое портфолио продакшн-кейсом, но пока сомневаетесь? Пройдите наш бесплатный демо-урок, чтобы протестировать формат перед покупкой.

👉 Забрать 3 курса по цене 1 и получить демо-урок
1
🤔 Стоит ли Go развиваться быстрее

В сабреддите r/golang разгорелось обсуждение, которое касается почти каждого, кто пишет на Go. Автор треда обратил внимание, что в 1.26 основная работа снова шла не в синтаксис, а в рантайм, тулчейн, планировщик и сборщик мусора.

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

➡️ Стабильность как главный аргумент

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

➡️ Маленький язык как осознанный выбор

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

➡️ Где сообщество видит реальный потенциал для роста

Почти никто в треде не просит радикальных изменений синтаксиса, но запрос на улучшения всё же есть. Чаще всего звучат три темы:

• Производительность и сборщик мусора, который многие сравнивают не в пользу Go с JVM.

• Полноценные enum, которые называют одной из немногих фич, добавление которых не выглядит избыточным.

• Доработка инструментов, в частности encoding/json/v2, который уже называют заметным шагом вперёд.

💬 Что вы думаете? Стоит ускорять разработку го и было ли многолетнее сдерживание силой языка или тормозом для его развития?

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

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

#GoTalk
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
🧑‍💻 Сравнение двух Go-структур без хардкода полей. Часть 3, как это устроено внутри

В прошлой части мы договорились про тег match. Теперь посмотрим на реализацию. Она занимает около 130 строк и состоит из обычной рефлексии.

Сначала разворачиваем указатели и проверяем, что перед нами действительно структуры.

Функция принимает any с обеих сторон, поэтому работает с любым типом:
func (m *Mapper) CompareAndMap(dto any, db any) (map[string]any, error) {
dtoVal := reflect.ValueOf(dto)
dbVal := reflect.ValueOf(db)
if dtoVal.Kind() == reflect.Ptr {
dtoVal = dtoVal.Elem()
}
if dbVal.Kind() == reflect.Ptr {
dbVal = dbVal.Elem()
}
if dtoVal.Kind() != reflect.Struct || dbVal.Kind() != reflect.Struct {
return nil, fmt.Errorf("both dto and db must be structs or pointers to structs")
}
// ...
}


Дальше один раз проходим по модели базы и строим карту вида имя колонки в значение поля. Так матчинг работает за O(1) на поле, а порядок полей в структурах может не совпадать:
func (m *Mapper) buildColumnMap(dbVal reflect.Value) map[string]reflect.Value {
columnMap := make(map[string]reflect.Value)
dbType := dbVal.Type()
for i := 0; i < dbType.NumField(); i++ {
if column := m.parseGormColumn(dbType.Field(i).Tag.Get(dbGormTag)); column != "" {
columnMap[column] = dbVal.Field(i)
}
}
return columnMap
}


Имя колонки достаётся из тега GORM, который бывает грязным, например type:smallint;column:is_suspended. Режем по точке с запятой и берём кусок после column:
func (m *Mapper) parseGormColumn(gormTag string) string {
for _, part := range strings.Split(gormTag, ";") {
if col, ok := strings.CutPrefix(part, dbColumnKey+":"); ok {
return col
}
}
return ""
}


Потом идём по полям DTO. Поле без тега match пропускаем, так DTO может иметь поля, которые мы намеренно не синхронизируем.

Поле, чей тег указывает на несуществующую колонку, тоже пропускаем без паники:
for i := 0; i < dtoVal.NumField(); i++ {
dtoField := dtoVal.Field(i)
dtoFieldType := dtoType.Field(i)
dbGormColumnName := dtoFieldType.Tag.Get(dtoMatchTag)
if dbGormColumnName == "" {
continue // нет тега match, это не наша забота
}
dbField, ok := columnMap[dbGormColumnName]
if !ok {
continue // DTO ссылается на колонку, которой нет в модели базы
}
// ... сравниваем dtoField и dbField
}


Каркас готов. Осталось самое скользкое, сравнение значений с указателями и nil. Об этом в следующей части.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21
😋 Топ-вакансий для Go-разработчиков за неделю

Go Developer — удаленно по Москве

Middle Golang разработчик — от 320 000 ₽, удаленно

Senior Golang Developer — до 500 000 ₽, гибрид в Москве или удаленно

➡️ Еще больше топовых вакансий — в нашем канале Go jobs

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

#GoWork
Please open Telegram to view this post
VIEW IN TELEGRAM
👨‍💻 Сравнение двух Go-структур без хардкода полей. Часть 4, указатели, nil и выводы

В прошлой части мы дошли до момента, где надо сравнить два поля. На этом месте чаще всего ломаются наивные реализации. Реальные модели базы полны *string, *int64, *time.Time. Указатель может быть nil, то есть «значения нет».

Сравнение указателя с не-указателем, или двух указателей, где один nil, требует аккуратности:
dtoIsPtr := dtoField.Kind() == reflect.Ptr
dbIsPtr := dbField.Kind() == reflect.Ptr

if dtoIsPtr && dbIsPtr {
if dtoField.IsNil() && dbField.IsNil() {
continue // оба nil, ничего не изменилось
}
if dtoField.IsNil() != dbField.IsNil() {
// одна сторона nil, другая нет, это точно изменение
if dtoField.IsNil() {
updateMap[dbGormColumnName] = nil
} else {
updateMap[dbGormColumnName] = dtoField.Elem().Interface()
}
continue
}
// оба не nil, разворачиваем и сравниваем значения
dtoField = dtoField.Elem()
dbField = dbField.Elem()
} else if dtoIsPtr || dbIsPtr {
// смешанный случай, одна сторона указатель, другая обычное значение
if dtoIsPtr {
if dtoField.IsNil() {
updateMap[dbGormColumnName] = nil
continue
}
dtoField = dtoField.Elem()
}
if dbIsPtr {
if dbField.IsNil() {
updateMap[dbGormColumnName] = dtoField.Interface()
continue
}
dbField = dbField.Elem()
}
}


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

Сравнение значений

Когда обе стороны развёрнуты до обычных значений, остаётся сравнить. Ожидаешь однострочник a.Interface() == b.Interface(), но есть ловушка. Одно и то же логическое число может храниться разными типами. В DTO поле CityID int, а в модели базы CityID int64. Для оператора == в Go это разные типы, и сравнение их упакованных форм вернёт false.

Поле будет помечаться как изменённое при каждой синхронизации. Поэтому compareValues приводит числовые семейства к общему виду:
func (m *Mapper) compareValues(a, b reflect.Value) bool {
aKind := a.Kind()
bKind := b.Kind()

switch {
case m.isSignedInt(aKind) && m.isSignedInt(bKind):
return a.Int() == b.Int()
case m.isUnsignedInt(aKind) && m.isUnsignedInt(bKind):
return a.Uint() == b.Uint()
case m.isSignedInt(aKind) && m.isUnsignedInt(bKind):
return a.Int() >= 0 && uint64(a.Int()) == b.Uint()
case m.isUnsignedInt(aKind) && m.isSignedInt(bKind):
return b.Int() >= 0 && a.Uint() == uint64(b.Int())
case m.isFloat(aKind) && m.isFloat(bKind):
return a.Float() == b.Float()
default:
return a.Interface() == b.Interface()
}
}


Методы .Int(), .Uint() и .Float() расширяют любую разрядность до самой большой формы, так что int8, int16, int, int64 сворачиваются в одно сравнение. Случаи знаковое против беззнакового защищены проверкой >= 0, чтобы отрицательное число не превратилось в гигантское беззнаковое. Всё, что не число, падает в обычный ==.

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

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

Получилось около 130 строк рефлексии вместо растущей кучи хрупких блоков if. Код работает на всех моделях и становится проще в поддержке по мере роста системы. Если ловите себя на том, что пишете одну и ту же логику по полям в N-й раз, проверьте, не лежат ли метаданные для обобщения прямо в ваших тегах. Чаще, чем кажется, они там уже есть.

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
ОПРОС: Ответьте на пару вопросов и получите промокод на все наши курсы! 🎁

Команда Prоglib.аcаdemy готовит к запуску новые продукты, и нам нужна ваша помощь. Мы хотим создавать обучение, которое будет решать ваши реальные карьерные задачи и бить точно в цель.

Поделитесь своим опытом и ожиданиями, чтобы мы сделали наши курсы еще полезнее именно для вас! Заполнение анкеты займет буквально 2–3 минуты, а с нас — скидка на любой наш курс!

👉 Пройти опрос в Яндекс Формах и забрать промокод
🖥 Эмулятор ZX Spectrum на Go

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

Либо приходится гонять эмулятор руками и проверять результат на глаз, либо городить виртуальный X-сервер только ради одного теста.

zenzx — эмулятор ZX Spectrum на Go. Помимо обычного графического режима он умеет работать совсем без окна, просто прогонять нужное число кадров и сохранять результат в виде PNG.

➡️Что умеет эмулятор

ZenZX поддерживает модели 48K, 128K, +2 и +3, включая контроллер дисковода у модели +3, а также загрузку с ленты и снапшоты. Собирается он в двух вариантах.

GUI-сборка по умолчанию рендерит через raylib, с поддержкой звука, и требует CGO вместе с системными библиотеками OpenGL, X11 или Wayland, и ALSA. Headless-сборка собирается с тегом headless, не требует CGO, не открывает окно и не трогает звуковую карту. Она загружает модель, прогоняет заданное число кадров и сохраняет PNG-снимки экрана, декодированные прямо из видеопамяти.

➡️ Сборка и запуск

Для обычной сборки нужны Go 1.25 или новее, а для GUI-варианта ещё и C-тулчейн с raylib и oto:
./build.sh            # обычный GUI-бинарник, нужны CGO и системные библиотеки
./build_headless.sh # headless-бинарник без CGO


Запуск GUI выглядит так:
./zenzx -model=48k
./zenzx -model=128k
./zenzx -model=plus3
./zenzx -tape=game.tap -tapemode=accurate


ZenZX интересен не столько как ещё один эмулятор Спектрума, а как пример того, как отделить эмуляцию от рендеринга и звука так, чтобы можно было гонять её в CI или в скрипте без единого окна.

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

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

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

#GoToProduction
Please open Telegram to view this post
VIEW IN TELEGRAM
👍32
👨‍💻 Лишние проверки на nil в Go. Когда защита от паники превращается в проблему

В коде на Go всё чаще встречается одна и та же картина, почти перед каждым обращением к указателю стоит if x != nil. Выглядит как разумная подстраховка, но часто это сигнал, что в системе уже не понятно, какие значения действительно могут быть nil, а какие нет, и проверяется всё подряд на всякий случай.

➡️ Проверка на зависимость

Структура RateLimiter хранит клиент Redis:
func (r *RateLimiter) Allow(ctx context.Context, req *Request) (bool, error) {
userID := GetUserID(req)
if r.redis != nil {
return r.checkLimit(ctx, userID)
}
return false, nil
}


Проверка r.redis != nil ничего не лечит. Если клиент равен nil, ошибка произошла раньше, при инициализации, и проверка здесь просто позволяет коду работать дальше в уже сломанном состоянии.

В Go лучше падать сразу и громко, чем тащить невалидное состояние по системе.

Перенос проверки в конструктор NewRateLimiter выглядит лучше, но тоже не решает задачу до конца, потому что nil всё равно успевает дойти до конструктора. Правильное место для обработки ошибки это сама точка создания клиента:
redisClient, err := NewRedisClient(addr)
if err != nil {
return nil, err
}
limiter := NewRateLimiter(redisClient)


Если клиент создан успешно, дальше по системе он уже не может быть nil, и проверки внутри RateLimiter просто не нужны. Если же системе важно переживать временную недоступность Redis, это стоит явно смоделировать отдельным типом, который сам внутри себя справляется с повторами, а наружу всегда отдаёт нечто гарантированно рабочее.

➡️ Громкий отказ

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

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

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

➡️ Где проводить границу

Полезно думать о коде как о внешнем и внутреннем слое. Внешний слой это там, где данные входят в программу, например HTTP-обработчик. Внутренний слой это код, до которого эти данные доходят дальше по стеку вызовов:
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
req, err := DecodeRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
allowed, err := h.limiter.Allow(r.Context(), req)
// ...
}


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

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

➡️ Оригинал

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

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

#GoDeep
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
🔄 Вышел gokrazy/rsync v0.3.4

gokrazy/rsync это реализация протокола rsync на чистом Go, и клиент, и сервер, с поддержкой передачи файлов в обе стороны.

В версии v0.3.4 решены три проблемы

➡️ Политику landlock расширили, добавив в неё /etc, а на gokrazy ещё и /tmp, потому что без доступа к этим путям через какое-то время ломалось разрешение DNS на системах с запущенным Tailscale.

➡️ Отдельно поправили совместимость с новыми версиями форка openrsync от Apple, и закрыли баг, из-за которого передача файла иногда падала с ошибкой file has changed mid-transfer.

➡️ Из менее заметного, но полезного, в релиз попал вклад нового контрибьютора, добавляющий клиенту возможность задать собственный net.DialContext. Это позволяет управлять тем, как именно устанавливается сетевое соединение, например проксировать его или привязывать к конкретному интерфейсу, не трогая остальной код клиента.

Если вы используете gokrazy/rsync вместе с Tailscale или синхронизируете большие файлы, которые могут меняться во время передачи, обновление до v0.3.4 стоит того, чтобы не упираться в уже известные и закрытые баги.

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

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

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

#GoLive
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6😢1