DNK_C_C++_Go_Rust
45 subscribers
14 photos
45 links
DNK - дневник кодера С и С++
Download Telegram
Интерфейсы следует помещать в тех местах, где они используются, чтобы обеспечить ясность требований и независимость реализации.
Тесная связь (tight coupling) возникает, когда компоненты системы сильно зависят друг от друга.
Тесная связь затрудняет поддержку, тестирование и повторное использование компонентов.
Ослабление связей достигается использованием интерфейсов и принципов проектирования, таких как Dependency Inversion Principle (Принцип Инверсии Зависимости).
#489_GO_ODP_Q14

Предположим, функция должна возвращать детализированные Recoverable и Fatal ошибки. Как это реализовано в пакете net?
Как это надо делать в современном Go? что нового и важного в плане обработки ошибок появилось начина с Go 1.13?
А чего не появилось, хоть мы и ждали?



Типичные случаи использования ошибок в пакете net.

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

Типичный пример из пакета net выглядит следующим образом:
conn, err := net.Dial("tcp", ":80")
if err != nil {
log.Fatal(err)
}


Однако важно отметить, что многие ошибки имеют внутреннюю структуру, которую можно исследовать с помощью методов проверки типов (например, errors.Is() и errors.As()):
err = someNetworkOperation()
if errors.Is(err, os.ErrExist) {
fmt.Println("File already exists.")
}


Этот подход даёт нам механизм классификации ошибок по содержимому и семантике, сохраняя при этом простоту конструкции обычного error.


Как правильно обрабатывать детализированные ошибки (Recoverable и Fatal).
Современный подход к обработке ошибок включает разделение ошибок на разные категории, такие как recoverable («исправимые») и fatal («фатальные»).


Правильная обработка этих категорий важна для устойчивости приложений.

1. Подход с помощью интерфейсов и структурированных ошибок.
Хорошей практикой является создание собственных специализированных типов ошибок, которые позволяют отличить recoverable ошибки от fatal:
type RecoverableError struct {
Err error
}

type FatalError struct {
Err error
}

func (re *RecoverableError) Error() string { return re.Err.Error() }
func (fe *FatalError) Error() string { return fe.Err.Error() }

// Пример возврата ошибки
func SomeFunction() error {
if somethingBadHappened {
return &RecoverableError{Err: errors.New("something bad happened")}
}
if reallyBadHappened {
return &FatalError{Err: errors.New("really bad thing happened")}
}
return nil
}



Затем можно проверять тип ошибки и реагировать соответствующим образом:
err := SomeFunction()
switch e := err.(type) {
case *RecoverableError:
fmt.Printf("Recovered from %v\n", e)
case *FatalError:
panic(e)
default:
fmt.Println("Unknown error:", err)
}


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


2. Использование стандартных функций Go для детальной обработки ошибок.
Начиная с Go 1.13 появились новые стандартные инструменты для улучшения диагностики и управления ошибками:
Функция errors.Unwrapпозволяет извлекать вложенные ошибки, созданные функцией fmt.Errorf("%w", ...).
Функции errors.Is и errors.As
помогают сравнивать типы ошибок и извлекать детали внутренних ошибок соответственно.

Примеры:
err := someNetOp()
var connRefused net.AddrError
if errors.As(err, &connRefused) && connRefused.Temporary() {
fmt.Println("Temporary connection refused:", connRefused.Err)
}



Новшества в Go 1.13 и далее касательно обработки ошибок.
Ключевые нововведения в Go, касающиеся обработки ошибок:
Поддержка упаковки ошибок с указанием контекста (%w)начиная с Go 1.13 появилась поддержка специальной директивы %w в функции fmt.Errorf, позволяющей сохранять исходную ошибку вместе с новой информацией. Эта ошибка доступна через метод Unwrap():
wrappedErr := fmt.Errorf("failed to open file: %w", os.ErrPermission)



Расширенная диагностика ошибок с помощью errors.Is и errors.As — эти функции позволяют точнее анализировать ошибки и определять их происхождение, особенно полезно для стандартных библиотек вроде os и net.

Утилиты для трассировки стека ошибок — появились удобные утилиты для вывода подробной информации о происхождении ошибки, включая стектрейс.
import "runtime/debug"

debug.PrintStack()

.
Разделение ошибок на категории через упаковку и проверку типовиспользуя новую функциональность fmt.Errorf и встроенные механизмы проверки типов, стало проще создавать специализированные обработчики ошибок.


Чего не произошло (ожидаемые новшества):
Несмотря на появление новых инструментов, некоторые ожидаемые расширения функциональности пока не были внедрены официально:
Автоматическое отслеживание места возникновения ошибки — многие разработчики ожидали автоматического сохранения информации о месте возникновения ошибки аналогично Python’овому traceback. Пока эта функциональность не была введена.
Более выразительные средства для сопоставления типов ошибок — хотя функции Is и As значительно улучшили диагностику, иногда хотелось бы иметь ещё больше возможностей по сравнению разных видов ошибок без ручной распаковки.
Структурированные ошибки как объект первой важности — несмотря на улучшение в диагностике и упаковке, базовые примитивы всё ещё остаются довольно простыми, без официальной поддержки высокоуровневых механизмов обработки ошибок, аналогичных модели исключений в других ЯП.


Основные рекомендации:
Используйте механизмы группировки ошибок с помощью специальных типов (типа RecoverableError и FatalError).
Применяйте стандарты, введённые
в Go 1.13, такие как %w, errors.Is и errors.As, для лучшей диагностики.
По мере развития проекта старайтесь придерживаться единообразия в подходе к работе с ошибками, обеспечивая лёгкую обработку и восстановление после проблем.
#490_GO_ODP_Q15

