#493_GO_ODP_Q18
Можно ли использовать один и тот же буфер []byte в нескольких горутинах?
Зачем может понадобиться использовать один и тот же буфер []byte в нескольких горутинах параллельно?
Что именно будем в защищать мьютексом в общем буфере?
Нет, напрямую нельзя безопасно использовать один и тот же срез байтов []byte одновременно несколькими горутинами без синхронизации.
Причина в том, что одновременный доступ к одному и тому же ресурсу (особенно запись) может привести к состоянию гонки (race condition), когда одна goroutine перезаписывает данные другой goroutine.
Однако можно обезопасить такой доступ с помощью механизмов синхронизации, таких как мьютексы (sync.Mutex), каналы (channel) или атомарные операции (atomic package).
Использование мьютекса позволит организовать безопасный параллельный доступ к общему буферу.
Зачем может понадобиться использовать один и тот же буфер []byte в нескольких горутинах параллельно?
Параллельное использование общего буфера может потребоваться в следующих ситуациях:
Разделение ресурсов памяти — когда общая память ограничена, и желательно уменьшить потребление оперативной памяти путём совместного использования одной большой области памяти разными частями программы.
Оптимизация производительности — общий буфер может использоваться для организации конвейеров обработки данных.
Одна goroutine записывает данные в буфер, другая считывает их оттуда, организуя параллельную обработку.
Буферизация данных — буферы могут служить для временного хранения результатов, полученных асинхронно, пока не завершатся другие этапы обработки.
Рассмотрим сценарий, где одна goroutine получает поток данных и сохраняет их в общий буфер, а другая goroutine отправляет эти данные дальше в сеть или файл.
Что именно будем защищать мьютексом в общем буфере?
Защищать придётся любую операцию, которая изменяет состояние буфера, а именно:
Операции записи (запись данных в буфер).
Операции чтения (чтение данных из буфера), если возможна ситуация конкурентного доступа на чтение и запись одновременно.
Пример безопасного использования буфера с мьютексом:
Здесь обе горутины используют общую область памяти (buffer), защищённую мьютексом.
Мьютекс блокирует доступ к общим ресурсам во избежание состояний гонок.
Чтобы избежать блокировки при чтении или записи данных, важно применять мьютекс только вокруг тех участков кода, которые действительно требуют защиты.
Можно ли использовать один и тот же буфер []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) — стандартный блокирующий мьютекс.
Когда один горутин захватывает мьютекс, другие горутины будут ожидать освобождения блокировки перед продолжением своей работы.
Используется для защиты совместного ресурса от одновременного доступа несколькими горутинами:
Что защищает?
Мьютекс обеспечивает исключительный доступ к критическим секциям кода или ресурсам (например, переменным), предотвращая конфликты между параллельными потоками исполнения (горутинами).
2. RWMutex (sync.RWMutex) — читательско-писательский мьютекс.
Позволяет нескольким читателям одновременно получать доступ к защищаемому ресурсу, но запись разрешена только одному писателю единовременно.
Это повышает производительность в ситуациях, когда чтение чаще встречается, чем запись:
Что защищает?
Этот тип мьютекса оптимизирован для ситуаций, когда чтение осуществляется гораздо чаще записи, обеспечивая эффективную синхронизацию между чтением и записью совместно используемого ресурса.
Оба типа мьютексов предназначены для предотвращения гонки данных (data race condition).
Они обеспечивают безопасность конкурентного доступа к разделяемым данным путём временной блокировки ресурсов, гарантируя целостность и согласованность состояния программы.
Какие типы мьютексов предоставляет 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), позволяя избежать необходимости в обычных мьютексах для простых операций типа чтения-загрузки, записи-хранения и арифметики:
Эти операции гарантируют безопасность доступа в многопоточной среде без задержек от традиционных мьютексов.
Sync.Map.
Тип sync.Map представляет собой многопоточную карту (ассоциативный массив), оптимизированную для высококонкурентных сред.
Основные характеристики:
Гарантированная безопасность конкурентности (не требует ручного управления мьютексами).
Оптимизация производительности для случаев интенсивного чтения.
Поддерживает неблокирующие итерации по карте (чтение значений одновременно несколькими потоками).
Является ли sync.Map lock-free?
Нет, sync.Map формально не является lock-free.
Однако, его реализация внутренне разделяет состояние карты таким образом, что чтение выполняется быстро и эффективно, используя минимальные блокировки лишь при изменении состояния карты (запись новых элементов или удаление существующих).
Таким образом, хотя технически он основан на блокировках (использует внутренний мьютекс), его использование минимизирует влияние конкуренции потоков, приближаясь по поведению к lock-free алгоритмам.
Lock-free — обеспечение конкурентного доступа без классических блокировок.
Atomic — набор низкоуровневых атомарных операций, предоставляющих безопасный доступ к данным в параллельном окружении.
Sync.Map — многопоточная карта, использующая внутренние блокировки для изменения состояния, но высокоэффективна благодаря минимальной конкуренции.
Таким образом, несмотря на наличие внутренней блокировки, sync.Map спроектирована так, чтобы минимизировать её влияние на общую производительность программы.
Что такое 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 спроектирована так, чтобы минимизировать её влияние на общую производительность программы.
#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).
.
Способы поиска проблем производительности на продакшене? Какие проблемы производительности вы знаете?
Может ли быть так, что потребление ресурсов (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-запросов.
Таким образом, диагностика должна учитывать одновременно и состояние серверов, и поведение клиентов, позволяя комплексно подходить к решению проблемы.
«Страницы долго загружаются» — долгое выполнение 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 (Счётчик) — используются для подсчёта количества событий (например, число обработанных запросов):
Gauge (Индикатор) — показатель текущего значения переменной величины. Может увеличиваться и уменьшаться (например, свободная память):
Histogram (Гистограмма) — используется для измерения распределения значений, таких как продолжительность обработки запросов:
Summary (Краткое описание) — обобщённая статистика (среднее значение, медиана, квантили) распределения значений:
Пример стандартного набора метрик для веб-приложения на Go:
.
Стандартный набор метрик 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).
Пример простого вывода метрик среды выполнения:
Другие важные показатели.
Помимо метрик runtime, Go-программы способны сообщать дополнительную полезную информацию:
Версия компилятора и окружения — имя операционной системы, архитектура, версия Go.
Процессы ввода-вывода — счётчики открытых файловых дескрипторов, используемых сокетов, временных затрат на ввод-вывод.
Информация о приложении — длительность жизни процесса, количество обработанных сообщений, уровень конкурентности (goroutines, каналы).
Эффекты многопоточности — детали работы механизма планировщика, взаимодействие c очередями, работа воркер-пулов.
Откуда берутся метрики runtime?
Метрики среды выполнения собираются автоматически самим Go-интерпретатором и виртуальной машиной.
Эти данные доступны через пакет runtime, предоставляющий доступ к внутренним структурам Go-процесса:
Пакет runtime отслеживает изменения в структуре выполнения, включая работу сборщика мусора, управление памятью и работой планировщика.
Периодические события синхронизации происходят каждые несколько миллисекунд (по умолчанию примерно раз в 10 мс), собирая актуальные сведения о состоянии приложения.
Интерфейс runtime.MemStats предоставляет статистику памяти и другие характеристики среды выполнения.
Методы типа runtime.ReadMemStats() или runtime.NumGoroutine() вызывают внутренний механизм записи текущих состояний и возвращают собранную информацию приложению.
Таким образом, 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 следующим образом:
При таком подходе доступ к маршруту /debug/pprof позволит увидеть стандартные страницы профилирования Go, включая статистику по процессору, памяти и горутинам.
Шаг 2: Сбор профилей вручную — также можно собрать профили вручную, используя API пакета runtime/pprof :
Пример создаст файл cpu.pprof, содержащий профиль потребления CPU приложением за 10 секунд.
Стандартный профайлер Go предоставляет следующие типы профилей:
Профиль CPU (/debug/pprof/profile) — позволяет видеть, какие функции тратят больше всего времени процессора.
Профиль памяти (/debug/pprof/heap) — показывает распределение памяти между функциями, помогает обнаружить утечки памяти.
Горутинный стек (/debug/pprof/goroutine) — демонстрирует количество активных горутин и их состояние.
Аллокатор блоков памяти (/debug/pprof/block) — помогает диагностировать блокировки потоков ожидания выделения памяти.
Блокировки каналов синхронизации (/debug/pprof/threadcreate) — анализирует создание новых потоков ОС, вызванное конкурентностью внутри приложения.
Почему встраивание предпочтительнее?
Простота диагностики — включаете профайлер один раз, получаете возможность регулярно проверять производительность даже в продакшене.
Меньше влияния на продуктивность разработчиков — не нужно переключаться между разными инструментами анализа производительности.
Получение реальных производственных данных — иногда тесты на локальной машине недостаточно репрезентативны, встроенные профайлеры позволяют наблюдать реальную картину нагрузки.
Использование встроенного профайлера значительно упрощает диагностику проблем производительности и повышает удобство эксплуатации ваших приложений на Go.
Как встроить стандартный профайлер в приложение на 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 сочетает простоту использования, высокую эффективность и минимальное воздействие на общую производительность приложения.
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:
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.
Формулировка принципа была дана Барбарой Лисков:
Функциональные спецификации дочерних классов не должны противоречить контрактам родительских классов.
Пример нарушения принципа:
Квадрат нарушает контракт прямоугольника, потому что метод set_width() одновременно устанавливает ширину и высоту, что недопустимо для прямоугольников общего вида.
Правильный подход состоит в том, чтобы избежать ситуаций, когда подкласс ведёт себя иначе, чем ожидалось бы от суперкласса.
Почему в 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, появились дженерики, позволяющие объявлять универсальные функции и структуры, работающие с любыми типами данных, соответствующими заданным ограничениям:
MapReduce — парадигма обработки данных, используемая для масштабирования вычислений на большие объёмы данных путём распределения задач по нескольким машинам или ядрам процессоров.
Процесс разделяется на две фазы:
Map-фаза — исходные данные разбиваются на отдельные фрагменты, обрабатываемые параллельно.
Reduce-фаза — результаты обработки объединяются в единый итоговый результат.
Пример реализации MapReduce в Go:
Допустим, нужно посчитать сумму всех значений в большом массиве чисел:
Эта реализация показывает базовые принципы MapReduce — разделение данных на кусочки, параллельную обработку каждого кусочка и последующее объединение результатов.
.
Какие средства обобщенного программирования есть в 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-контент.
Пример простого генератора интерфейсов:
Команда go generate выполнит скрипт и сгенерирует нужный интерфейс.
Обобщенное программирование в Go реализуется через интерфейсы и дженерики.
Парадигма MapReduce полезна для параллельной обработки больших объёмов данных.
Кодогенерация позволяет упростить рутинные задачи и ускорить разработку сложных систем.
Кодогенерация — автоматический процесс создания кода на основании входных данных или шаблонов.
Часто применяется в случаях, когда приходится повторять однообразные операции над множеством похожих сущностей.
Например, можно автоматически генерировать сериализаторы 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 полезна для параллельной обработки больших объёмов данных.
Кодогенерация позволяет упростить рутинные задачи и ускорить разработку сложных систем.
#502_GO_ODP_Q27
Какие технологические преимущества есть у языка Go?
Для каких задач Go подходит идеально?
Для каких задач Go не очень подходит, но может быть полезен?
Чем отличается goroutine от OS thread?
Как устроен сетевой ввод-вывод в Go?
Golang разработан Google с целью решения ряда практических проблем, возникающих при разработке крупных проектов.
Основные технические достоинства Go:
Высокая производительность и компактность двоичных файлов — компиляция в нативный машинный код обеспечивает высокую скорость исполнения.
Минималистичный дизайн библиотеки стандартных пакетов способствует созданию небольших исполняемых файлов.
Эффективная работа с многопоточностью — goroutines предоставляют эффективный механизм асинхронного выполнения задач, позволяя обрабатывать тысячи одновременных запросов с минимальным расходом ресурсов.
Встроенная поддержка каналов (channels) упрощает взаимодействие между горутинами и обмен данными.
Сборка мусора (GC) — автоматическая очистка неиспользуемых объектов снижает нагрузку на разработчика и улучшает устойчивость приложений.
Сборщик мусора оптимизирован для высокой пропускной способности и низкой задержки.
Статическая типизация с простым синтаксисом — типы проверяются на этапе компиляции, снижая риск ошибок во время выполнения.
Легко читаемый и лаконичный синтаксис облегчает понимание и сопровождение кода.
Приятная экосистема и стандарты качества — богатая стандартная библиотека покрывает большинство базовых потребностей (сетевые протоколы, базы данных, криптография и т.п.).
Инструментарий Go (go fmt, go vet, go test) поощряет аккуратный стиль написания кода и дисциплину тестирования.
Кросс-компиляция — возможность сборки бинарных файлов для различных платформ и архитектур (Windows, Linux, macOS, ARM и т.д.) одним нажатием кнопки.
Простота изучения и поддержки проекта — четкая философия
"Less is more" — меньше ключевых слов, минимум магии, отсутствие перегрузки операторов.
Отсутствие сложной объектно-ориентированной модели упрощает архитектуру и делает проекты легко поддерживаемыми.
Go отлично подходит для следующего набора задач:
Разработка серверных приложений и микросервисов — высокая производительность и простота развертывания делают Go идеальным выбором для backend-разработки.
Поддерживает RESTful API, RPC-сервисы, webhooks и интеграционные сервисы.
Работа с большими объемами данных и вычислительные задачи — благодаря эффективной работе с памятью и быстрым goroutines, Go прекрасно справляется с задачами обработки больших объемов данных.
Инфраструктура и DevOps-инструменты — такие утилиты, как Docker, Kubernetes, Terraform, изначально написаны на Go благодаря его эффективности и удобству кросс-компиляции.
API-серверы и шлюзы — Go способен быстро обрабатывать запросы и эффективно взаимодействовать с внешними сервисами.
Создание высоконагруженных сервисов — производительность Go на уровне C++, однако разработка быстрее и дешевле, чем на языках с более высоким порогом входа.
Хотя Go широко используется, существуют ситуации, когда его особенности становятся недостатками:
Численные вычисления (HPC) — приложения, требующие экстремальных математических расчетов (научные исследования, физика частиц, симуляции), часто выигрывают от специализированных языков (Python, Julia, Fortran).
Интерактивные графические интерфейсы (GUI) — платформы GUI вроде Qt, Electron или JavaFX поддерживают языки с богатыми фреймворками (C++, Python, JavaScript). Графические интерфейсы на чистом Go возможны, но требуют дополнительной инфраструктуры.
Параллелизм на уровне железа (GPU) — gorutines ориентированы на CPU, тогда как GPU-вычисления требуют низкоуровневых технологий (OpenCL, CUDA), поддерживаемых преимущественно на Си и C++.
Однако в перечисленных сценариях Go может играть вспомогательную роль (например, обработка сетей и логика сервера для HPC-проектов, контроль логики приложений на стороне клиента в GUI-проектах).
Какие технологические преимущества есть у языка Go?
Для каких задач Go подходит идеально?
Для каких задач Go не очень подходит, но может быть полезен?
Чем отличается goroutine от OS thread?
Как устроен сетевой ввод-вывод в Go?
Golang разработан Google с целью решения ряда практических проблем, возникающих при разработке крупных проектов.
Основные технические достоинства Go:
Высокая производительность и компактность двоичных файлов — компиляция в нативный машинный код обеспечивает высокую скорость исполнения.
Минималистичный дизайн библиотеки стандартных пакетов способствует созданию небольших исполняемых файлов.
Эффективная работа с многопоточностью — goroutines предоставляют эффективный механизм асинхронного выполнения задач, позволяя обрабатывать тысячи одновременных запросов с минимальным расходом ресурсов.
Встроенная поддержка каналов (channels) упрощает взаимодействие между горутинами и обмен данными.
Сборка мусора (GC) — автоматическая очистка неиспользуемых объектов снижает нагрузку на разработчика и улучшает устойчивость приложений.
Сборщик мусора оптимизирован для высокой пропускной способности и низкой задержки.
Статическая типизация с простым синтаксисом — типы проверяются на этапе компиляции, снижая риск ошибок во время выполнения.
Легко читаемый и лаконичный синтаксис облегчает понимание и сопровождение кода.
Приятная экосистема и стандарты качества — богатая стандартная библиотека покрывает большинство базовых потребностей (сетевые протоколы, базы данных, криптография и т.п.).
Инструментарий Go (go fmt, go vet, go test) поощряет аккуратный стиль написания кода и дисциплину тестирования.
Кросс-компиляция — возможность сборки бинарных файлов для различных платформ и архитектур (Windows, Linux, macOS, ARM и т.д.) одним нажатием кнопки.
Простота изучения и поддержки проекта — четкая философия
"Less is more" — меньше ключевых слов, минимум магии, отсутствие перегрузки операторов.
Отсутствие сложной объектно-ориентированной модели упрощает архитектуру и делает проекты легко поддерживаемыми.
Go отлично подходит для следующего набора задач:
Разработка серверных приложений и микросервисов — высокая производительность и простота развертывания делают Go идеальным выбором для backend-разработки.
Поддерживает RESTful API, RPC-сервисы, webhooks и интеграционные сервисы.
Работа с большими объемами данных и вычислительные задачи — благодаря эффективной работе с памятью и быстрым goroutines, Go прекрасно справляется с задачами обработки больших объемов данных.
Инфраструктура и DevOps-инструменты — такие утилиты, как Docker, Kubernetes, Terraform, изначально написаны на Go благодаря его эффективности и удобству кросс-компиляции.
API-серверы и шлюзы — Go способен быстро обрабатывать запросы и эффективно взаимодействовать с внешними сервисами.
Создание высоконагруженных сервисов — производительность Go на уровне C++, однако разработка быстрее и дешевле, чем на языках с более высоким порогом входа.
Хотя Go широко используется, существуют ситуации, когда его особенности становятся недостатками:
Численные вычисления (HPC) — приложения, требующие экстремальных математических расчетов (научные исследования, физика частиц, симуляции), часто выигрывают от специализированных языков (Python, Julia, Fortran).
Интерактивные графические интерфейсы (GUI) — платформы GUI вроде Qt, Electron или JavaFX поддерживают языки с богатыми фреймворками (C++, Python, JavaScript). Графические интерфейсы на чистом Go возможны, но требуют дополнительной инфраструктуры.
Параллелизм на уровне железа (GPU) — gorutines ориентированы на CPU, тогда как GPU-вычисления требуют низкоуровневых технологий (OpenCL, CUDA), поддерживаемых преимущественно на Си и C++.
Однако в перечисленных сценариях Go может играть вспомогательную роль (например, обработка сетей и логика сервера для HPC-проектов, контроль логики приложений на стороне клиента в GUI-проектах).
Отличия goroutines от OS threads.
Threads (потоки ОС) — создаются и управляются ОС.
Каждому потоку выделяется кусок оперативной памяти, и стоимость создания потока высока.
Goroutines — легкие потоки управления, создаваемые языком Go.
Их исполнение мультиплексируется поверх небольшого количества настоящих потоков ОС, что сильно экономит ресурсы и позволяет запускать десятки тысяч gorutines.
Стоимость запуска:
Создание потока ОС требует сотни килобайт памяти и занимает значительное время.
Горутины инициализируются с небольшим стеком (~2KB) и начинают выполняться мгновенно.
Управление ресурсами:
Потоки ОС используют глобальное управление потоками, и каждое ядро ОС управляет своими собственными потоками независимо.
Goroutines управляются внутренним диспетчером Go runtime, который распределяет выполнение горутин среди имеющихся ядер и потоков.
Контекст переключений:
Переключение контекста между потоками ОС дорого обходится (сохранение регистров, обновление кэшей и т.д.).
Switching context в рамках горутин осуществляется намного эффективнее, так как они сами организуют собственную очередь выполнения.
Организация сетевого ввода-вывода в Go.
Сетевое взаимодействие в Go построено вокруг концепций неблокирующего ввода-вывода и синхронного/асинхронного выполнения операций:
Неблокирующий ввод-вывод (non-blocking I/O) — операции чтения и записи сети выполняются асинхронно через механизм неблокирующих сокетов, предоставляя эффективный механизм обработки множества соединений.
Синхронизация с помощью каналов (channels) — каналы обеспечивают удобный способ обмена данными между горутинами и синхронизируют доступ к общим ресурсам.
Модель событийной петли (event loop) — внутренний диспетчер (scheduler) управляет выполнением горутин и планирует выполнение операций в очередях событий.
Шаблоны буферизации и мультиплексинга — пакеты net/http, bufio, tcp, udp предоставляют удобные способы работы с соединениями и пакетами данных.
Типичная схема сетевого взаимодействия выглядит примерно так:
Такой подход позволяет создать эффективные, высокопроизводительные серверы, способные обслуживать большое количество клиентов одновременно.
Threads (потоки ОС) — создаются и управляются ОС.
Каждому потоку выделяется кусок оперативной памяти, и стоимость создания потока высока.
Goroutines — легкие потоки управления, создаваемые языком Go.
Их исполнение мультиплексируется поверх небольшого количества настоящих потоков ОС, что сильно экономит ресурсы и позволяет запускать десятки тысяч gorutines.
Стоимость запуска:
Создание потока ОС требует сотни килобайт памяти и занимает значительное время.
Горутины инициализируются с небольшим стеком (~2KB) и начинают выполняться мгновенно.
Управление ресурсами:
Потоки ОС используют глобальное управление потоками, и каждое ядро ОС управляет своими собственными потоками независимо.
Goroutines управляются внутренним диспетчером Go runtime, который распределяет выполнение горутин среди имеющихся ядер и потоков.
Контекст переключений:
Переключение контекста между потоками ОС дорого обходится (сохранение регистров, обновление кэшей и т.д.).
Switching context в рамках горутин осуществляется намного эффективнее, так как они сами организуют собственную очередь выполнения.
Организация сетевого ввода-вывода в Go.
Сетевое взаимодействие в Go построено вокруг концепций неблокирующего ввода-вывода и синхронного/асинхронного выполнения операций:
Неблокирующий ввод-вывод (non-blocking I/O) — операции чтения и записи сети выполняются асинхронно через механизм неблокирующих сокетов, предоставляя эффективный механизм обработки множества соединений.
Синхронизация с помощью каналов (channels) — каналы обеспечивают удобный способ обмена данными между горутинами и синхронизируют доступ к общим ресурсам.
Модель событийной петли (event loop) — внутренний диспетчер (scheduler) управляет выполнением горутин и планирует выполнение операций в очередях событий.
Шаблоны буферизации и мультиплексинга — пакеты net/http, bufio, tcp, udp предоставляют удобные способы работы с соединениями и пакетами данных.
Типичная схема сетевого взаимодействия выглядит примерно так:
listener, _ := net.Listen("tcp", ":8080") // слушаем входящие соединения
for {
conn, _ := listener.Accept() // принимаем новое соединение
go handleConnection(conn) // отправляем в отдельную горутину
}
func handleConnection(conn net.Conn) {
buf := bufio.NewReader(conn) // создаем буферированный reader
for {
line, err := buf.ReadBytes('\n') // читаем строку из соединения
if err != nil {
break
}
processData(line) // обрабатываем полученные данные
}
}Такой подход позволяет создать эффективные, высокопроизводительные серверы, способные обслуживать большое количество клиентов одновременно.
#503_GO_ODP_Q28
Какие технологические недостатки есть у языка Go?
Для каких задач были бы полезны дженерики?
Как превратить []io.ReadWriter в []io.Reader?
Несмотря на ряд значительных преимуществ, язык Go имеет ограничения и слабые стороны, которые важно учитывать при выборе технологии для конкретного проекта:
Ограниченность функционала дженериков — до Go 1.18 (включительно) поддержка дженериков отсутствовала вовсе, что приводило к дублированию кода при обработке коллекций разнотипных данных.
После введения дженериков в Go 1.18 многие задачи стали решаться удобнее, но функционал дженериков ограничен по сравнению с аналогичными средствами в других языках (Java, C#), что иногда вызывает неудобства.
Отсутствие перегрузки методов и операторов — разработчик вынужден использовать разные имена для методов с одинаковой сигнатурой, что увеличивает объем кода и ухудшает читаемость.
Нет возможности переопределять операторы (+, -, *, /), что затрудняет реализацию высокооптимизированных математических моделей.
Недостаточность рефлексии (reflection) — отражение в Go ограничено возможностями статической проверки типов, что препятствует глубокому использованию мета-данных и динамических возможностей, присутствующих в других языках (например, Python, Ruby).
Система памяти (memory model) — концептуальная простота работы с памятью скрывает детали оптимизации, такие как pinning и garbage collector, что может привести к неоправданным потерям производительности в критичных приложениях.
Фиксированная структура пакета (vendor directory) — в ранних версиях Go возникали трудности с управлением зависимостями, хотя ситуация улучшилась с появлением модуля управления зависимостями (Go modules).
Неполноценная поддержка макросов и AOP (Aspect-Oriented Programming) — макросы отсутствуют в Go, что ограничивает применение техники аспектно-ориентированного программирования (AOP), удобной для внедрения сквозных механизмов логирования, безопасности и транзакционности.
Эти аспекты показывают, что Go не является панацеей и не решает абсолютно все задачи одинаково эффективно.
Где полезны дженерики (generics)?
Дженерики полезны в тех случаях, когда нужно написать общий алгоритм или структуру данных, работающую с различными типами данных:
Коллекции и контейнеры:
Дженерики облегчают написание списков, карт, деревьев и прочих структур данных, работающих с произвольными типами.
Алгоритмы обработки данных:
Алгоритмы сортировки, фильтрации, отображения данных (map/reduce) работают с несколькими типами данных, позволяя повторно использовать тот же самый код.
Интерфейсы и классы с общими операциями:
Можно создать обобщённые интерфейсы и абстрактные классы, которые используются разными типами данных.
До Go 1.18 приходилось либо копипастить код, либо применять рефлексию и преобразование типов через interface{}, что увеличивало накладные расходы и уменьшало безопасность.
Теперь дженерики решают проблему повторения однотипного кода, обеспечивая большую гибкость и улучшая качество кода.
Превращение []io.ReadWriter в []io.Reader
Часто возникает необходимость конвертации срезов интерфейсов одного типа в другой. Рассмотрим конкретный пример преобразования среза интерфейсов ReadWriter в срез интерфейсов Reader:
Объект io.ReadWriter реализует оба интерфейса: Reader и Writer.
Поскольку Reader является частью ReadWriter, его можно безопасно кастовать обратно к типу Reader, не теряя функциональность.
Такая техника конверсии полезна, когда нужно применить общие операции чтения данных к объектам, обладающим дополнительными возможностями (запись данных).
Какие технологические недостатки есть у языка Go?
Для каких задач были бы полезны дженерики?
Как превратить []io.ReadWriter в []io.Reader?
Несмотря на ряд значительных преимуществ, язык Go имеет ограничения и слабые стороны, которые важно учитывать при выборе технологии для конкретного проекта:
Ограниченность функционала дженериков — до Go 1.18 (включительно) поддержка дженериков отсутствовала вовсе, что приводило к дублированию кода при обработке коллекций разнотипных данных.
После введения дженериков в Go 1.18 многие задачи стали решаться удобнее, но функционал дженериков ограничен по сравнению с аналогичными средствами в других языках (Java, C#), что иногда вызывает неудобства.
Отсутствие перегрузки методов и операторов — разработчик вынужден использовать разные имена для методов с одинаковой сигнатурой, что увеличивает объем кода и ухудшает читаемость.
Нет возможности переопределять операторы (+, -, *, /), что затрудняет реализацию высокооптимизированных математических моделей.
Недостаточность рефлексии (reflection) — отражение в Go ограничено возможностями статической проверки типов, что препятствует глубокому использованию мета-данных и динамических возможностей, присутствующих в других языках (например, Python, Ruby).
Система памяти (memory model) — концептуальная простота работы с памятью скрывает детали оптимизации, такие как pinning и garbage collector, что может привести к неоправданным потерям производительности в критичных приложениях.
Фиксированная структура пакета (vendor directory) — в ранних версиях Go возникали трудности с управлением зависимостями, хотя ситуация улучшилась с появлением модуля управления зависимостями (Go modules).
Неполноценная поддержка макросов и AOP (Aspect-Oriented Programming) — макросы отсутствуют в Go, что ограничивает применение техники аспектно-ориентированного программирования (AOP), удобной для внедрения сквозных механизмов логирования, безопасности и транзакционности.
Эти аспекты показывают, что Go не является панацеей и не решает абсолютно все задачи одинаково эффективно.
Где полезны дженерики (generics)?
Дженерики полезны в тех случаях, когда нужно написать общий алгоритм или структуру данных, работающую с различными типами данных:
Коллекции и контейнеры:
Дженерики облегчают написание списков, карт, деревьев и прочих структур данных, работающих с произвольными типами.
Алгоритмы обработки данных:
Алгоритмы сортировки, фильтрации, отображения данных (map/reduce) работают с несколькими типами данных, позволяя повторно использовать тот же самый код.
Интерфейсы и классы с общими операциями:
Можно создать обобщённые интерфейсы и абстрактные классы, которые используются разными типами данных.
До Go 1.18 приходилось либо копипастить код, либо применять рефлексию и преобразование типов через interface{}, что увеличивало накладные расходы и уменьшало безопасность.
Теперь дженерики решают проблему повторения однотипного кода, обеспечивая большую гибкость и улучшая качество кода.
Превращение []io.ReadWriter в []io.Reader
Часто возникает необходимость конвертации срезов интерфейсов одного типа в другой. Рассмотрим конкретный пример преобразования среза интерфейсов ReadWriter в срез интерфейсов Reader:
package main
import (
"io"
"fmt"
)
func convertToReaders(rw []io.ReadWriter) []io.Reader {
readers := make([]io.Reader, len(rw)) // Создаем новый срез нужного размера
for i, rwItem := range rw {
readers[i] = io.Reader(rwItem) // Присваиваем интерфейс Reader каждому элементу
}
return readers
}
func main() {
rwSlice := []io.ReadWriter{ /* заполните необходимыми объектами */ }
readerSlice := convertToReaders(rwSlice)
fmt.Println(readerSlice)
}
Объект io.ReadWriter реализует оба интерфейса: Reader и Writer.
Поскольку Reader является частью ReadWriter, его можно безопасно кастовать обратно к типу Reader, не теряя функциональность.
Такая техника конверсии полезна, когда нужно применить общие операции чтения данных к объектам, обладающим дополнительными возможностями (запись данных).
#504_Cpp_IdPt
Рекомендации С.Мэйерса из книги (издательство ДМК 2006 г):
"Эффективное использование С++. 55 верных советов улучшить структуру и код ваших программ".
1. Относитесь к С++ как к конгломерату языков.
Чтобы увидеть смысл в С++, нужно распознавать его основные подъязыки:
— С. В глубине своей С++ все еще основан на С.
— Объектно-ориентированный С++.
— С++ с шаблонами.
— STL.
CЛЕДУЕТ ПОМНИТЬ:
2. Предпочитайте const, enum и inline использованию #define.
CЛЕДУЕТ ПОМНИТЬ:
3. Везде, где только можно, используйте const.
CЛЕДУЕТ ПОМНИТЬ:
4. Прежде чем использовать объекты, убедитесь, что они инициализированы.
CЛЕДУЕТ ПОМНИТЬ:
5. Какие функции С++ создает и вызывает молча?
CЛЕДУЕТ ПОМНИТЬ:
6. Явно запрещайте компилятору генерировать функции, которые вам не нужны.
CЛЕДУЕТ ПОМНИТЬ:
7. Объявляйте деструкторы виртуальными в полиморфном базовом классе.
CЛЕДУЕТ ПОМНИТЬ:
8. Не позволяйте исключениям покидать деструкторы.
CЛЕДУЕТ ПОМНИТЬ:
9. Никогда не вызывайте виртуальные функции в конструкторе
или деструкторе.
CЛЕДУЕТ ПОМНИТЬ:
10. Операторы присваивания должны возвращать ссылку на *this.
CЛЕДУЕТ ПОМНИТЬ:
.
Рекомендации С.Мэйерса из книги (издательство ДМК 2006 г):
"Эффективное использование С++. 55 верных советов улучшить структуру и код ваших программ".
1. Относитесь к С++ как к конгломерату языков.
Чтобы увидеть смысл в С++, нужно распознавать его основные подъязыки:
— С. В глубине своей С++ все еще основан на С.
— Объектно-ориентированный С++.
— С++ с шаблонами.
— STL.
CЛЕДУЕТ ПОМНИТЬ:
— Правила эффективного программирования меняются в зависимости от части С++, которую вы используете.
2. Предпочитайте const, enum и inline использованию #define.
CЛЕДУЕТ ПОМНИТЬ:
— Директиве #define следует предпочесть константные объекты и перечисления (enum).
— Вместо имитирующих функции макросов, определенных через #define, лучше применять встроенные функции (inline).
3. Везде, где только можно, используйте const.
CЛЕДУЕТ ПОМНИТЬ:
— Объявление чего-либо с модификатором const помогает компиляторам обнаруживать ошибки.
const можно использовать с объектами в любой области действия, с параметрами функций и возвращаемых значений, а также с функциями-членами в целом.
— Компиляторы проверяют побитовую константность, но вы должны программировать, применяя логическую константность.
— Когда константные и неконстантные функции-члены имеют, по сути, одинаковую реализацию, то дублирования кода можно избежать, заставив неконстантную версию вызывать константную.
4. Прежде чем использовать объекты, убедитесь, что они инициализированы.
CЛЕДУЕТ ПОМНИТЬ:
— Всегда вручную инициализируйте объекты встроенных типов, по
скольку С++ делает это не всегда.
— В конструкторе отдавать предпочтение применению списков инициализации членов перед прямым присваиванием значений в теле конструктора.
Перечисляйте данные-члены в списке инициализации в том же порядке, в каком они объявлены в классе.
— Избегайте проблем с порядком инициализации в разных единицах трансляции, заменяя нелокальные статические объекты локальными статическими объектами.
5. Какие функции С++ создает и вызывает молча?
CЛЕДУЕТ ПОМНИТЬ:
— Компилятор может неявно генерировать для класса конструктор по умолчанию, конструктор копирования, оператор присваивания и деструктор.
6. Явно запрещайте компилятору генерировать функции, которые вам не нужны.
CЛЕДУЕТ ПОМНИТЬ:
— Чтобы отключить функциональность, автоматически предоставляемую компилятором, объявите соответствующую функцию-член закрытой и не включайте ее реализацию.
Наследование базовому классу типа UncopyaЬle - один из способов сделать это.
— Начиная с версии С++11 конструктор копирования и оператор присваивания можно исключить с помощью слова delete
7. Объявляйте деструкторы виртуальными в полиморфном базовом классе.
CЛЕДУЕТ ПОМНИТЬ:
— Полиморфные базовые классы должны объявлять виртуальные деструкторы.
Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор.
— В классах, не предназначенных для использования в качестве базовых или для полиморфного применения, не следует объявлять виртуальные деструкторы.
8. Не позволяйте исключениям покидать деструкторы.
CЛЕДУЕТ ПОМНИТЬ:
— Деструкторы никогда не должны возбуждать исключений.
Если функция, вызываемая в деструкторе, может это сделать, то деструктор обязан перехватывать все исключения, а затем "проглатывать" их либо прерывать программу.
— Если клиенты класса нуждаются в возможности реагировать на исключения во время некоторой операции, то класс должен предоставить обычную функцию (то есть не деструктор), которая эту операцию выполнит.
9. Никогда не вызывайте виртуальные функции в конструкторе
или деструкторе.
CЛЕДУЕТ ПОМНИТЬ:
— Не вызывайте виртуальные функции во время работы конструкторов и деструкторов, потому что такие вызовы никогда не дойдут до производных классов, расположенных в иерархии наследования ниже того, который сейчас конструируется или уничтожается.
10. Операторы присваивания должны возвращать ссылку на *this.
CЛЕДУЕТ ПОМНИТЬ:
— Пишите операторы присваивания так, чтобы они возвращали ссылку на *this.
.
11. В operator= осуществляйте проверку на присваивание самому себе.
CЛЕДУЕТ ПОМНИТЬ:
12. Копируйте все части объекта.
CЛЕДУЕТ ПОМНИТЬ:
13. Используйте объекты для управления ресурсами.
CЛЕДУЕТ ПОМНИТЬ:
14. Тщательно продумывайте поведение при копировании классов,
управляющих ресурсами.
CЛЕДУЕТ ПОМНИТЬ:
15. Предоставляйте доступ к самим ресурсам из управляющих
ими классов.
CЛЕДУЕТ ПОМНИТЬ:
16. Используйте одинаковые формы new и delete.
CЛЕДУЕТ ПОМНИТЬ:
17. Помещение в "интеллектуальный" указатель объекта, выделенного с помощью new, лучше располагать в отдельном предложении.
CЛЕДУЕТ ПОМНИТЬ:
18. Проектируйте интерфейсы так, чтобы их легко было использовать правильно и трудно - неправильно.
CЛЕДУЕТ ПОМНИТЬ:
.
CЛЕДУЕТ ПОМНИТЬ:
— Убедитесь, что operator= правильно ведет себя, когда объект присваивается самому себе. Для этого можно сравнить адреса исходного и целевого объектов, аккуратно упорядочить предложения или применить идиому копирования обменом.
— Убедитесь, что все функции, оперирующие более чем одним объектом, ведут себя корректно при совпадении двух или более объектов.
12. Копируйте все части объекта.
CЛЕДУЕТ ПОМНИТЬ:
— Копирующие функции должны гарантировать копирование всех членов-данных объекта и частей его базовых классов.
— Не пытайтесь реализовать одну из копирующих функций в терминах другой. Вместо этого поместите общую функциональность в третью функцию, которую вызовут обе.
13. Используйте объекты для управления ресурсами.
CЛЕДУЕТ ПОМНИТЬ:
— Чтобы предотвратить утечку ресурсов, используйте объекты RAII, которые захватывают ресурсы в своих конструкторах и освобождают в деструкторах.
— Два часто используемых класса RAII - это tr1::shared_ptr и auto_ptr.
Обычно лучше остановить выбор на классе tr1::shared_ptr, потому что его поведение при копировании соответствует интуитивным ожиданиям.
Что касается auto_ptr, то после копирования он уже не указывает ни на какой объект.
14. Тщательно продумывайте поведение при копировании классов,
управляющих ресурсами.
CЛЕДУЕТ ПОМНИТЬ:
— Копирование RАII-объектов влечет за собой копирование ресурсов, которыми они управляют, поэтому поведение ресурса при копировании определяет поведение RАII-объекта.
— Обычно при реализации RАII-классов применяется одна из двух схем: запрет копирования или подсчет ссылок, но возможны и другие варианты.
15. Предоставляйте доступ к самим ресурсам из управляющих
ими классов.
CЛЕДУЕТ ПОМНИТЬ:
— Программные интерфейсы (API) часто требуют прямого обращения к ресурсам. Именно поэтому каждый RAII-клacc должен предоставлять возможность получения доступа к ресурсу, которым он управляет.
— Доступ может быть обеспечен посредством явного либо неявного преобразования. Вообще говоря, явное преобразование безопаснее, но не явное более удобно для пользователей.
16. Используйте одинаковые формы new и delete.
CЛЕДУЕТ ПОМНИТЬ:
— Если вы используете [] в выражении new, то должны применять [] и в соответствующем выражении delete. Если вы не используете квадратные скобки [] в выражении new, то не должны использовать их и в соответствующем выражении delete.
17. Помещение в "интеллектуальный" указатель объекта, выделенного с помощью new, лучше располагать в отдельном предложении.
CЛЕДУЕТ ПОМНИТЬ:
— Помещайте объекты, выделенные оператором new, в "интеллектуальные" указатели в отдельном предложении.
В противном случае такие вызовы могут привести к утечкам ресурсов, если возникнет исключение.
18. Проектируйте интерфейсы так, чтобы их легко было использовать правильно и трудно - неправильно.
CЛЕДУЕТ ПОМНИТЬ:
— Хорошие интерфейсы легко использовать правильно и трудно использовать неправильно.
Нужно стремиться обеспечить эти характеристики в интерфейсах.
— Для обеспечения корректного использования интерфейсы должны быть согласованы и совместимы со встроенными типами.
— Для предотвращения ошибок применяют следующие способы:
- создание новых типов;
- ограничение допустимых операций над этими типами;
- ограничение допустимых значений;
- освобождение пользователя от обязанностей по управлению ресурсами.
— Класс tr1::shared_ptr поддерживает пользовательские функции-чистильщики. Это снимает "проблему нескольких DLL" и может быть использовано для автоматического освобождения мьютекса (см. правило 14).
.
19. Рассматривайте проектирование класса как проектирование типа.
Проектирование почти любого класса ставит перед разработчиком вопросы, ответы на которые часто ограничивают спектр возможных решений:
— Как должны создаваться и уничтожаться объекты нового типа?
От ответа на этот вопрос зависит дизайн конструкторов и деструкторов, а равно функций распределения и освобождения памяти (оператор new, оператор пеw[], оператор delete и оператор delete[]), если вы собираетесь их переопределить.
— Чем должна отличаться инициализация объекта от присваивания значе ний?
Ответ на этот вопрос определяет разницу в поведении между конструкторами и операторами присваивания.
Важно не путать инициализацию с присваиванием, потому что им соответствуют разные вызовы функций (см. правило 4).
— Что означает для объектов нового типа быть переданными по значению?
Помните, что конструктор копирования определяет реализацию передачи по значению для данного типа.
— Каковы ограничения на допустимые значения вашего нового типа?
Обычно только некоторые комбинации значений данных-членов класса являются правильными.
Эти комбинации определяют инварианты, которые должен поддерживать класс.
А инварианты уже диктуют, как следует контролировать ошибки в функциях-членах, в особенности в конструкторах, операторах присваивания и функциях установки значений ("setter" functions).
Могут быть также затронуты исключения, которые возбуждают ваши функции, и спецификации этих исключений.
— Укладьmается ли ваш новыii тип в граф наследования?
Наследуя классы от других, нужно следовать ограничениям, налагаемым базовыми классами.
В частности, нужно учитывать, как объявлены в них функции-члены: виртуальньши или нет (см. правила 34 и 36).
Если необходимо, чтобы класс могли наследовать другие, то нужно тщательно продумать, какие функции объявить виртуальны.ми; в особенности это относится к деструктору (правило 7).
— Какие варианты преобразования типов допустимы для нового типа?
Новый тип существует в море других типов, поэтому должны ли быть предусмотрены варианты преобразования между проектируемым типом и другими?
Если необходимо разрешить неявное преобразование объекта типа Т1 в объект типа Т2, придется либо написать функцию преобразования в классе Т1 (то есть орегаtог Т2), либо неявный конструктор в классе Т2, который может быть вызван с единственным аргументом.
Если же необходимо разрешить только явные преобразования, то нужно будет написать специальные функции, но ни в коем случае не делать их операторами преобразования или не-explicit конструкторами с одним аргументом.
— Какие операторы и функции имеют смысл для нового тнпа?
Ответ на этот вопрос определяет набор функций, которые необходимо объявить в новом классе. Некоторые из них будут функциями-членами, другие - нет (см. правила 23, 24 и 46).
— Какие стандартные функции должны стать недоступными?
Их нужно будет объявить, закрытыми (правило 6).
— Кто должен получить доступ к членам вашего нового типа?
Ответ на этот вопрос помогает определить, какие члены должны быть открытыми (public), какие — защищенными (protected) и какие — закрытыми (private).
Также преlстоит решить, какие классы и/или функции должны быть
друзьями класса, а также когда имеет смысл вложить один класс внутрь другого.
— Что такое "необъявленный иmерфейс" нового типа?
Какого рода гарантии мoryт быть предоставлены относительно производительности, безопасности относительно исключений (см. правило 29) и использования ресурсов (например, блокировок и динамической памяти)?
Такого рода гарантии определяют ограничения на реализацию нового класса.
— Насколько общий новый тип?
Возможно, в действительности не нужно определять новый тип.
Возможно, необходимо определить целое семейство типов. Если это так, то нужно определять не новый класс, а новый шаблон классa.
Проектирование почти любого класса ставит перед разработчиком вопросы, ответы на которые часто ограничивают спектр возможных решений:
— Как должны создаваться и уничтожаться объекты нового типа?
От ответа на этот вопрос зависит дизайн конструкторов и деструкторов, а равно функций распределения и освобождения памяти (оператор new, оператор пеw[], оператор delete и оператор delete[]), если вы собираетесь их переопределить.
— Чем должна отличаться инициализация объекта от присваивания значе ний?
Ответ на этот вопрос определяет разницу в поведении между конструкторами и операторами присваивания.
Важно не путать инициализацию с присваиванием, потому что им соответствуют разные вызовы функций (см. правило 4).
— Что означает для объектов нового типа быть переданными по значению?
Помните, что конструктор копирования определяет реализацию передачи по значению для данного типа.
— Каковы ограничения на допустимые значения вашего нового типа?
Обычно только некоторые комбинации значений данных-членов класса являются правильными.
Эти комбинации определяют инварианты, которые должен поддерживать класс.
А инварианты уже диктуют, как следует контролировать ошибки в функциях-членах, в особенности в конструкторах, операторах присваивания и функциях установки значений ("setter" functions).
Могут быть также затронуты исключения, которые возбуждают ваши функции, и спецификации этих исключений.
— Укладьmается ли ваш новыii тип в граф наследования?
Наследуя классы от других, нужно следовать ограничениям, налагаемым базовыми классами.
В частности, нужно учитывать, как объявлены в них функции-члены: виртуальньши или нет (см. правила 34 и 36).
Если необходимо, чтобы класс могли наследовать другие, то нужно тщательно продумать, какие функции объявить виртуальны.ми; в особенности это относится к деструктору (правило 7).
— Какие варианты преобразования типов допустимы для нового типа?
Новый тип существует в море других типов, поэтому должны ли быть предусмотрены варианты преобразования между проектируемым типом и другими?
Если необходимо разрешить неявное преобразование объекта типа Т1 в объект типа Т2, придется либо написать функцию преобразования в классе Т1 (то есть орегаtог Т2), либо неявный конструктор в классе Т2, который может быть вызван с единственным аргументом.
Если же необходимо разрешить только явные преобразования, то нужно будет написать специальные функции, но ни в коем случае не делать их операторами преобразования или не-explicit конструкторами с одним аргументом.
— Какие операторы и функции имеют смысл для нового тнпа?
Ответ на этот вопрос определяет набор функций, которые необходимо объявить в новом классе. Некоторые из них будут функциями-членами, другие - нет (см. правила 23, 24 и 46).
— Какие стандартные функции должны стать недоступными?
Их нужно будет объявить, закрытыми (правило 6).
— Кто должен получить доступ к членам вашего нового типа?
Ответ на этот вопрос помогает определить, какие члены должны быть открытыми (public), какие — защищенными (protected) и какие — закрытыми (private).
Также преlстоит решить, какие классы и/или функции должны быть
друзьями класса, а также когда имеет смысл вложить один класс внутрь другого.
— Что такое "необъявленный иmерфейс" нового типа?
Какого рода гарантии мoryт быть предоставлены относительно производительности, безопасности относительно исключений (см. правило 29) и использования ресурсов (например, блокировок и динамической памяти)?
Такого рода гарантии определяют ограничения на реализацию нового класса.
— Насколько общий новый тип?
Возможно, в действительности не нужно определять новый тип.
Возможно, необходимо определить целое семейство типов. Если это так, то нужно определять не новый класс, а новый шаблон классa.
— Действительно ли новый тип представляет собой то, что нужно?
Если определение нового производного класса только расширяет функциональность существующего класса, возможно, этой цели
лучше достичь простым определением одной или более функций-нечленов либо шаблонов.
CЛЕДУЕТ ПОМНИТЬ:
20. Предпочитайте передачу по ссылке на const передаче по значению.
CЛЕДУЕТ ПОМНИТЬ:
21. Не пытайтесь вернуть ссылку, когда должны вернуть объект.
CЛЕДУЕТ ПОМНИТЬ:
22. Объявляйте данные-члены закрытыми.
CЛЕДУЕТ ПОМНИТЬ:
23. Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса.
CЛЕДУЕТ ПОМНИТЬ:
24. Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам.
CЛЕДУЕТ ПОМНИТЬ:
25. Подумайте о поддержке функции swap, не возбуждающей исключений.
CЛЕДУЕТ ПОМНИТЬ:
26. Откладывайте определение переменных насколько возможно.
CЛЕДУЕТ ПОМНИТЬ:
27. Не зпоупотребпяйте приведением типов.
CЛЕДУЕТ ПОМНИТЬ:
28. Избегайте возвращения "дескрипторов" внуrренних данных.
CЛЕДУЕТ ПОМНИТЬ:
.
Если определение нового производного класса только расширяет функциональность существующего класса, возможно, этой цели
лучше достичь простым определением одной или более функций-нечленов либо шаблонов.
CЛЕДУЕТ ПОМНИТЬ:
— Проектирование класса - это проектирование типа.
Прежде чем определять новый тип, убедитесь, что рассмотрены все вопросы, которые обсуждаются в настоящем правиле.
20. Предпочитайте передачу по ссылке на const передаче по значению.
CЛЕДУЕТ ПОМНИТЬ:
— Передаче по значению предпочитайте передачу по ссылке на константу.
Обычно это более эффективно и позволяет избежать проблемы "срезки".
— Это правило не касается встроенных типов, итераторов и функциональных объектов STL.
Для них передача по значению обычно подходит больше.
21. Не пытайтесь вернуть ссылку, когда должны вернуть объект.
CЛЕДУЕТ ПОМНИТЬ:
— Никогда не возвращайте указатель или ссылку на локальный объект, ссылку на объект, распределенный в "куче", либо указатель или ссылку на локальный статический объект, если есть шанс, что понадобится более, чем один экземпляр такого объекта. (в правиле 4 приведен пример ситуации, когда возврат ссылки на локальный статический объект имеет смысл, по крайней мере, в однопоточных средах).
22. Объявляйте данные-члены закрытыми.
CЛЕДУЕТ ПОМНИТЬ:
— Объявляйте данные-члены закрытыми (private).
Это дает клиентам синтаксически однородный доступ к данным, обеспечивает возможность тонкого управления доступом, позволяет гарантировать инвариантность и предоставляет авторам рсализации классов гибкость.
— Защищенные члены не более инкапсулированы, чем открытые.
23. Предпочитайте функциям-членам функции, не являющиеся ни членами, ни друзьями класса.
CЛЕДУЕТ ПОМНИТЬ:
— Предпочитайте функциям-членам функции, не яnляющиеся ни членами, ни друзьями класса.
Это повышает степень инкапсулянии н расширяемости, а также гибкость "упаковки" функциональности.
24. Объявляйте функции, не являющиеся членами, когда преобразование типов должно быть применимо ко всем параметрам.
CЛЕДУЕТ ПОМНИТЬ:
— Если преобразование типов должно быть применимо ко всем параметрам функции (включая и скрытый параметр this), то функция не должна быть членом класса.
25. Подумайте о поддержке функции swap, не возбуждающей исключений.
CЛЕДУЕТ ПОМНИТЬ:
— Предоставьте функцию-член swap, если std::swap работает с новым типом неэффективно.
Убедитесь, что она не возбуждает исключений.
— Если вы предоставляете функцию-член swap, то также предоставьте свободную функцию, вызывающую функцию-член. Для классов (не шаблонов) специализируйте также std::swap.
— Когда вызывается swap, используйте using-объявление, вводящее std::swap в область видимости, и вызывайте swap без квалификатора пространства имен.
— Допускается предоставление полной специализации шаблонов, находящихся в пространстве имен std, для пользовательских типов, но никогда не нытайтссь добавить в пространство std что-либо новое.
26. Откладывайте определение переменных насколько возможно.
CЛЕДУЕТ ПОМНИТЬ:
— Откладывайте определение переменных насколько возможно. Это делает программы яснее и повышает их эффективность.
27. Не зпоупотребпяйте приведением типов.
CЛЕДУЕТ ПОМНИТЬ:
— Избегайте насколько возможно приведений типов, особенно dynamic_cast, в критичном по производительности коде.
Если дизайн требует приведения, попытайтесь разработать альтернативу, где такой необходимости не возникает.
— Когда приведение типа необходимо, постарайтесь скрыть его внутри функции. Тогда пользователи смогут вызывать эту функцию вместо помещения приведения в их собственный код.
— Предпочитайте приведения в стиле С++ старому стилю. Их легче увидеть, и они более избирательны.
28. Избегайте возвращения "дескрипторов" внуrренних данных.
CЛЕДУЕТ ПОМНИТЬ:
— Избегайте возвращать «дескрипторы» (ссылки, указатели, итераторы) внутренних данных объекта.
Это повышает степень инкапсуляции, помогает константным функциям-членам быть константными и минимизирует вероятность появления «висячих дескрипторов».
.
29. Стремитесь, чтобы программа была безопасна относительно исключений.
CЛЕДУЕТ ПОМНИТЬ:
30. Тщательно обдумывайте использование встроенных функций.
CЛЕДУЕТ ПОМНИТЬ:
31. Уменьшайте зависимости файлов при компиляции.
CЛЕДУЕТ ПОМНИТЬ:
32. Используйте открытое наследование для моделирования отношения «является».
CЛЕДУЕТ ПОМНИТЬ:
33. Не скрывайте унаследованные имена.
CЛЕДУЕТ ПОМНИТЬ:
34. Различайте наследование интерфейса и наследование реализации.
CЛЕДУЕТ ПОМНИТЬ:
35. Рассмотрите альтернативы виртуальным функциям.
CЛЕДУЕТ ПОМНИТЬ:
36. Никогда не переопределяйте наследуемые невирrуальные функции.
CЛЕДУЕТ ПОМНИТЬ:
37. Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию.
CЛЕДУЕТ ПОМНИТЬ:
38. Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции.
CЛЕДУЕТ ПОМНИТЬ:
CЛЕДУЕТ ПОМНИТЬ:
— Безопасные относительно исключений функции не допускают утечки ресурсов и повреждения структур данных, даже в случае возбуждения исключений.
Такие функции предоставляют базовую гарантию, строгую гарантию либо гарантию полного отсутствия исключений.
— Строгая гарантия часто может быть реализована посредством копирования и обмена, но предоставлять ее для всех функций непрактично.
— Функция обычно может предоставить гарантию не строже, чем самая слабая гарантия, обеспечиваемая вызываемыми из нее функциями.
30. Тщательно обдумывайте использование встроенных функций.
CЛЕДУЕТ ПОМНИТЬ:
— Делайте встраиваемыми только небольшие, часто вызываемые функции.
Это облегчит отладку, даст возможность выполнять обновления библиотек на двоичном уровне, уменьшит эффект "разбухания" кода и поможет повысить быстродействие программы.
— Не объявляйте шаблоны функций встроенными только потому, что они появляются в заголовочных файлах.
31. Уменьшайте зависимости файлов при компиляции.
CЛЕДУЕТ ПОМНИТЬ:
— Основная идея уменьшения зависимостей на этапе компиляции состоит в том, чтобы заменить зависимость от определения зависимостью от объявления.
Эта идея лежит в основе двух подходов: классов-дескрипторов и интерфейсных классов.
— Заголовочные файлы библиотек должны существовать в обеих формах: полной и содержащей только объявления.
Это справедливо независимо от того, включают они шаблоны или нет.
32. Используйте открытое наследование для моделирования отношения «является».
CЛЕДУЕТ ПОМНИТЬ:
— Открытое наследование означает «является». Все, что применимо к базовому классу, должно быть применимо также и к производным от него классам, потому что каждый объект производного класса является также объектом базового класса.
33. Не скрывайте унаследованные имена.
CЛЕДУЕТ ПОМНИТЬ:
— Имена в производных классах скрывают имена из базовых классов.
При открытом наследовании это всегда нежелательно.
— Чтобы сделать скрытые имена видимыми, используйте using-объявления либо перенаправляющие функции.
34. Различайте наследование интерфейса и наследование реализации.
CЛЕДУЕТ ПОМНИТЬ:
— Наследование интерфейса отличается от наследования реализации.
При открытом наследовании производные классы всегда наследуют интерфейсы базовых классов.
— Чисто виртуальные функции означают, что наследуется только интерфейс.
— Обычные виртуальные функции означают, что наследуются интерфейс и реализация по умолчанию.
— Невиртуальные функции означают, что наследуются интерфейс и обязательная реализация.
35. Рассмотрите альтернативы виртуальным функциям.
CЛЕДУЕТ ПОМНИТЬ:
— К числу альтернатив виртуальным функциям относятся идиома NVI и различные формы паттерна проектирования "Стратегия".
Идиома NVI сама по себе - это пример реализации паттерна "Шаблонный Метод".
— Недостаток переноса функциональности из функций-членов вовне класса заключается в том, что функциям-нечленам недостает прав доступа к закрытым членам класса.
— Объекты tr1::function работают как обобщенные указатели на функции.
Такие объекты поддерживают все вызываемые сущности, совместимые с сигнатурой целевой функции.
36. Никогда не переопределяйте наследуемые невирrуальные функции.
CЛЕДУЕТ ПОМНИТЬ:
— Никогда не переопределяйте наследуемые невиртуальные функции.
37. Никогда не переопределяйте наследуемое значение аргумента функции по умолчанию.
CЛЕДУЕТ ПОМНИТЬ:
— Никогда не переопределяйте наследуемые значения аргументов по умолчанию, потому что аргументы по умолчанию связываются статически, тогда как виртуальные функции (а только их и можно переопределять) динамически.
38. Моделируйте отношение «содержит» или «реализуется посредством» с помощью композиции.
CЛЕДУЕТ ПОМНИТЬ:
— Семантика композиции кардинально отличается от семантики открытого наследования.
— В предметной области композиция означает "содержит".
В области реализации она означает "реализовано посредством".