Главный недостаток стандартного логгера в Golang?
Каково основное применение информации из логов?



Недостатки стандартного логгера Go (log package).
Стандартный пакет log в Go обладает несколькими ключевыми недостатками:
Отсутствие структурированного вывода — стандартный логгер пишет данные в виде обычного текста, что делает извлечение и последующий анализ сложной задачей.
Нет встроенной поддержки формата JSON или другого способа представления структуры данных, удобной для машинной обработки.
Ограниченный уровень детализации — логгер поддерживает лишь простейшие уровни (panic, fatal, print), тогда как современные приложения требуют больше градации: trace, debug, info, warn, error, fatal.
Невозможность фильтрации — невозможно настроить вывод отдельных категорий сообщений, что важно для высоконагруженных систем. Сообщения пишутся сразу в поток без предварительной сортировки.
Нет ротации файлов логовесли объем логов велик, стандартная библиотека не способна автоматически вращать файлы логов, удалять старые или создавать архивы.
Недостаточная поддержка многопоточности — несмотря на поддержку потоков в Go, стандартный логгер не оптимизирован для параллельных сценариев. Синхронизация осуществляется блокировкой всей файловой записи, что снижает производительность в многопоточных приложениях.


Информация из логов применяется в следующих ключевых областях:
Диагностика и отладка — важнейшая задача логирования — фиксация деталей возникновения ошибок и сбоев.
Программист получает представление о состоянии приложения в конкретный момент времени, помогающее быстро диагностировать проблему.
Мониторинг состояния и производительности — по данным логов удобно анализировать состояние системы, проверять загрузку ресурсов, идентифицировать узкие места и планировать дальнейшее развитие инфраструктуры.
Безопасность и аудит — логи играют центральную роль в защите данных и контроле за действиями пользователей. Фиксация попыток взлома, изменение привилегий, выполнение административных команд позволяет своевременно реагировать на возможные инциденты.
Сбор статистики и аналитика — посредством логов можно собирать статистику о действиях пользователей, популярности сервисов, частоте обращений к ресурсам и многое другое, что полезно для улучшения продукта и понимания потребностей клиентов.


Стандартный логгер в Go недостаточно эффективен для большинства промышленных проектов, требующих продвинутых возможностей логирования.
Для серьезных решений лучше использовать сторонние библиотеки вроде Logrus, Zap или Kataras/iris.
#491_GO_ODP_Q16

Есть ли для Go хороший orm?
Что означает буква M в аббревиатуре ORM?
Как устроен GORM?
Что на букву M есть в GORM?
Что такое DAL и зачем он нужен?


Есть ли для Go хороший ORM?
Да, для Go существует несколько популярных ORM-фреймворков, среди которых наиболее известны следующие:
GORM — один из самых популярных и широко используемых ORM для Go. Поддерживает различные базы данных, такие как PostgreSQL, MySQL, SQLite, SQL Server и другие. Обладает высокой производительностью и гибкостью настройки.
SQLX — библиотека, расширяющая стандартный пакет database/sql, добавляет удобные методы для работы с базой данных и поддерживает автоматическое отображение полей структуры Go на столбцы таблицы БД.
ENT — современный ORM с поддержкой автоматического создания схемы и миграций, предлагает удобный API для CRUD операций и мощное построение запросов.

Каждый из этих инструментов имеет свои преимущества и подходит под разные сценарии разработки приложений.


Что означает буква M в аббревиатуре ORM?
Буква "M" в аббревиатуре ORM расшифровывается как Mapping, что переводится на русский как "отображение".
Полная расшифровка звучит как Object-Relational Mapping.
Это технология, позволяющая разработчику оперировать объектами ЯП (например, структурами в Go), которые автоматически сопоставляются с таблицами и полями реляционной БД.

Основная цель ORM — упростить работу с базами данных путем предоставления удобного API для взаимодействия с ними, минимизируя необходимость написания большого объема рутинного SQL-кода вручную.


Как устроен GORM?

GORM — представляет собой легкую библиотеку, предоставляющую мощный набор функций для работы с реляционными БД в Go.

Основные особенности и принципы устройства:
Поддержка различных СУБД — GORM совместим с популярными СУБД, как PostgreSQL, MySQL, SQLite, SQL Server и др.
Автоматическое отображение структур на таблицы — структуры Go легко преобразуются в таблицы БД. Поля структуры становятся колонками, а сама структура — таблицей.
Простота использования — предоставляет простой интерфейс для CRUD-операций (создание, чтение, обновление, удаление записей).
Миграции и авто-миграционные возможности — позволяет автоматически создавать схему базы данных на основе структур Go.
Производительность — благодаря использованию нативных драйверов баз данных, обеспечивает высокую производительность.
Расширяемость — легко интегрируется с различными middleware-компонентами, обеспечивая дополнительную функциональность, такую как ведение журнала, транзакционная поддержка и кэширование.

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

.
Что на букву M есть в GORM?

Буква "M" в терминологии ORM обозначает mapping, т.е. процесс преобразования объектов Go в записи базы данных и наоборот. В GORM этот аспект реализуется следующим образом:
Отображение структур на таблицы — каждая структура Go связывается с определенной таблицей в базе данных. Имена полей структуры соответствуют именам столбцов в таблице.
Настройка отображений — разработчик может настраивать поведение отображения через аннотации (так называемые tags) в структуре, например, задавая правила сериализации/десериализации, устанавливая связи между моделями ("один ко многим", "многие ко многим") и определяя уникальные ключи.
Запросы и манипуляции — библиотека автоматически генерирует соответствующие SQL-запросы на основе действий над объектами Go (например, сохранение объекта приведет к вставке новой строки в таблицу).

Эти механизмы позволяют писать приложения с минимальным количеством SQL-кода и сосредоточиваться непосредственно на бизнес-логике проекта.


Что такое DAL и зачем он нужен?

DAL (Data Access Layer) — слой доступа к данным.
Этот уровень программного обеспечения предназначен для изоляции уровня хранения данных от остальной части системы.
Основная задача DAL — предоставление стандартного интерфейса для взаимодействия с хранилищем данных (обычно база данных), скрывая детали реализации конкретного хранилища (тип базы данных, SQL-запросы и т.п.).

Основные цели использования DAL:

Изоляция деталей работы с БД — код основного приложения не зависит от конкретной базы данных или формата хранимых данных.
Упрощение поддержки и тестирования — возможность замены одного механизма доступа к данным другим без изменения основной бизнес-логики приложения.
Улучшение производительности и масштабируемостивозможность добавления оптимизаций (кэширование, индексы, оптимизация запросов) на уровне слоя доступа к данным без затрагивания остальных частей приложения.

Примеры слоев DAL включают библиотеки вроде ORM (например, GORM), адаптеры к различным источникам данных, специализированные сервисы обработки данных и промежуточные слои между моделью предметной области и уровнем данных.


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

Какие линтеры применят для работы с Golang?
Какое отношение линтеры имеют к CI?
Зачем нужен CI в процессе разработки?



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


Стандартные инструменты:
go vet проверяет синтаксические ошибки, подозрительные конструкции и возможные баги в коде.
Включается стандартным инструментом компилятора Go.
golint — анализирует соблюдение стиля кода согласно рекомендациям сообщества Go (имена переменных, функций и пакетов).
Не находит критичных ошибок, помогает соблюдать стандарты.
errcheck — линтер, проверяющий правильность обработки ошибок в программе.
Подчеркивает места, где игнорируются возвращаемые ошибки.
staticcheck — статический анализатор, обнаруживающий проблемные участки, ненужные проверки, неиспользуемые поля и т.д.
Помогает находить проблемы до запуска тестов.
ineffassign — отмечает неэффективные присваивания переменным, которые впоследствии нигде не используются.
megacheck — объединяет несколько линтеров (staticcheck и ineffectual assign) в одном инструменте.
misspell — инструмент для обнаружения опечаток в комментариях и строковых литералах.


Дополнительные инструменты:
govet — встроенный инструмент для анализа кода (аналогичен go vet, но часто используется отдельно).
testify — фреймворк для юнит-тестирования, включает полезную утилиту assert для улучшения читаемости тестового кода.
depguard — контролирует зависимости проекта, предотвращает использование нежелательных внешних библиотек.
varcheck — ищет неиспользованные локальные переменные.
deadcode — отслеживает неиспользованный код, удаляя мертвые функции и пакеты.


Какое отношение линтеры имеют к CI?

CI (Continuous Integration — непрерывная интеграция) — методология регулярного выполнения автоматизированных сборок и тестов после каждого коммита или определённых интервалов времени.
Линтеры важная часть процесса CI, поскольку выполняют предварительную проверку качества кода перед запуском сборки и тестирования.


Связь линтеров и CI:
Автоматизированные проверки с использованием линтеров выполняются в рамках пайплайна CI, что позволяет выявить проблемы с качеством кода, стилем или потенциальными ошибками.
Интеграция линтеров гарантирует единообразие стиля и улучшает качество кода, снижая риск появления багов и затруднений в поддержке проекта.
Многие CI-платформы (Jenkins, GitLab CI, CircleCI, Travis CI) поддерживают запуск линтеров автоматически вместе с этапами сборки.


Зачем нужен CI в процессе разработки?

Непрерывная интеграция направлена на повышение эффективности командной работы и снижение рисков, связанных с интеграцией новых изменений в проект.


Основные причины важности CI:


Преимущества CI:
Быстрая обратная связь — каждое изменение проходит серию проверок, включая тесты и линтеры, что позволяет получать уведомления о проблемах и оперативно устранять их.
Обнаружение конфликтов заранее — частые интеграции уменьшают вероятность возникновения крупных конфликтов при объединении ветвей, особенно в больших командах.
Повышение качества кода — регулярные проверки с помощью линтеров и тестов обеспечивают высокий уровень качества и снижают количество дефектов.
Минимизация риска регрессий — тесты и проверка покрытия показывают, что новые изменения не нарушили существующие функциональные возможности.
Автоматизация процессовавтоматизация этапов сборки, тестирования и деплоймента экономит время и снижает нагрузку на разработчиков.
Постоянная готовность к релизу — постоянная интеграция и тестирование делают продукт готовым к выпуску, сокращая задержку между разработкой и релизом.

CI существенно ускоряет разработку и уменьшает риски внесения ошибок в систему, повышая стабильность и надёжность продукта.
#493_GO_ODP_Q18

Можно ли использовать один и тот же буфер []byte в нескольких горутинах?
Зачем может понадобиться использовать один и тот же буфер []byte в нескольких горутинах параллельно?
Что именно будем в защищать мьютексом в общем буфере?


Нет, напрямую нельзя безопасно использовать один и тот же срез байтов []byte одновременно несколькими горутинами без синхронизации.
Причина в том, что одновременный доступ к одному и тому же ресурсу (особенно запись) может привести к состоянию гонки (race condition), когда одна goroutine перезаписывает данные другой goroutine.

Однако можно обезопасить такой доступ с помощью механизмов синхронизации, таких как мьютексы (sync.Mutex), каналы (channel) или атомарные операции (atomic package).
Использование мьютекса позволит организовать безопасный параллельный доступ к общему буферу.


Зачем может понадобиться использовать один и тот же буфер []byte в нескольких горутинах параллельно?

Параллельное использование общего буфера может потребоваться в следующих ситуациях:
Разделение ресурсов памятикогда общая память ограничена, и желательно уменьшить потребление оперативной памяти путём совместного использования одной большой области памяти разными частями программы.
Оптимизация производительности — общий буфер может использоваться для организации конвейеров обработки данных.
Одна goroutine записывает данные в буфер, другая считывает их оттуда, организуя параллельную обработку.
Буферизация данных — буферы могут служить для временного хранения результатов, полученных асинхронно, пока не завершатся другие этапы обработки.

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


Что именно будем защищать мьютексом в общем буфере?
Защищать придётся любую операцию, которая изменяет состояние буфера, а именно:
Операции записи (запись данных в буфер).
Операции чтения (чтение данных из буфера), если возможна ситуация конкурентного доступа на чтение и запись одновременно.

Пример безопасного использования буфера с мьютексом:
package main

import (
"fmt"
"sync"
)

func main() {
var buffer []byte
bufferMutex := sync.Mutex{}

go func() {
for i := 0; i < 10; i++ {
bufferMutex.Lock()
defer bufferMutex.Unlock()
buffer = append(buffer, byte(i))
}
}()

go func() {
for len(buffer) < 10 {
bufferMutex.Lock()
if len(buffer) > 0 {
fmt.Printf("%d\n", buffer[len(buffer)-1])
buffer = buffer[:len(buffer)-1]
}
bufferMutex.Unlock()
}
}()

// Ждём завершение всех горутин
wg := &sync.WaitGroup{}
wg.Add(2)
wg.Done()
wg.Done()
wg.Wait()
}


Здесь обе горутины используют общую область памяти (buffer), защищённую мьютексом.
Мьютекс
блокирует доступ к общим ресурсам во избежание состояний гонок.

Чтобы избежать блокировки при чтении или записи данных, важно применять мьютекс только вокруг тех участков кода, которые действительно требуют защиты.
#494_GO_ODP_Q19

Какие типы мьютексов предоставляет stdlib Golang?
Что именно и от чего защищает мьютекс?


Библиотека stdlib Go предлагает два основных типа мьютексов:

1. Mutex (sync.Mutex) — стандартный блокирующий мьютекс.
Когда один горутин захватывает мьютекс, другие горутины будут ожидать освобождения блокировки перед продолжением своей работы.
Используется для защиты совместного ресурса от одновременного доступа несколькими горутинами:
import (
"sync"
)

var mu sync.Mutex

func main() {
mu.Lock()
defer mu.Unlock()
}


Что защищает?
Мьютекс обеспечивает исключительный доступ к критическим секциям кода или ресурсам (например, переменным), предотвращая конфликты между параллельными потоками исполнения (горутинами).


2. RWMutex (sync.RWMutex) — читательско-писательский мьютекс.
Позволяет нескольким читателям одновременно получать доступ к защищаемому ресурсу, но запись разрешена только одному писателю единовременно.
Это повышает производительность в ситуациях, когда чтение чаще встречается, чем запись:
import (
"sync"
)

var rwmu sync.RWMutex

func read() {
rwmu.RLock()
defer rwmu.RUnlock()
}

func write() {
rwmu.Lock()
defer rwmu.Unlock()
}


Что защищает?
Этот тип мьютекса оптимизирован для ситуаций, когда чтение осуществляется гораздо чаще записи, обеспечивая эффективную синхронизацию между чтением и записью совместно используемого ресурса.


Оба типа мьютексов предназначены для предотвращения гонки данных (data race condition).
Они обеспечивают безопасность конкурентного доступа к разделяемым данным путём временной блокировки ресурсов, гарантируя целостность и согласованность состояния программы.
#495_GO_ODP_Q20

Что такое lock-free структуры данных, и есть ли в Go такие?
Что такое atomic?
Что такое sync.Map?
Sync.Map — lock-free или нет?



Lock-Free структура данных — тип параллельной структуры данных, обеспечивающий согласованность состояния между потоками без блокировки критической секции мьютексами (mutex).
Это означает, что даже если один поток временно приостановлен или блокирован (например, вызванное ожидание ввода-вывода), другие потоки всё равно смогут продолжить свою работу с данной структурой данных без ожидания освобождения ресурса.
Такая конструкция улучшает производительность многопоточных приложений, снижая вероятность возникновения узких мест.

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


Atomic.
В Go пакет sync/atomic обеспечивает доступ к примитивам атомарных операций над базовыми типами данных (uint32, uint64, uintptr), позволяя избежать необходимости в обычных мьютексах для простых операций типа чтения-загрузки, записи-хранения и арифметики:
import "sync/atomic"

// Загрузка значения атома
value := atomic.LoadUint32(&val)

// Установка нового значения атома
atomic.StoreUint32(&val, newValue)

// Атомарная операция сравнения и обмена (CAS)
if atomic.CompareAndSwapUint32(&val, oldVal, newVal) {
// Обмен успешно выполнен
}


Эти операции гарантируют безопасность доступа в многопоточной среде без задержек от традиционных мьютексов.


Sync.Map.
Тип sync.Map представляет собой многопоточную карту (ассоциативный массив), оптимизированную для высококонкурентных сред.

Основные характеристики:
Гарантированная безопасность конкурентности
(не требует ручного управления мьютексами).
Оптимизация производительности для случаев интенсивного чтения.
Поддерживает неблокирующие итерации по карте (чтение значений одновременно несколькими потоками).


Является ли sync.Map lock-free?
Нет, sync.Map формально не является lock-free.
Однако, его реализация внутренне разделяет состояние карты таким образом, что чтение выполняется быстро и эффективно, используя минимальные блокировки лишь при изменении состояния карты (запись новых элементов или удаление существующих).
Таким образом, хотя технически он основан на блокировках (использует внутренний мьютекс), его использование минимизирует влияние конкуренции потоков, приближаясь по поведению к lock-free алгоритмам.


Lock-free обеспечение конкурентного доступа без классических блокировок.
Atomicнабор низкоуровневых атомарных операций, предоставляющих безопасный доступ к данным в параллельном окружении.
Sync.Map — многопоточная карта, использующая внутренние блокировки для изменения состояния, но высокоэффективна благодаря минимальной конкуренции.


Таким образом, несмотря на наличие внутренней блокировки, sync.Map спроектирована так, чтобы минимизировать её влияние на общую производительность программы.
Channel name was changed to «DNK_C_C++_Go_Rust»
#496_GO_ODP_Q21

Способы поиска проблем производительности на продакшене? Какие проблемы производительности вы знаете?
Может ли быть так, что потребление ресурсов (CPU, RAM, disk/net bandwidth) вполне умеренное, а пользователи жалуются на «тормоза»?
На что, чаще всего, жалуются пользователи, и как это связано с тем, что вы видите в системе?



Способы поиска проблем производительности на продакшене:

Мониторинг метрик системы — включает сбор и визуализацию различных показателей серверов и приложений:
Load Average — средняя нагрузка на систему.
Использование CPU, памяти (RAM), дисков (I/O), сети (bandwidth).
Количество активных процессов, соединений TCP/IP, очередь запросов базы данных и др.

Инструменты мониторинга:
Prometheus + Grafana, Zabbix, New Relic, Datadog.


Логирование ошибок и предупреждений
— анализ лог-файлов помогает выявить узкие места приложения:
Ошибки HTTP-запросов (5xx, 4xx).
Таймауты запросов (request timeouts).
Исключение исключений внутри сервера.
Медленные SQL-запросы.

Средства анализа логов:
ELK Stack (Elasticsearch+Logstash+Kibana), Graylog, Splunk.


Профилирование нагрузки и трассировка запросовпозволяет измерять производительность отдельных компонентов системы:
Tracing — понимание пути выполнения запросов от клиента до бэкенд-сервисов и обратно.
Трассировка конкретных операций (SQL-запросы, внешние API-вызовы).

Средства профилирования:
Zipkin, Jaeger, Pinpoint, OpenTelemetry.


Анализ зависимостей и взаимодействий сервисовпри распределенной архитектуре важно отслеживать зависимости между сервисами:
Задержки передачи данных между микросервисами.
Пропускная способность каналов связи
(сетевые задержки).
Блокировки транзакций в базах данных.


APM (Application Performance Monitoring) — комплексные решения позволяют мониторить сразу несколько аспектов:
Время отклика страниц/API.
Нагрузка на серверы.
Производительность фронтенда.

Примеры инструментов:
AppDynamics, Dynatrace, Lightstep.


Проблемы производительности:

High Load Average высокая загрузка процессора из-за чрезмерного количества потоков или блокировок (например, lock contention).
Медленная база данныхнедостаточная индексация таблиц, плохие запросы, нехватка ресурсов БД (slow queries, deadlocks).
Нехватка оперативной памяти — большое количество объектов в памяти (memory leaks), недостаточный объем выделенного ресурса (OOM issues).
Недостаточная пропускная способность сети перегруженность сетевых интерфейсов, низкая скорость чтения/записи файлов.
Высокая задержка I/O (disk I/O latency)низкая производительность жестких дисков или SSD, медленное чтение записей.
Проблемы масштабированиянеправильно настроенная балансировка нагрузки, недостаток реплик, плохо подобранное горизонтальное масштабирование.
Ошибка конфигураций некорректные настройки контейнеров Docker/Kubernetes, неправильные тайминги и лимиты.
Зависимость от внешних сервисов — медленный отклик сторонних API, перегрузка CDN-ресурсов, долгие DNS-разрешения.


Умеренные ресурсы и жалобы пользователей («тормоза»).

Такое возможно! Основные причины:
Latency сетиплохое соединение у конечного пользователя (низкий ping, нестабильная связь). Например, клиент находится далеко географически от сервера, и каждый запрос проходит большое расстояние.
Неподходящие браузеры или устройствапользователь работает на слабых устройствах или устаревших версиях браузера.
JavaScript/CSS/HTML проблемы на стороне фронтаизбыточные DOM-операции, неэффективные JS-скрипты, неоптимизированные CSS-правила замедляют рендеринг страницы.
Асинхронность и ожидание ресурсов — долгие загрузки статики (изображения, стили, скрипты), много асинхронных AJAX-запросов.
Кэширование — отсутствие кэшированных ресурсов на стороне клиента (неправильные заголовки Cache-Control, Expires).
Ожидания на стороне бизнес-логики — внутренняя логика приложения имеет высокие ожидания (long running transactions, bulk operations).

.
Жалобы пользователей и видимые симптомы в системе:

«Страницы долго загружаются»
долгое выполнение PHP-кода, долгая отдача статичных ресурсов, медленные операции на сервере (SQL-запросы, вызовы API).

«Система подвисает при работе»длительные вычисления на фронте (JS-циклы, перерисовка UI), ресурсоемкость браузера, тяжелые JavaScript-библиотеки.

«Приложение медленно откликается»высокая задержка на стороне сети, некорректная настройка кеширования, неправильно выбранные индексы в базе данных.

«Заполнение формы тормозит»сложная обработка форм на стороне клиента (валидация полей, Ajax-подсказки), не оптимальные взаимодействия (slow response times from server-side logic).


Чтобы связать такие жалобы с системой, нужно анализировать журналы запросов, сетевую активность, профилировку исполнения и мониторинг фронтенда.
Например, инструмент Lighthouse (Google Chrome DevTools) покажет конкретные моменты тормозов во фронтенде, а серверные метрики дадут подсказки относительно возможной внутренней блокировки или долгих SQL-запросов.


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

Стандартный набор метрик Prometheus в Go -программе?
Оставив Prometeus в стороне — что вообще Go-программа способна о себе рассказать?
Метрики runtime — что это, и откуда берется?



Стандартный набор метрик Prometheus в Go-программе.

Prometheus предлагает стандартный набор библиотек для сбора метрик в Go-приложениях.

Основные типы метрик:

Counter (Счётчик)используются для подсчёта количества событий (например, число обработанных запросов):
var requestsTotal = prometheus.NewCounter(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of processed HTTP requests.",
})


Gauge (Индикатор)показатель текущего значения переменной величины. Может увеличиваться и уменьшаться (например, свободная память):
var freeMemoryBytes = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "free_memory_bytes",
Help: "Amount of available memory in bytes.",
})


Histogram (Гистограмма)используется для измерения распределения значений, таких как продолжительность обработки запросов:
var requestDurationSeconds = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration distribution.",
Buckets: []float64{.005, .01, .025, .05, .1, .25},
},
[]string{"method"},
)


Summary (Краткое описание)обобщённая статистика (среднее значение, медиана, квантили) распределения значений:
var requestLatencyMilliseconds = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_latency_milliseconds",
Help: "Request processing time summary statistics.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, // percentiles
},
[]string{"endpoint"},
)


Пример стандартного набора метрик для веб-приложения на Go:
package main
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
requestsTotal = promauto.NewCounter(prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of processed HTTP requests.",
})
requestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration histogram.",
Buckets: prometheus.LinearBuckets(0.001, 0.001, 10),
}, []string{"path"})
)

func handler(w http.ResponseWriter, r *http.Request) {
defer func() { requestsTotal.Inc() }()
timer := prometheus.NewTimer(requestDurationSeconds.WithLabelValues(r.URL.Path))
defer timer.ObserveDuration()
w.Write([]byte("Hello World"))
}

func main() {
http.HandleFunc("/", handler)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}


.
Что вообще Go-программа способна о себе рассказать?

Go-программы предоставляют огромное количество встроенных возможностей для самоотчета благодаря пакету runtime.


Основные категории информации, которую можно собрать из программы на Go:

Метрики среды выполнения (runtime)эти данные предоставляются стандартным пакетом runtime.
Памятьобщий объём используемой памяти, выделенный heap-size, числа объектов GC, stack trace и размеры сегментов.
Процессориспользование процессорного времени, частота переключений контекста.
Сборщик мусора (GC)информация о количестве циклов сборки мусора, объёмах освобождаемой памяти, интервалах запуска.
Threads/Goroutinesобщее количество горутин, потоки выполнения, режим параллелизма.
Heapструктура куч, распределение объектов по типам и состояниям (allocated vs freed objects).

Пример простого вывода метрик среды выполнения:
package main
import (
"fmt"
"log"
"runtime"
)

func main() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc: %v MiB\n", bToMb(m.Alloc)) // Выделено памяти
fmt.Printf("Sys: %v MiB\n", bToMb(m.Sys)) // Общая система памяти
fmt.Printf("NumGC: %d\n", m.NumGC) // Число запусков GC
fmt.Printf("PauseTotalNs: %v ms\n", nsToMs(m.PauseTotalNs)) // Всего пауза GC
log.Println(runtime.NumGoroutine()) // Текущие активные goroutines
}

// Вспомогательные функции преобразования байтов и наносекунд
func bToMb(b uint64) float64 { return float64(b) / 1024 / 1024 }
func nsToMs(ns int64) float64 { return float64(ns) / 1_000_000 }



Другие важные показатели.

Помимо метрик runtime, Go-программы способны сообщать дополнительную полезную информацию:
Версия компилятора и окруженияимя операционной системы, архитектура, версия Go.
Процессы ввода-выводасчётчики открытых файловых дескрипторов, используемых сокетов, временных затрат на ввод-вывод.
Информация о приложениидлительность жизни процесса, количество обработанных сообщений, уровень конкурентности (goroutines, каналы).
Эффекты многопоточностидетали работы механизма планировщика, взаимодействие c очередями, работа воркер-пулов.


Откуда берутся метрики runtime?

Метрики среды выполнения собираются автоматически самим Go-интерпретатором и виртуальной машиной.
Эти данные доступны через пакет runtime, предоставляющий доступ к внутренним структурам Go-процесса:

Пакет runtime отслеживает изменения в структуре выполнения, включая работу сборщика мусора, управление памятью и работой планировщика.
Периодические события синхронизации происходят каждые несколько миллисекунд (по умолчанию примерно раз в 10 мс), собирая актуальные сведения о состоянии приложения.
Интерфейс runtime.MemStats предоставляет статистику памяти и другие характеристики среды выполнения.

Методы типа runtime.ReadMemStats() или runtime.NumGoroutine() вызывают внутренний механизм записи текущих состояний и возвращают собранную информацию приложению.


Таким образом, Go-программа сама собирает и агрегирует большую часть необходимой статистики без дополнительного вмешательства разработчика, кроме добавления соответствующих точек наблюдения и регистрации нужных коллекционеров метрик.
#498_GO_ODP_Q23

Как встроить стандартный профайлер в приложение на Go?
A нужен ли профайлер?
Что там есть вообще полезного?
Почему его нужно встраивать, а не запускать из-под него приложение?


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


Go поставляется со стандартной библиотекой профилировщика (net/http/pprof), позволяющей легко интегрировать профилирование прямо в приложение. Она поддерживает сбор статистики по CPU, памяти, блокировкам и другим метрикам.


Почему встроенный профайлер лучше отдельного запуска приложения под профайлером?
Удобство интеграции — не придется изменять способ запуска приложения каждый раз, когда необходимо посмотреть производительность.
Автоматическое получение результатовзапустив приложение с профайлером, можно получать данные удаленно и удобно анализировать их.
Минимальное влияние на продуктивную среду если профайлинг отключён в production, можно включить его временно, не останавливая работу системы.


Как встроить профайлер в приложение на Go?
Рассмотрим простое веб-приложение на Go, которое расширим возможностью профилирования.

Шаг 1: Подключение HTTP-профайлерачтобы добавить поддержку профайлинга, достаточно зарегистрировать обработчики маршрутов /debug/pprof следующим образом:
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
log.Println("Starting server at :8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}


При таком подходе доступ к маршруту /debug/pprof позволит увидеть стандартные страницы профилирования Go, включая статистику по процессору, памяти и горутинам.

Шаг 2: Сбор профилей вручнуютакже можно собрать профили вручную, используя API пакета runtime/pprof :
// Запись профиля ЦПУ
package main
import (
"log"
"os"
"runtime/pprof"
"time"
)

func main() {
f, err := os.Create("cpu.pprof")
if err != nil {
log.Fatal(err)
}
defer f.Close()

// Начинаем запись профиля ЦПУ
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()

time.Sleep(time.Second * 10) // Моделируем длительную операцию
}


Пример создаст файл cpu.pprof, содержащий профиль потребления CPU приложением за 10 секунд.


Стандартный профайлер Go предоставляет следующие типы профилей:
Профиль CPU (/debug/pprof/profile)позволяет видеть, какие функции тратят больше всего времени процессора.
Профиль памяти (/debug/pprof/heap)показывает распределение памяти между функциями, помогает обнаружить утечки памяти.
Горутинный стек (/debug/pprof/goroutine)демонстрирует количество активных горутин и их состояние.
Аллокатор блоков памяти (/debug/pprof/block)помогает диагностировать блокировки потоков ожидания выделения памяти.
Блокировки каналов синхронизации (/debug/pprof/threadcreate)анализирует создание новых потоков ОС, вызванное конкурентностью внутри приложения.


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


Использование встроенного профайлера значительно упрощает диагностику проблем производительности и повышает удобство эксплуатации ваших приложений на Go.
#499_GO_ODP_Q24

Overhead от стандартного профайлера?
Что такое «семплирующий профайлер» и почему это хорошо?



Использование встроенных инструментов профилирования, предоставляемых пакетом net/http/pprof, практически не оказывает значительного негативного влияния на производительность приложения.
Это связано с особенностями реализации профайлеров в Go:

Низкий оверхед сбора данныхстандартный профайлер собирает статистические данные периодически (например, каждые 10 миллисекунд), используя механизм семплирования.
Поэтому профайлер минимально влияет на нормальную работу программы.

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

Тем не менее, рекомендуется выключать профайлер в productive среде, если он не нужен постоянно. Хотя overhead низкий, лишние расходы ресурсов всё же присутствуют.


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

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

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


Профайлер особенно эффективен в следующих ситуациях:
Выяснение причины медленной работы отдельных функций или операций.
Поиск утечек памяти или чрезмерного расхода ресурсов.
Определение неэффективных алгоритмов или структур данных.
Мониторинг активности большого числа параллельных процессов
(горутин).



Встроенный профайлер Go сочетает простоту использования, высокую эффективность и минимальное воздействие на общую производительность приложения.
#500_GO_ODP_Q25

Почему в Go используется встраивание (композиция), а не наследование?
Что означает буква L в аббревиатуре SOLID?



Композиция (также известная как встраивание типов) является основным способом повторного использования поведения и организации структуры объектов в Go.


Причины отказа от традиционного наследования классов в Go:

Простота и ясность дизайнаGo стремится минимизировать сложность и избыточность, делая акцент на простой и понятной структуре кода.
Наследование может приводить к запутанным иерархиям классов, сложностям в поддержке и тестировании.
Гибкость и композиция интерфейсоввместо строгого дерева наследования, композиция позволяет свободно комбинировать различные функциональные элементы и создавать новые объекты путем объединения существующих компонентов.
Этот подход соответствует принципам проектирования Go, где предпочтение отдаётся легкости изменения и расширения функциональности без усложнения базовой архитектуры.
Уменьшение сложности зависимостейкомпозиция уменьшает вероятность возникновения ошибок, связанных с конфликтующими методами и свойствами, которые возникают при множественном наследовании.
Гораздо проще управлять поведением объекта, состоящего из независимых элементов, чем объектом, зависящим от множества предков.
Совместимость с принципами программирования на Goструктуры и интерфейсы в Go специально разработаны для лёгкого взаимодействия друг с другом посредством композиции, что делает этот подход естественным и эффективным.

Пример композиции в Go:
type Animal struct {
Name string
Sound string
}

type Dog struct {
Animal
Breed string
}

func NewDog(name, breed string) *Dog {
return &Dog{
Animal: Animal{Name: name, Sound: "Woof"},
Breed: breed,
}
}

func main() {
dog := NewDog("Rex", "Labrador")
fmt.Printf("%s is a %s and says %s\n", dog.Name, dog.Breed, dog.Sound)
}


Cтруктура Dog включает структуру Animal, что позволяет использовать её поля и методы непосредственно.


SOLID — набор принципов проектирования программного обеспечения, предложенных Робертом Мартином («Uncle Bob»).

S: Single Responsibility Principle (Принцип единственной ответственности)
O: Open/Closed Principle (Принцип открытости-закрытости)
L: Liskov Substitution Principle (Принцип замещения Барбары Лисков)
I: Interface Segregation Principle (Принцип разделения интерфейса)
D: Dependency Inversion Principle (Принцип инверсии зависимости)


Liskov Substitution Principle (Принцип замещения Барбары Лисков) гласит, что подклассы должны быть взаимозаменяемыми с базовыми классами.
То есть, если есть тип T, и cоздаётся производный тип S, то любой экземпляр типа S должен быть допустимым аргументом везде, где ожидается аргумент типа T.

Формулировка принципа была дана Барбарой Лисков:
Функциональные спецификации дочерних классов не должны противоречить контрактам родительских классов.

Пример нарушения принципа:
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height

@property
def area(self):
return self._width * self._height

class Square(Rectangle):
def __init__(self, side_length):
super().__init__(side_length, side_length)

# Нарушение контракта родителя, изменение ширины меняет высоту
def set_width(self, value):
self._width = value
self._height = value


Квадрат нарушает контракт прямоугольника, потому что метод set_width() одновременно устанавливает ширину и высоту, что недопустимо для прямоугольников общего вида.

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

Какие средства обобщенного программирования есть в Go?
Что такое map-reduce? Как его реализовать в Go? А без interface{}?
Что такое кодогенерация и как ее можно использовать в этой задаче?



Обобщённое программирование подход, позволяющий писать универсальный код, который применим к различным типам данных.
До появления версии Go 1.18 (включительно), Go не поддерживал полноценные дженерики (обобщённый тип).
В Go 1.18 появилась поддержка дженериков, позволяющая реализовывать обобщённые структуры и функции.

Инструменты обобщённого программирования в Go:
Интерфейсыописывают поведение, а не конкретные типы данных.
Любой тип, удовлетворяющий интерфейсному контракту, может использоваться в качестве параметра метода или возвращаемого значения.
Джентики (Generics)в Go 1.18, появились дженерики, позволяющие объявлять универсальные функции и структуры, работающие с любыми типами данных, соответствующими заданным ограничениям:
// Пример интерфейса Sorter
type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

// Реализация сортировки массива чисел
type IntArray []int

func (a IntArray) Len() int { return len(a) }
func (a IntArray) Less(i, j int) bool { return a[i] < a[j] }
func (a IntArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// Обобщённая функция сортировки
func Sort(data Sorter) {
for i := range data {
for j := i + 1; j < data.Len(); j++ {
if data.Less(j, i) {
data.Swap(i, j)
}
}
}
}

// Пример с использованием дженериков (начиная с Go 1.18)
func Min[T any](values ...T) T {
var min T
first := true
for _, v := range values {
if first || less(v, min) {
min = v
first = false
}
}
return min
}

func less[T comparable](a, b T) bool {
return a < b
}



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

Процесс разделяется на две фазы:

Map-фазаисходные данные разбиваются на отдельные фрагменты, обрабатываемые параллельно.
Reduce-фазарезультаты обработки объединяются в единый итоговый результат.

Пример реализации MapReduce в Go:
Допустим, нужно посчитать сумму всех значений в большом массиве чисел:

package main
import (
"fmt"
"sync"
)

// Функция мапа, применяемая к каждому элементу
func mapper(chunks [][]int) chan int {
outCh := make(chan int)
go func() {
for _, chunk := range chunks {
sum := 0
for _, val := range chunk {
sum += val
}
outCh <- sum
}
close(outCh)
}()
return outCh
}

// Функция reduce, собирающая промежуточные суммы
func reducer(sumChans []chan int) int {
totalSum := 0
wg := sync.WaitGroup{}
for _, ch := range sumChans {
wg.Add(1)
go func(c chan int) {
for s := range c {
totalSum += s
}
wg.Done()
}(ch)
}
wg.Wait()
return totalSum
}

func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
chunkSize := 2
chunks := make([][]int, 0)

// Разбиение исходных данных на чанки
for i := 0; i < len(numbers); i += chunkSize {
end := i + chunkSize
if end > len(numbers) {
end = len(numbers)
}
chunks = append(chunks, numbers[i:end])
}

// Создаем каналы для каждой порции данных
mapChannels := make([]chan int, len(chunks))
for i := range chunks {
mapChannels[i] = mapper([][]int{chunks[i]})
}

// Собираем итоги
result := reducer(mapChannels)
fmt.Println(result) // Выведет 55
}


Эта реализация показывает базовые принципы MapReduceразделение данных на кусочки, параллельную обработку каждого кусочка и последующее объединение результатов.

.
Кодогенерация в Go.

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


Инструменты для кодогенерации в Go включают:
go generateкоманда компилятора Go, позволяющая запускать скрипты для автоматической генерации кода.
Templating toolsбиблиотеки вроде text/template и html/template, позволяющие динамически формировать тексты или HTML-контент.

Пример простого генератора интерфейсов:
//gen_interface.go
//+build ignore
package main
import (
"fmt"
"go/types"
"strings"
)

const template = `
type MyInterface interface {
Get%s() %s
Set%s(%s)
}`

func main() {
types := []string{"Int", "String"}
for _, t := range types {
getter := fmt.Sprintf(template, strings.Title(t), t, strings.Title(t), t)
fmt.Println(getter)
}
}


Команда go generate выполнит скрипт и сгенерирует нужный интерфейс.


Обобщенное программирование в Go реализуется через интерфейсы и дженерики.
Парадигма MapReduce полезна для параллельной обработки больших объёмов данных.
Кодогенерация позволяет упростить рутинные задачи и ускорить разработку сложных систем.