<?xml version="1.0"?>
<rss version="2.0"><channel><title>Articles: IThub &#x421;&#x442;&#x430;&#x442;&#x44C;&#x438; &#x43F;&#x43E; &#x43F;&#x440;&#x43E;&#x433;&#x440;&#x430;&#x43C;&#x43C;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x44E;</title><link>https://ithub.uno/statiarticles/1_articles/?d=1</link><description>Articles: IThub &#x421;&#x442;&#x430;&#x442;&#x44C;&#x438; &#x43F;&#x43E; &#x43F;&#x440;&#x43E;&#x433;&#x440;&#x430;&#x43C;&#x43C;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x44E;</description><language>en</language><item><title>&#x41E;&#x41E;&#x41F; &#x434;&#x43B;&#x44F; &#x412;&#x441;&#x435;&#x445;! &#x418;&#x43B;&#x438; &#x43A;&#x430;&#x43A; &#x43F;&#x435;&#x440;&#x435;&#x441;&#x442;&#x430;&#x442;&#x44C; &#x431;&#x43E;&#x44F;&#x442;&#x44C;&#x441;&#x44F; &#x43A;&#x43B;&#x430;&#x441;&#x441;&#x43E;&#x432; &#x438; &#x43D;&#x430;&#x447;&#x430;&#x442;&#x44C; &#x43F;&#x43E;&#x43B;&#x443;&#x447;&#x430;&#x442;&#x44C; &#x43E;&#x442; &#x43D;&#x438;&#x445; &#x443;&#x434;&#x43E;&#x432;&#x43E;&#x43B;&#x44C;&#x441;&#x442;&#x432;&#x438;&#x435;.</title><link>https://ithub.uno/statiarticles/1_articles/%D0%BE%D0%BE%D0%BF-%D0%B4%D0%BB%D1%8F-%D0%B2%D1%81%D0%B5%D1%85-%D0%B8%D0%BB%D0%B8-%D0%BA%D0%B0%D0%BA-%D0%BF%D0%B5%D1%80%D0%B5%D1%81%D1%82%D0%B0%D1%82%D1%8C-%D0%B1%D0%BE%D1%8F%D1%82%D1%8C%D1%81%D1%8F-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%BE%D0%B2-%D0%B8-%D0%BD%D0%B0%D1%87%D0%B0%D1%82%D1%8C-%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B0%D1%82%D1%8C-%D0%BE%D1%82-%D0%BD%D0%B8%D1%85-%D1%83%D0%B4%D0%BE%D0%B2%D0%BE%D0%BB%D1%8C%D1%81%D1%82%D0%B2%D0%B8%D0%B5-r2/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_01/scale_1200.webp.839f0521732f07e6da2021976e8db6e1.webp" /></p>
<p></p><h2>Введение: зачем вообще это ваше ООП?</h2><p>Если ты только начинаешь путь программиста, то, скорее всего, уже слышал загадочные слова: <strong>ООП</strong>, <strong>классы</strong>, <strong>объекты</strong>, <strong>инкапсуляция</strong>, <strong>наследование</strong>… В этот момент многие новички испытывают лёгкую панику и желание вернуться к <code>echo "Hello, world";</code>.</p><p>Спокойно. Ты не один. ООП — это не магия и не заговор сеньоров. Это всего лишь <strong>способ думать о программе</strong>, чтобы:</p><ul><li><p>код был понятнее;</p></li><li><p>код было легче расширять;</p></li><li><p>код было не стыдно показать другим (и себе через полгода).</p></li></ul><p>Сегодня мы разберём ООП <strong>от и до</strong>, <strong>простым человеческим языком</strong>, с примерами на <strong>PHP</strong>, с шутками и жизненными аналогиями.</p><hr><h2>Что такое ООП простыми словами</h2><p><strong>ООП (объектно-ориентированное программирование)</strong> — это подход, при котором программа состоит из <strong>объектов</strong>, которые:</p><ul><li><p>имеют <strong>данные</strong> (состояние);</p></li><li><p>умеют что‑то <strong>делать</strong> (поведение);</p></li><li><p>общаются друг с другом.</p></li></ul><h3>Аналогия из жизни (без неё никак)</h3><p>Представь, что ты играешь в RPG.</p><ul><li><p>Персонаж — это <strong>объект</strong></p></li><li><p>Класс персонажа (воин, маг) — это <strong>класс</strong></p></li><li><p>Здоровье, мана, сила — это <strong>свойства</strong></p></li><li><p>Ударить мечом, кастануть огненный шар — это <strong>методы</strong></p></li></ul><p>Примерно так же всё работает и в коде.</p><hr><h2>Класс — это чертёж</h2><p><strong>Класс</strong> — это описание того, <em>каким должен быть объект</em>.</p><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Класс — как чертёж дома. По одному чертежу можно построить много одинаковых домов.</p></div></blockquote><h3>Пример класса в PHP</h3><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>class Car {
    public string $brand;
    public string $color;

    public function drive() {
        echo "Машина едет";
    }
}
</code></pre><p>Здесь мы описали:</p><ul><li><p>что у машины есть <strong>бренд</strong> и <strong>цвет</strong>;</p></li><li><p>что машина умеет <strong>ехать</strong>.</p></li></ul><hr><h2>Объект — это конкретная вещь</h2><p><strong>Объект</strong> — это экземпляр класса. То есть реальная штука, созданная по чертежу.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$car1 = new Car();
$car1-&gt;brand = 'BMW';
$car1-&gt;color = 'black';

$car1-&gt;drive();
</code></pre><p>Теперь:</p><ul><li><p><code>$car1</code> — это конкретная машина;</p></li><li><p>у неё есть бренд BMW;</p></li><li><p>она умеет ехать (и едет).</p></li></ul><p>Можно создать хоть тысячу машин:</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$car2 = new Car();
$car2-&gt;brand = 'Toyota';
$car2-&gt;color = 'white';
</code></pre><hr><h2>Свойства и методы</h2><h3>Свойства</h3><p><strong>Свойства</strong> — это данные объекта.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>public string $brand;
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Проще говоря: <em>что объект знает о себе</em>.</p></div></blockquote><h3>Методы</h3><p><strong>Методы</strong> — это действия объекта.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>public function drive() {
    echo "Машина едет";
}
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Проще говоря: <em>что объект умеет делать</em>.</p></div></blockquote><hr><h2>Конструктор: момент рождения объекта</h2><p>Когда объект создаётся, мы часто хотим сразу задать ему начальные данные.</p><p>Для этого есть <strong>конструктор</strong>.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>class Car {
    public string $brand;
    public string $color;

    public function __construct(string $brand, string $color) {
        $this-&gt;brand = $brand;
        $this-&gt;color = $color;
    }
}
</code></pre><p>Использование:</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$car = new Car('Audi', 'red');
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p><code>$this</code> — это ссылка на <strong>текущий объект</strong>. Типа «я сам».</p></div></blockquote><hr><h2>Инкапсуляция: не трогай, сломается</h2><p><strong>Инкапсуляция</strong> — это принцип, который говорит:</p><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Внутренности объекта должны быть скрыты от внешнего мира.</p></div></blockquote><h3>Зачем это нужно?</h3><p>Чтобы:</p><ul><li><p>объект нельзя было сломать случайно;</p></li><li><p>данные изменялись только правильным способом.</p></li></ul><h3>Модификаторы доступа</h3><p>В PHP есть три основных:</p><ul><li><p><code>public</code> — доступно всем</p></li><li><p><code>protected</code> — доступно классу и наследникам</p></li><li><p><code>private</code> — доступно только внутри класса</p></li></ul><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>class BankAccount {
    private int $balance = 0;

    public function deposit(int $amount) {
        $this-&gt;balance += $amount;
    }

    public function getBalance(): int {
        return $this-&gt;balance;
    }
}
</code></pre><p>Теперь нельзя сделать так:</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$account-&gt;balance = 1000000; // </code></pre><p><code><span class="tmiEmoji" title="">❌</span></code></p><p><code>нельзя</code></p><p>И это хорошо. Банк доволен. Ты доволен. Мир стабилен.</p><hr><h2>Наследование: не изобретай велосипед</h2><p><strong>Наследование</strong> позволяет создавать новые классы на основе существующих.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>class Animal {
    public function speak() {
        echo "Животное издаёт звук";
    }
}

class Dog extends Animal {
    public function speak() {
        echo "Гав!";
    }
}
</code></pre><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$dog = new Dog();
$dog-&gt;speak(); // Гав!
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Собака — это животное, но с уточнениями.</p></div></blockquote><hr><h2>Полиморфизм: один интерфейс — разное поведение</h2><p><strong>Полиморфизм</strong> — это когда разные объекты могут отвечать на один и тот же вызов по‑разному.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>class Cat extends Animal {
    public function speak() {
        echo "Мяу";
    }
}
</code></pre><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>$animals = [new Dog(), new Cat()];

foreach ($animals as $animal) {
    $animal-&gt;speak();
}
</code></pre><p>Результат:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Гав!
Мяу
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Один метод — разное поведение. Магия? Нет, полиморфизм.</p></div></blockquote><hr><h2>Абстракция: только главное</h2><p><strong>Абстракция</strong> — это когда мы описываем <em>что объект должен делать</em>, но не <em>как именно</em>.</p><pre spellcheck="" class="tmiCode language-php" data-language="PHP"><code>abstract class Shape {
    abstract public function getArea(): float;
}

class Square extends Shape {
    public function __construct(private float $side) {}

    public function getArea(): float {
        return $this-&gt;side ** 2;
    }
}
</code></pre><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>Абстрактный класс — это как ТЗ от заказчика: «сделай», а как — твоя проблема.</p></div></blockquote><hr><h2>Итог: вся суть ООП в одном месте</h2><p>ООП держится на четырёх китах:</p><ol><li><p><strong>Инкапсуляция</strong> — скрываем лишнее</p></li><li><p><strong>Наследование</strong> — переиспользуем код</p></li><li><p><strong>Полиморфизм</strong> — один интерфейс, разное поведение</p></li><li><p><strong>Абстракция</strong> — работаем с сутью, а не деталями</p></li></ol><p>Если ты понял это — поздравляю, ты понял ООП.</p><hr><h2>Напоследок</h2><p>ООП — это не цель, а инструмент. Он не делает код автоматически хорошим, но <strong>помогает писать его осознанно</strong>.</p><p>Если после этой статьи ты:</p><ul><li><p>не боишься слова «класс»;</p></li><li><p>понимаешь, зачем нужны объекты;</p></li><li><p>можешь объяснить ООП другу на кухне;</p></li></ul><p>значит, всё получилось.</p><p><em>А если нет — перечитай ещё раз. Программисты так делают постоянно.</em></p><p>Удачи в коде и поменьше фатальных ошибок.</p>]]></description><guid isPermaLink="false">2</guid><pubDate>Mon, 12 Jan 2026 23:25:00 +0000</pubDate></item><item><title>&#x41E;&#x431;&#x44A;&#x435;&#x43A;&#x442;&#x43D;&#x43E;-&#x43E;&#x440;&#x438;&#x435;&#x43D;&#x442;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x43D;&#x43E;&#x435; &#x43F;&#x440;&#x43E;&#x433;&#x440;&#x430;&#x43C;&#x43C;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435; &#x432; C: &#x437;&#x430;&#x447;&#x435;&#x43C; &#x438; &#x43A;&#x430;&#x43A; &#x440;&#x435;&#x430;&#x43B;&#x438;&#x437;&#x43E;&#x432;&#x430;&#x442;&#x44C;</title><link>https://ithub.uno/statiarticles/1_articles/%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B2-c-%D0%B7%D0%B0%D1%87%D0%B5%D0%BC-%D0%B8-%D0%BA%D0%B0%D0%BA-%D1%80%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D1%8C-r6/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_01/b7f531d8-8f4f-4b8c-b8e8-ba3831966216.webp.fc53e131d4b3d29b7477a37ea9b3c03a.webp" /></p>
<p>Объектно-ориентированное программирование (ООП) — это <strong>парадигма разработки программного обеспечения</strong>, в которой программная система строится из «объектов» — сущностей, объединяющих данные и операции над ними. В отличие от чисто процедурного стиля, где функции и данные существуют отдельно, ООП помогает моделировать предметную область ближе к реальности, улучшает модульность, повторное использование и поддержку кода.Хотя язык <strong>С (ANSI C)</strong> исторически не является объектно-ориентированным, он обладает инструментами, которые позволяют <strong>эмулировать многие свойства ООП</strong> (такие как инкапсуляция, полиморфизм и частично наследование) с помощью структур, указателей и функций.</p><hr><p> </p><h2>Зачем нам применять ООП в C</h2><p> </p><p>ООП дает следующие важные преимущества:</p><ul><li><p><strong>Модульность и инкапсуляция.</strong> Код структурируется вокруг объектов, а не процедур, что облегчает разделение ответственности и управление сложностью.</p></li></ul><p><strong>Повторное использование компонентов.</strong> Объекты и функции, работающие с ними, можно многократно использовать в разных частях программы.</p><p><strong>Упрощённая поддержка.</strong> В больших проектах изменения в одном объекте не требуют полномасштабной переработки остального кода.</p><p><strong>Чёткие интерфейсы.</strong> Абстрагирование внутренних деталей объектов делает использование API понятным и безопасным.</p><p>Тем не менее, реализация ООП в C требует <strong>ручного управления памятью и структурой</strong>, а также дополнительных усилий для имитации механизмов, которые в других языках реализованы на уровне компилятора.</p><hr><p> </p><h2>Основная идея: структура плюс функции</h2><p> </p><p>В традиционных объектно-ориентированных языках (например, Java или C++) класс содержит <strong>поля (состояние)</strong> и <strong>методы (поведение)</strong>. В C аналог класса достигается через: </p><ul><li><p><code>struct</code> — описание набора данных, представляющего объект.</p><p></p></li></ul><p><strong>Функции</strong>, принимающие указатель на <code>struct</code> — имитация методов, которые работают с объектом</p><p><strong>Указатели на функции в структуре</strong> — позволяют эмулировать полиморфизм и виртуальные методы.</p><hr><p> </p><h2>Простой пример: объект «Point»</h2><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>/* point.h */
#ifndef POINT_H
#define POINT_H

typedef struct Point Point;

/* Конструктор */
Point* Point_new(int x, int y);

/* «Методы» */
void Point_move(Point *self, int dx, int dy);
void Point_print(const Point *self);

/* Деструктор */
void Point_delete(Point *self);

#endif
</code></pre><p> </p><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>/* point.c */
#include "point.h"
#include &lt;stdlib.h&gt;
#include &lt;stdio.h&gt;

struct Point {
    int x;
    int y;
};

Point* Point_new(int x, int y) {
    Point *p = malloc(sizeof(Point));
    if (p) {
        p-&gt;x = x;
        p-&gt;y = y;
    }
    return p;
}

void Point_move(Point *self, int dx, int dy) {
    if (self) {
        self-&gt;x += dx;
        self-&gt;y += dy;
    }
}

void Point_print(const Point *self) {
    if (self) {
        printf("Point at (%d, %d)\n", self-&gt;x, self-&gt;y);
    }
}

void Point_delete(Point *self) {
    free(self);
}
</code></pre><p> </p><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>/* main.c */
#include "point.h"

int main(void) {
    Point *p = Point_new(10, 20);
    Point_print(p);
    Point_move(p, 5, -3);
    Point_print(p);
    Point_delete(p);
    return 0;
}
</code></pre><p> </p><p>В этом примере структура <code>Point</code> представляет объект, а функции <code>Point_move</code>, <code>Point_print</code> работают как методы. Объект создаётся через конструктор, а затем использует «методы». Такой подход соответствует <strong>эмуляции классов в C</strong>.</p><hr><p> </p><h2>Инкапсуляция и сокрытие данных</h2><p>Хотя C не обладает уровнями доступа (<code>private</code>, <code>public</code>), инкапсуляцию можно приблизить с помощью <strong>неполных типов</strong> (opaque type): в заголовке объявляется только указатель на структуру, а сама структура определена в .c-файле. Это скрывает внутренние поля объекта от пользователя API.</p><hr><p> </p><h2>Полиморфизм с помощью указателей на функции</h2><p>Для поддержки поведения, похожего на виртуальные методы, объект может содержать <strong>указатели на функции</strong>:</p><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>typedef struct {
    void (*speak)(void *);
} Animal;

void dogSpeak(void *self) { printf("Woof\n"); }
void catSpeak(void *self) { printf("Meow\n"); }

int main(void) {
    Animal dog = { dogSpeak };
    Animal cat = { catSpeak };
    dog.speak(&amp;dog);
    cat.speak(&amp;cat);
}
</code></pre><p> </p><p>Это позволяет разным объектам иметь разную реализацию поведения через <strong>динамическое связывание</strong>.</p><hr><p> </p><h2>Наследование: структурное встраивание</h2><p>Хотя прямого механизма наследования в C нет, его можно приблизить через <strong>встраивание структур</strong>:</p><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>typedef struct {
    int value;
} Base;

typedef struct {
    Base base;
    int extra;
} Derived;
</code></pre><p> </p><p>Это позволяет обращаться к общей части как к «базовому классу». Такие техники широко используются в практике написания крупных библиотек на C (например, в Linux kernel).</p><hr><p> </p><h2>Когда стоит и когда не стоит использовать ООП в C</h2><p> </p><h3>Когда стоит</h3><ul><li><p>Пишете сложные библиотеки или драйверы, где нужно чёткое разделение ответственности.</p></li></ul><p>Проект должен быть расширяемым и легко поддерживаемым в долгосрочной перспективе.</p><p>Вы хотите приблизить архитектуру к индустриальным стандартам, но обязаны использовать ANSI C.</p><h3>Когда не стоит</h3><ul><li><p>Проект очень мал, и усложнение архитектуры не оправдано.</p></li></ul><p>Требуется высокая производительность и минимальные накладные расходы.</p><p>Вы работаете в команде, где стандартные C++ механизмы ООП предпочтительнее и доступны.</p><hr><p> </p><h2>Заключение</h2><p>Объектно-ориентированное программирование в C <strong>возможно и полезно</strong> в задачах, где требуется структурирование больших систем и разделение интерфейса от реализации. Хотя язык не предоставляет встроенных механик классов, наследования и полиморфизма, эти свойства можно эффективно имитировать через структуры, указатели и хорошо продуманные API. Такой подход усиливает поддерживаемость, модульность и переиспользуемость кода в серьёзных проектах на С.</p>]]></description><guid isPermaLink="false">6</guid><pubDate>Sun, 25 Jan 2026 21:20:17 +0000</pubDate></item><item><title>Make &#x438; New &#x432; Go: &#x440;&#x430;&#x437;&#x431;&#x43E;&#x440; &#x438; &#x43D;&#x430;&#x433;&#x43B;&#x44F;&#x434;&#x43D;&#x44B;&#x435; &#x43F;&#x440;&#x438;&#x43C;&#x435;&#x440;&#x44B; &#x438;&#x441;&#x43F;&#x43E;&#x43B;&#x44C;&#x437;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x44F;</title><link>https://ithub.uno/statiarticles/1_articles/make-%D0%B8-new-%D0%B2-go-%D1%80%D0%B0%D0%B7%D0%B1%D0%BE%D1%80-%D0%B8-%D0%BD%D0%B0%D0%B3%D0%BB%D1%8F%D0%B4%D0%BD%D1%8B%D0%B5-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B-%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F-r27/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/https___dev-to-uploads_s3.amazonaws.com_uploads_articles_3wmodd43nx4l9h99z9dv.webp.57317d9205be99c709d7308b46ebbbab.webp" /></p>
<h2>Введение: что мы создаём в Go</h2><p>В Go есть два больших семейства типов:</p><ol><li><p><strong>Value types</strong> — обычные значения: <code>int</code>, <code>float64</code>, <code>bool</code>, <code>struct</code>, массивы.</p></li><li><p><strong>Reference types</strong> — ссылочные структуры: <code>slice</code>, <code>map</code>, <code>chan</code>.</p></li></ol><p>Value types можно размещать где угодно: в стеке, в куче или внутри других объектов.<br>Reference types — это конструкции уровня рантайма с внутренними механизмами: простое объявление через <code>var</code> даст <code>nil</code> и вызовет панику при попытке использовать. Именно поэтому Go использует <code>new</code> и <code>make</code> по-разному.</p><hr><h2><code>new</code> в Go: простое выделение памяти</h2><p>Функция <code>new(T)</code> выделяет память под тип <code>T</code>, обнуляет её и возвращает <strong>указатель на </strong><code>T</code>.</p><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>type Config struct {
    Enabled bool
    Count   int
}

cfg := new(Config)
// cfg имеет тип *Config
// cfg.Enabled == false
// cfg.Count == 0
</code></pre><ul><li><p>Внутри вызывается <code>runtime.newobject</code>, который делает <code>malloc</code> нужного размера и очищает память нулями.</p></li><li><p>Подходит для <strong>value types</strong>: int, string, bool, struct, array.</p></li></ul><hr><h3><code>new(T)</code> vs <code>&amp;T{}</code></h3><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>type User struct {
    Name string
    Age  int
}

u1 := new(User)   // выделяет объект в куче
u2 := &amp;User{}     // может быть в стеке или куче
</code></pre><p>Разница:</p><ul><li><p><code>new(T)</code> всегда аллоцирует в <strong>куче</strong>.</p></li><li><p><code>&amp;T{}</code> может остаться в <strong>стеке</strong>, если компилятор считает это безопасным (escape analysis).</p></li></ul><p>Используйте <code>&amp;T{}</code> для высокоэффективного кода с минимальными аллокациями.</p><hr><h3>Когда <code>new</code> действительно нужен</h3><ol><li><p><strong>Generic‑код</strong>: тип <code>T</code> неизвестен на этапе компиляции.</p></li></ol><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>func NewPointer[T any]() *T {
    return new(T)
}
</code></pre><ol start="2"><li><p><strong>Явная аллокация в куче</strong>: иногда нужно гарантировать объект в heap.</p></li></ol><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>pool := sync.Pool{
    New: func() any {
        return new(MyStruct)
    },
}
</code></pre><ol start="3"><li><p><strong>Опциональные значения через nil</strong>:</p></li></ol><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>type Options struct {
    RetryCount *int
}

o := Options{}
o.RetryCount = new(int)
*o.RetryCount = 3
</code></pre><hr><h3>Ограничения <code>new</code></h3><ul><li><p><code>new([]int)</code> вернёт <code>*[]int</code> с nil — использовать как полноценный slice нельзя:</p></li></ul><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>s := new([]int)
fmt.Println(*s == nil) // true
(*s)[0] = 1            // panic
</code></pre><ul><li><p>Аналогично с <code>map</code> и <code>chan</code>.</p></li></ul><hr><h2><code>make</code> в Go: инициализация runtime‑структур</h2><p><code>make</code> не просто выделяет память. Он <strong>создаёт рабочие slice, map и chan</strong> с полностью инициализированными внутренними структурами.</p><hr><h3>Slice: указатель, длина, вместимость</h3><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>s := make([]int, 10, 100)
</code></pre><ul><li><p>Создаёт slice header:</p></li></ul><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>type sliceHeader struct {
    Data uintptr // указатель на массив
    Len  int
    Cap  int
}
</code></pre><ul><li><p>Slice готов к использованию и может расширяться до <code>cap</code> без новых аллокаций.</p></li><li><p><code>var s []int</code> даст nil-слайс — любое обращение к элементу вызовет панику.</p></li></ul><hr><h3>Map: работа с бакетами</h3><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>m := make(map[string]int, 100)
</code></pre><ul><li><p>Инициализирует внутреннюю структуру hmap:</p><ul><li><p>count, flags, бакеты, старые бакеты для перестройки.</p></li></ul></li><li><p><code>new(map[string]int)</code> создаст только nil-указатель — использовать его нельзя.</p></li></ul><hr><h3>Chan: синхронизация и буфер</h3><pre spellcheck="" class="tmiCode language-go" data-language="Go"><code>c := make(chan int, 5)
</code></pre><ul><li><p>Создаёт полноценную очередь сообщений с буфером и счетчиками.</p></li><li><p><code>var c chan int</code> даст nil-канал — операции блокируют навсегда.</p></li></ul><hr><h2>Когда использовать <code>make</code>, а когда <code>new</code></h2><div class="tmiRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Сценарий</p></th><th colspan="1" rowspan="1"><p>Выбор</p></th><th colspan="1" rowspan="1"><p>Почему</p></th></tr><tr><td colspan="1" rowspan="1"><p>Slice, map, chan</p></td><td colspan="1" rowspan="1"><p><code>make</code></p></td><td colspan="1" rowspan="1"><p>Создаёт рабочую структуру, готовую к использованию</p></td></tr><tr><td colspan="1" rowspan="1"><p>Value type, generic</p></td><td colspan="1" rowspan="1"><p><code>new</code></p></td><td colspan="1" rowspan="1"><p>Получение указателя на zero-value</p></td></tr><tr><td colspan="1" rowspan="1"><p>Явная куча для value type</p></td><td colspan="1" rowspan="1"><p><code>new</code></p></td><td colspan="1" rowspan="1"><p>Гарантированное выделение в heap</p></td></tr><tr><td colspan="1" rowspan="1"><p>Опциональные значения</p></td><td colspan="1" rowspan="1"><p><code>new</code></p></td><td colspan="1" rowspan="1"><p>Возможность различать nil и заданное значение</p></td></tr></tbody></table></div><hr><h2>Вывод</h2><ul><li><p><code>new</code>: простое выделение памяти и указатель на ноль.</p></li><li><p><code>make</code>: инициализация runtime‑структур, готовых к работе.</p></li></ul><p>Понимание разницы между ними — обязательный минимум для любого разработчика на Go. Используйте <code>make</code> для slice, map и chan, а <code>new</code> — для value types и generic-кода.</p>]]></description><guid isPermaLink="false">27</guid><pubDate>Fri, 06 Feb 2026 18:44:31 +0000</pubDate></item><item><title>&#x41F;&#x43E;&#x447;&#x435;&#x43C;&#x443; &#x41B;&#x438;&#x43D;&#x443;&#x441; &#x422;&#x43E;&#x440;&#x432;&#x430;&#x43B;&#x44C;&#x434;&#x441; &#x43E;&#x442;&#x43A;&#x430;&#x437;&#x44B;&#x432;&#x430;&#x435;&#x442;&#x441;&#x44F; &#x43E;&#x442; C++ &#x432; &#x44F;&#x434;&#x440;&#x435; Linux: &#x440;&#x430;&#x437;&#x431;&#x43E;&#x440; &#x430;&#x440;&#x433;&#x443;&#x43C;&#x435;&#x43D;&#x442;&#x43E;&#x432;</title><link>https://ithub.uno/statiarticles/1_articles/%D0%BF%D0%BE%D1%87%D0%B5%D0%BC%D1%83-%D0%BB%D0%B8%D0%BD%D1%83%D1%81-%D1%82%D0%BE%D1%80%D0%B2%D0%B0%D0%BB%D1%8C%D0%B4%D1%81-%D0%BE%D1%82%D0%BA%D0%B0%D0%B7%D1%8B%D0%B2%D0%B0%D0%B5%D1%82%D1%81%D1%8F-%D0%BE%D1%82-c-%D0%B2-%D1%8F%D0%B4%D1%80%D0%B5-linux-%D1%80%D0%B0%D0%B7%D0%B1%D0%BE%D1%80-%D0%B0%D1%80%D0%B3%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2-r30/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/8658-1.jpg.5438f042799cc824b1a068952e16b005.jpg" /></p>
<h3>Линус Торвальдс и его отказ от C++ для ядра Linux</h3><p>Линус Торвальдс, создатель Linux и «великодушный диктатор» проекта, известен своей критикой языка C++. Он не отвергает его просто так — он приводит убедительные технические и практические аргументы против его применения в ядре Linux.</p><p>В чем же причина неприятия C++? Давайте разберем ключевые доводы Линуса.</p><hr><h3>Почему C++ не подходит для ядра Linux</h3><p>C и C++ похожи, но не идентичны. C++ — это объектно-ориентированное расширение C, добавляющее классы, конструкторы, деструкторы, шаблоны, обработку исключений, пространства имен и перегрузку операторов. Эти «улучшения» приносят новые парадигмы, но и новые риски.</p><h4>1. Обработка исключений</h4><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Вся обработка исключений в C++ фундаментально сломана, особенно для ядра»</p></div></blockquote><p>В C ошибки обрабатываются через возвращаемые значения, что делает поведение программы предсказуемым. В C++ используются исключения, которые могут возникнуть в любой части кода. Для ядра с миллионами строк кода это неприемлемо:</p><ul><li><p>Исключения трудно отлаживать;</p></li><li><p>Они меняют парадигму обработки ошибок;</p></li><li><p>Могут привести к нестабильности ядра.</p></li></ul><p>Для пользовательских приложений вроде GNOME редактора это мелочь, но для ядра Linux — реальный риск.</p><h4>2. Управление памятью и RAII</h4><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Любой язык, который выделяет память за вашей спиной, не подходит для ядра»</p></div></blockquote><p>C++ использует RAII (Resource Acquisition Is Initialization), автоматическое управление ресурсами через деструкторы. В ядре Linux память контролируется тонко и вручную, чтобы обеспечить максимальную производительность. Автоматизация в C++:</p><ul><li><p>Увеличивает зависимость от компилятора;</p></li><li><p>Может снижать скорость работы модулей;</p></li><li><p>Увеличивает вероятность багов.</p></li></ul><h4>3. Объектно-ориентированное программирование</h4><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Вы можете писать объектно-ориентированный код на C без всей этой *** чуши C++»</p></div></blockquote><p>Линус считает, что ООП полезно, но C++ вносит слишком много лишнего. ООП можно реализовать и на чистом C через структуры и функции. Пример «класса» на C:</p><pre spellcheck="" class="tmiCode language-c" data-language="C"><code>#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;

typedef struct {
    int value;
    void (*increment)(struct Person *self);
} Person;

void increment_person(Person *self) {
    self-&gt;value++;
}

Person* person_new(int initial_value) {
    Person *p = (Person*)malloc(sizeof(Person));
    p-&gt;value = initial_value;
    p-&gt;increment = increment_person;
    return p;
}

void person_free(Person *p) {
    free(p);
}

int main() {
    Person *p = person_new(5);
    printf("Initial value: %d\n", p-&gt;value);
    p-&gt;increment(p);
    printf("After increment: %d\n", p-&gt;value);
    person_free(p);
    return 0;
}
</code></pre><p>Это доказывает, что функциональность ООП доступна и в чистом C.</p><h4>4. Стабильность библиотек и зависимостей</h4><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«STL и Boost для ядра — это боль и нестабильность»</p></div></blockquote><p>Пользовательские библиотеки вроде STL или Boost могут быть стабильными для приложений, но не для ядра. Любая новая зависимость:</p><ul><li><p>Увеличивает риски безопасности;</p></li><li><p>Требует поддержки и обновлений;</p></li><li><p>Может снизить производительность.</p></li></ul><p>Пример — уязвимость CVE-2024–3094 в liblzma, когда сторонний бэкдор повлиял на безопасность.</p><h4>5. Сложные абстракции</h4><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Неэффективные абстракции через два года могут разрушить весь код»</p></div></blockquote><p>ОП-паттерны могут быть удобны для приложений, но в ядре Linux они опасны:</p><ul><li><p>Большие иерархии классов трудно поддерживать;</p></li><li><p>Ошибки в логике абстракций могут разрушить архитектуру;</p></li><li><p>Потребуется переработка больших частей кода.</p></li></ul><hr><h3>Итог: почему ядро Linux на чистом C</h3><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Единственный способ сделать хороший, эффективный и портируемый код — ограничиться тем, что доступно на C»</p></div></blockquote><p>Аргумент Линуса: разработка ядра требует предсказуемости, производительности и стабильности. C++ добавляет много возможностей, но они создают риски, которые ядро не может себе позволить.</p><p>Использование чистого C привлекает разработчиков, сосредоточенных на аппаратных и системных задачах, а не на сложностях ООП.</p><hr><h3>Будущее: Rust, Go или Java?</h3><p>Линус открыто обсуждал Rust для ядра. Вопросы применения Go, Java или C# повторяют дискуссию о C++. Основной принцип остается прежним: ядро должно оставаться максимально стабильным и оптимизированным.</p><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p>«Вы хотите, чтобы ядро МРТ выбрасывало исключения?» — юмор Линуса иллюстрирует риск внедрения непредсказуемого кода в критически важные системы.</p></div></blockquote><hr><h3>Выводы для разработчиков</h3><ol><li><p>Выбор языка и инструментов влияет на стабильность и производительность;</p></li><li><p>Любые зависимости требуют оценки долгосрочных последствий;</p></li><li><p>Иногда простота C — лучший выбор для системного программирования;</p></li><li><p>Разработчики должны балансировать эргономику и стабильность, особенно в критичных проектах.</p></li></ol><hr><p><strong>Ключевой урок:</strong><br>Линус демонстрирует, что при разработке системного программного обеспечения важнее стабильность и предсказуемость, чем удобство и новые парадигмы. Иногда лучше использовать проверенные, простые инструменты, чтобы гарантировать работу миллионов устройств по всему миру.</p>]]></description><guid isPermaLink="false">30</guid><pubDate>Fri, 06 Feb 2026 18:49:55 +0000</pubDate></item><item><title>Bash-&#x441;&#x43A;&#x440;&#x438;&#x43F;&#x442;&#x44B; &#x434;&#x43B;&#x44F; &#x441;&#x438;&#x441;&#x442;&#x435;&#x43C;&#x43D;&#x44B;&#x445; &#x430;&#x434;&#x43C;&#x438;&#x43D;&#x438;&#x441;&#x442;&#x440;&#x430;&#x442;&#x43E;&#x440;&#x43E;&#x432;: &#x43E;&#x442; &#x430;&#x437;&#x43E;&#x432; &#x434;&#x43E; &#x43F;&#x440;&#x43E;&#x434;&#x432;&#x438;&#x43D;&#x443;&#x442;&#x44B;&#x445; &#x43F;&#x430;&#x442;&#x442;&#x435;&#x440;&#x43D;&#x43E;&#x432;</title><link>https://ithub.uno/statiarticles/1_articles/bash-%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D1%8B-%D0%B4%D0%BB%D1%8F-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%BD%D1%8B%D1%85-%D0%B0%D0%B4%D0%BC%D0%B8%D0%BD%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2-%D0%BE%D1%82-%D0%B0%D0%B7%D0%BE%D0%B2-%D0%B4%D0%BE-%D0%BF%D1%80%D0%BE%D0%B4%D0%B2%D0%B8%D0%BD%D1%83%D1%82%D1%8B%D1%85-%D0%BF%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD%D0%BE%D0%B2-r37/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/ec4846bbad3ec0a1071e3d2c5d54dfae.jpg.6d32432fd76833790ce5965af4b60a5b.jpg" /></p>
<p>Bash — это не «просто командная строка». Это полноценный скриптовый язык, на котором написаны тысячи скриптов деплоя, мониторинга, бэкапов и автоматизации по всему миру. Проблема в том, что большинство bash-скриптов написаны наспех, без понимания подводных камней, и падают в самый неподходящий момент. Эта статья о том, как писать bash-скрипты, которым можно доверять.</p><hr><h2>Основы: заголовок, который должен быть в каждом скрипте</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
# Описание: что делает скрипт
# Автор: your@email.com
# Версия: 1.0.0

set -euo pipefail
IFS=$'\n\t'
</code></pre><p>Разберём <code>set -euo pipefail</code> — это не магия, это защита:</p><ul><li><p><code>set -e</code> — скрипт останавливается при ошибке любой команды</p></li><li><p><code>set -u</code> — ошибка при обращении к неустановленной переменной</p></li><li><p><code>set -o pipefail</code> — ошибка в пайпе не скрывается (без этого <code>false | true</code> вернёт 0)</p></li><li><p><code>IFS=$'\n\t'</code> — разделитель полей только перевод строки и таб, не пробел</p></li></ul><p>Без этих строк скрипт будет молча продолжаться после ошибок, что приводит к катастрофическим последствиям на продакшне.</p><hr><h2>Работа с переменными и параметрами</h2><h3>Правильное использование переменных</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Всегда кавычки вокруг переменных!
name="John Doe"
echo "$name"        # правильно
echo $name          # сломается если пробел в имени

# Значения по умолчанию
database="${DB_NAME:-myapp_production}"
timeout="${TIMEOUT:-30}"

# Проверка обязательного параметра
: "${API_KEY:?'API_KEY is required but not set'}"

# Массивы
servers=("web01" "web02" "web03")
for server in "${servers[@]}"; do
    echo "Processing $server"
done

# Ассоциативные массивы (bash 4+)
declare -A ports
ports[web]=80
ports[mysql]=3306
ports[redis]=6379

for service in "${!ports[@]}"; do
    echo "$service: ${ports[$service]}"
done
</code></pre><h3>Обработка аргументов командной строки</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
set -euo pipefail

# Продвинутая обработка аргументов с getopts
usage() {
    cat &lt;&lt;EOF
Usage: $(basename "$0") [OPTIONS] &lt;environment&gt;

Options:
  -h, --help        Show this help
  -v, --verbose     Verbose output
  -n, --dry-run     Dry run, don't make changes
  -c, --config FILE Config file path (default: /etc/myapp/config.conf)

Environment:
  staging    Deploy to staging
  production Deploy to production

Examples:
  $(basename "$0") -v staging
  $(basename "$0") --dry-run production
EOF
    exit 1
}

# Разбор длинных опций через getopt
OPTS=$(getopt -o hvnc: --long help,verbose,dry-run,config: -n "$(basename "$0")" -- "$@")
eval set -- "$OPTS"

VERBOSE=false
DRY_RUN=false
CONFIG="/etc/myapp/config.conf"

while true; do
    case "$1" in
        -h|--help) usage ;;
        -v|--verbose) VERBOSE=true; shift ;;
        -n|--dry-run) DRY_RUN=true; shift ;;
        -c|--config) CONFIG="$2"; shift 2 ;;
        --) shift; break ;;
        *) echo "Unknown option: $1"; usage ;;
    esac
done

ENVIRONMENT="${1:-}"
[[ -z "$ENVIRONMENT" ]] &amp;&amp; { echo "Error: environment required"; usage; }
[[ "$ENVIRONMENT" != "staging" &amp;&amp; "$ENVIRONMENT" != "production" ]] &amp;&amp; {
    echo "Error: environment must be 'staging' or 'production'"
    exit 1
}

$VERBOSE &amp;&amp; echo "Config: $CONFIG"
$VERBOSE &amp;&amp; echo "Environment: $ENVIRONMENT"
$DRY_RUN &amp;&amp; echo "DRY RUN MODE - no changes will be made"
</code></pre><hr><h2>Логирование — правильный подход</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Цвета для терминала
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'  # No Color

# Лог-файл
LOG_FILE="/var/log/myapp/deploy-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$(dirname "$LOG_FILE")"

log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    # В файл — без цветов
    echo "[$timestamp] [$level] $message" &gt;&gt; "$LOG_FILE"
    
    # В терминал — с цветами
    case "$level" in
        INFO)  echo -e "${GREEN}[INFO]${NC}  $message" ;;
        WARN)  echo -e "${YELLOW}[WARN]${NC}  $message" ;;
        ERROR) echo -e "${RED}[ERROR]${NC} $message" &gt;&amp;2 ;;
        DEBUG) $VERBOSE &amp;&amp; echo -e "${BLUE}[DEBUG]${NC} $message" ;;
    esac
}

# Использование
log INFO "Starting deployment to $ENVIRONMENT"
log WARN "Skipping health check in dry-run mode"
log ERROR "Failed to connect to database"
log DEBUG "Connection string: $DB_URL"
</code></pre><hr><h2>Обработка ошибок и cleanup</h2><h3>trap — всегда убирай за собой</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
set -euo pipefail

# Временные файлы
TEMP_DIR=$(mktemp -d)
LOCK_FILE="/tmp/myapp.lock"

# Функция очистки — выполняется при любом выходе
cleanup() {
    local exit_code=$?
    
    log INFO "Cleaning up..."
    rm -rf "$TEMP_DIR"
    rm -f "$LOCK_FILE"
    
    if [[ $exit_code -ne 0 ]]; then
        log ERROR "Script failed with exit code $exit_code"
        # Отправляем уведомление
        send_alert "Deployment failed on $(hostname)" "$LOG_FILE"
    fi
    
    exit $exit_code
}

# Обработка сигналов
error_handler() {
    local line_number="$1"
    log ERROR "Error on line $line_number"
}

trap cleanup EXIT
trap 'error_handler $LINENO' ERR
trap 'log WARN "Interrupted by user"; exit 130' INT TERM

# Защита от параллельного запуска через lockfile
acquire_lock() {
    if ! mkdir "$LOCK_FILE" 2&gt;/dev/null; then
        local pid
        pid=$(cat "$LOCK_FILE/pid" 2&gt;/dev/null || echo "unknown")
        log ERROR "Another instance is running (PID: $pid)"
        exit 1
    fi
    echo $$ &gt; "$LOCK_FILE/pid"
    log DEBUG "Lock acquired"
}

acquire_lock
</code></pre><h3>Retry-логика для нестабильных операций</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Универсальная функция retry
retry() {
    local max_attempts="$1"
    local delay="$2"
    local description="$3"
    shift 3
    local cmd=("$@")
    
    local attempt=1
    while true; do
        log INFO "[$attempt/$max_attempts] Trying: $description"
        
        if "${cmd[@]}"; then
            log INFO "Success: $description"
            return 0
        fi
        
        if [[ $attempt -ge $max_attempts ]]; then
            log ERROR "All $max_attempts attempts failed: $description"
            return 1
        fi
        
        log WARN "Attempt $attempt failed, retrying in ${delay}s..."
        sleep "$delay"
        ((attempt++))
        
        # Экспоненциальная задержка (удваиваем каждый раз, макс 60 сек)
        delay=$(( delay * 2 &gt; 60 ? 60 : delay * 2 ))
    done
}

# Использование
retry 5 2 "Health check" curl -sf http://localhost/health
retry 3 5 "Database backup" pg_dump myapp &gt; /backup/dump.sql
</code></pre><hr><h2>Параллельное выполнение задач</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Простой способ — запуск в background + wait
deploy_to_servers() {
    local servers=("$@")
    local pids=()
    local failed=0
    
    for server in "${servers[@]}"; do
        log INFO "Starting deploy on $server"
        (
            ssh "$server" 'cd /app &amp;&amp; git pull &amp;&amp; systemctl restart myapp'
            log INFO "Deploy done on $server"
        ) &amp;
        pids+=($!)
    done
    
    # Ждём все процессы и собираем статусы
    for i in "${!pids[@]}"; do
        if ! wait "${pids[$i]}"; then
            log ERROR "Deploy failed on ${servers[$i]}"
            ((failed++))
        fi
    done
    
    return $failed
}

# С ограничением параллелизма (не более N одновременно)
run_parallel() {
    local max_jobs="$1"
    shift
    local items=("$@")
    
    local running=0
    local pids=()
    
    for item in "${items[@]}"; do
        # Ждём если достигнут лимит
        while [[ $running -ge $max_jobs ]]; do
            # Проверяем завершившиеся процессы
            local new_pids=()
            for pid in "${pids[@]}"; do
                if kill -0 "$pid" 2&gt;/dev/null; then
                    new_pids+=("$pid")
                else
                    ((running--))
                fi
            done
            pids=("${new_pids[@]}")
            sleep 0.1
        done
        
        process_item "$item" &amp;
        pids+=($!)
        ((running++))
    done
    
    wait
}
</code></pre><hr><h2>Работа с файлами и текстом</h2><h3>Безопасное чтение конфигурации</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Загрузка .env файла
load_env() {
    local env_file="${1:-.env}"
    
    [[ -f "$env_file" ]] || { log WARN "No $env_file found"; return 0; }
    
    # Безопасная загрузка: только KEY=VALUE строки, без выполнения кода
    while IFS='=' read -r key value; do
        # Пропускаем комментарии и пустые строки
        [[ "$key" =~ ^[[:space:]]*# ]] &amp;&amp; continue
        [[ -z "$key" ]] &amp;&amp; continue
        
        # Удаляем пробелы вокруг ключа
        key="${key//[[:space:]]/}"
        
        # Удаляем кавычки из значения
        value="${value%\"}"
        value="${value#\"}"
        value="${value%\'}"
        value="${value#\'}"
        
        # Экспортируем только валидные имена переменных
        if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
            export "$key=$value"
        fi
    done &lt; "$env_file"
}
</code></pre><h3>Манипуляции со строками</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Встроенные операции (без fork, быстрее)
string="Hello, World!"

# Длина
echo "${#string}"  # 13

# Подстрока (offset:length)
echo "${string:7:5}"  # World

# Замена (первое вхождение)
echo "${string/Hello/Hi}"  # Hi, World!

# Замена (все вхождения)
path="/usr/local/bin:/usr/bin:/bin"
echo "${path//:/ }"  # /usr/local/bin /usr/bin /bin

# Upper/Lower case (bash 4+)
echo "${string^^}"  # HELLO, WORLD!
echo "${string,,}"  # hello, world!

# Trim (удаление пробелов)
trim() {
    local str="$*"
    str="${str#"${str%%[![:space:]]*}"}"
    str="${str%"${str##*[![:space:]]}"}"
    echo "$str"
}

# Разбивка строки на массив
IFS=',' read -ra parts &lt;&lt;&lt; "one,two,three"
for part in "${parts[@]}"; do echo "$part"; done
</code></pre><hr><h2>Реальные примеры: скрипты для продакшна</h2><h3>Скрипт деплоя с zero-downtime</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
set -euo pipefail

DEPLOY_DIR="/var/www/myapp"
RELEASES_DIR="$DEPLOY_DIR/releases"
CURRENT_LINK="$DEPLOY_DIR/current"
SHARED_DIR="$DEPLOY_DIR/shared"
KEEP_RELEASES=5

deploy() {
    local release_dir
    release_dir="$RELEASES_DIR/$(date +%Y%m%d%H%M%S)"
    
    log INFO "Creating release directory: $release_dir"
    mkdir -p "$release_dir"
    
    # Клонируем или обновляем код
    log INFO "Deploying code..."
    if [[ -d "$DEPLOY_DIR/repo" ]]; then
        git -C "$DEPLOY_DIR/repo" fetch origin
        git -C "$DEPLOY_DIR/repo" archive HEAD | tar -x -C "$release_dir"
    else
        git clone --depth 1 "$GIT_REPO" "$DEPLOY_DIR/repo"
        git -C "$DEPLOY_DIR/repo" archive HEAD | tar -x -C "$release_dir"
    fi
    
    # Линкуем shared директории
    ln -sfn "$SHARED_DIR/uploads" "$release_dir/public/uploads"
    ln -sfn "$SHARED_DIR/.env" "$release_dir/.env"
    
    # Устанавливаем зависимости
    log INFO "Installing dependencies..."
    composer install --no-dev --optimize-autoloader -d "$release_dir"
    
    # Прогреваем кэш
    log INFO "Warming up cache..."
    php "$release_dir/spark" cache:clear
    
    # Переключаем симлинк атомарно
    log INFO "Switching to new release..."
    ln -sfn "$release_dir" "${CURRENT_LINK}.new"
    mv -Tf "${CURRENT_LINK}.new" "$CURRENT_LINK"
    
    # Перезагружаем PHP-FPM (graceful)
    kill -USR2 $(cat /var/run/php/php8.2-fpm.pid)
    
    # Очищаем старые релизы
    cleanup_old_releases
    
    log INFO "Deploy completed successfully!"
}

cleanup_old_releases() {
    local releases
    releases=$(ls -1t "$RELEASES_DIR")
    local count
    count=$(echo "$releases" | wc -l)
    
    if [[ $count -gt $KEEP_RELEASES ]]; then
        echo "$releases" | tail -n +"$((KEEP_RELEASES + 1))" | while read -r release; do
            log INFO "Removing old release: $release"
            rm -rf "$RELEASES_DIR/$release"
        done
    fi
}

rollback() {
    local previous
    previous=$(ls -1t "$RELEASES_DIR" | sed -n '2p')
    
    if [[ -z "$previous" ]]; then
        log ERROR "No previous release to rollback to"
        exit 1
    fi
    
    log INFO "Rolling back to: $previous"
    ln -sfn "$RELEASES_DIR/$previous" "${CURRENT_LINK}.rollback"
    mv -Tf "${CURRENT_LINK}.rollback" "$CURRENT_LINK"
    kill -USR2 $(cat /var/run/php/php8.2-fpm.pid)
    log INFO "Rollback completed"
}

case "${1:-deploy}" in
    deploy) deploy ;;
    rollback) rollback ;;
    *) echo "Usage: $0 [deploy|rollback]"; exit 1 ;;
esac
</code></pre><h3>Скрипт мониторинга и уведомлений</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
set -euo pipefail

# Пороговые значения
CPU_THRESHOLD=85
MEM_THRESHOLD=90
DISK_THRESHOLD=85
LOAD_THRESHOLD=4.0

ALERT_EMAIL="ops@example.com"
WEBHOOK_URL="${SLACK_WEBHOOK:-}"

send_alert() {
    local subject="$1"
    local body="$2"
    
    # Email
    echo "$body" | mail -s "[$HOSTNAME] ALERT: $subject" "$ALERT_EMAIL"
    
    # Slack webhook
    if [[ -n "$WEBHOOK_URL" ]]; then
        curl -s -X POST "$WEBHOOK_URL" \
            -H 'Content-type: application/json' \
            -d "{\"text\":\":warning: *[$HOSTNAME]* $subject\n\`\`\`$body\`\`\`\"}"
    fi
}

check_cpu() {
    local cpu_usage
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1)
    cpu_usage=${cpu_usage%.*}  # целая часть
    
    if [[ $cpu_usage -gt $CPU_THRESHOLD ]]; then
        local top_processes
        top_processes=$(ps aux --sort=-%cpu | head -6 | tail -5)
        send_alert "High CPU: ${cpu_usage}%" "CPU usage: ${cpu_usage}%\n\nTop processes:\n$top_processes"
    fi
}

check_memory() {
    local mem_total mem_used mem_percent
    mem_total=$(free -m | awk '/^Mem:/{print $2}')
    mem_used=$(free -m | awk '/^Mem:/{print $3}')
    mem_percent=$(( mem_used * 100 / mem_total ))
    
    if [[ $mem_percent -gt $MEM_THRESHOLD ]]; then
        local top_processes
        top_processes=$(ps aux --sort=-%mem | head -6 | tail -5)
        send_alert "High Memory: ${mem_percent}%" "Memory: ${mem_used}MB / ${mem_total}MB (${mem_percent}%)\n\n$top_processes"
    fi
}

check_disk() {
    while IFS= read -r line; do
        local usage mount
        usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
        mount=$(echo "$line" | awk '{print $6}')
        
        if [[ $usage -gt $DISK_THRESHOLD ]]; then
            send_alert "High Disk: $mount at ${usage}%" \
                "Disk usage on $mount: ${usage}%\n\n$(df -h "$mount")"
        fi
    done &lt; &lt;(df -h | grep -E '^/dev/' | grep -v tmpfs)
}

check_services() {
    local services=("nginx" "php8.2-fpm" "mysql" "redis")
    
    for service in "${services[@]}"; do
        if ! systemctl is-active --quiet "$service"; then
            send_alert "Service DOWN: $service" \
                "Service $service is not running!\n\n$(systemctl status "$service" --no-pager | tail -20)"
            
            # Попытка перезапуска
            log WARN "Attempting to restart $service..."
            systemctl restart "$service" &amp;&amp; \
                send_alert "Service RECOVERED: $service" "Service $service was restarted successfully"
        fi
    done
}

# Запускаем все проверки
check_cpu
check_memory
check_disk
check_services
</code></pre><hr><h2>Лучшие практики</h2><p><strong>Используйте shellcheck</strong> — это ESLint для bash:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>apt install shellcheck
shellcheck myscript.sh
# В CI/CD:
find . -name "*.sh" -exec shellcheck {} +
</code></pre><p><strong>Тестируйте скрипты с bats</strong>:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>apt install bats
# test.bats:
@test "cleanup removes old files" {
    touch /tmp/testfile
    run cleanup /tmp/testfile
    [ "$status" -eq 0 ]
    [ ! -f /tmp/testfile ]
}
bats test.bats
</code></pre><p><strong>Документируйте через heredoc</strong>:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>show_help() {
    cat &lt;&lt;'EOF'
...документация...
EOF
}
</code></pre><p>Bash — это мощный инструмент, но он требует дисциплины. Скрипт, написанный правильно — это надёжный автоматизированный сотрудник. Написанный небрежно — бомба с часовым механизмом.</p>]]></description><guid isPermaLink="false">37</guid><pubDate>Sun, 22 Feb 2026 13:13:44 +0000</pubDate></item><item><title>Git &#x438; &#x441;&#x43E;&#x432;&#x440;&#x435;&#x43C;&#x435;&#x43D;&#x43D;&#x44B;&#x435; &#x43F;&#x440;&#x430;&#x43A;&#x442;&#x438;&#x43A;&#x438; &#x440;&#x430;&#x437;&#x440;&#x430;&#x431;&#x43E;&#x442;&#x43A;&#x438;: &#x43E;&#x442; &#x445;&#x430;&#x43E;&#x441;&#x430; &#x43A; &#x43F;&#x43E;&#x440;&#x44F;&#x434;&#x43A;&#x443;</title><link>https://ithub.uno/statiarticles/1_articles/git-%D0%B8-%D1%81%D0%BE%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B8-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B8-%D0%BE%D1%82-%D1%85%D0%B0%D0%BE%D1%81%D0%B0-%D0%BA-%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D1%83-r116/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/image-1.webp.b8ca5fea31216e01b402239c3d33616e.webp" /></p>
<h2>Git: это не просто "сохранить файл"</h2><p>Git изобрёл Линус Торвальдс в 2005 году за две недели — потому что существующие системы контроля версий его раздражали. Результат стал стандартом де-факто для всей современной разработки.</p><p>Но большинство разработчиков используют только 10% возможностей Git: <code>git add</code>, <code>git commit</code>, <code>git push</code>. И потом удивляются, почему в команде хаос, история проекта нечитаема, а деплой — это страшный ритуал.</p><p>Правильное использование Git — это не набор команд, это <strong>культура разработки</strong>. Сегодня разберём, как устроена эта культура в реальных командах.</p><hr><h2>Анатомия правильного коммита</h2><p>Коммит — это единица изменений. Плохой коммит: "исправил баги и добавил фичи". Хороший коммит: одно логическое изменение, понятное описание.</p><h3>Conventional Commits — стандарт сообщений</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>&lt;type&gt;(&lt;scope&gt;): &lt;description&gt;

[optional body]

[optional footer(s)]
</code></pre><p><strong>Типы:</strong></p><ul><li><p><code>feat</code> — новая функциональность</p></li><li><p><code>fix</code> — исправление бага</p></li><li><p><code>docs</code> — только документация</p></li><li><p><code>style</code> — форматирование, точки с запятой (нет изменений логики)</p></li><li><p><code>refactor</code> — рефакторинг (нет новой функциональности, нет фикса)</p></li><li><p><code>perf</code> — оптимизация производительности</p></li><li><p><code>test</code> — добавление тестов</p></li><li><p><code>chore</code> — обслуживание: обновление зависимостей, конфигурации CI</p></li><li><p><code>ci</code> — изменения CI/CD конфигурации</p></li><li><p><code>revert</code> — откат предыдущего коммита</p></li></ul><p><strong>Примеры:</strong></p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Плохо:
git commit -m "fix"
git commit -m "wip"
git commit -m "changes"
git commit -m "поправил немного"

# Хорошо:
git commit -m "fix(auth): исправлена утечка токена при logout"
git commit -m "feat(modbus): добавлена поддержка FC15 (write multiple coils)"
git commit -m "perf(historian): оптимизирован batch-insert, +340% throughput"
git commit -m "docs(api): добавлены примеры для /api/v1/devices endpoint"

# С телом для сложных изменений:
git commit -m "fix(plc): исправлено переполнение счётчика при rollover

Счётчик типа UINT использовался для значений &gt;65535.
Изменён на DINT (32-бит). Затронутые устройства: все узлы с FC03.

Closes #247"
</code></pre><h3>Почему это важно?</h3><ol><li><p><strong>Автоматический CHANGELOG</strong> — инструменты как <code>conventional-changelog</code> генерируют его автоматически</p></li><li><p><strong>Семантическое версионирование</strong> — <code>feat</code> → minor, <code>fix</code> → patch, <code>feat!</code> или <code>BREAKING CHANGE</code> → major</p></li><li><p><strong>Читаемая история</strong> — через год понятно что и зачем было сделано</p></li><li><p><strong>Быстрый поиск</strong> — <code>git log --grep="fix(modbus)"</code> найдёт все фиксы Modbus</p></li></ol><hr><h2>Стратегии ветвления</h2><h3>Git Flow — классика для релизного цикла</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>main ────────────────────────────────────── (production-ready, теги версий)
   \                                      /
release/1.2.0 ───────────────────────────  (только bagfixes перед релизом)
         \                               /
develop ──────────────────────────────── (интеграция фич)
          \          \          /
           feat/A     feat/B  feat/C
</code></pre><p><strong>Ветки:</strong></p><ul><li><p><code>main</code> — всегда стабильный, деплоится в прод, только через merge из <code>release/*</code></p></li><li><p><code>develop</code> — основная ветка разработки, всегда должна собираться</p></li><li><p><code>feature/*</code> — новые фичи, создаются из <code>develop</code>, мержатся в <code>develop</code></p></li><li><p><code>release/*</code> — подготовка релиза (версия, changelog), только bugfix</p></li><li><p><code>hotfix/*</code> — срочные фиксы прод, мержатся в <code>main</code> И <code>develop</code></p></li></ul><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создать фичу:
git checkout develop
git checkout -b feature/modbus-fc15-support

# Завершить фичу:
git checkout develop
git merge --no-ff feature/modbus-fc15-support  # --no-ff сохраняет историю
git branch -d feature/modbus-fc15-support

# Подготовить релиз:
git checkout develop
git checkout -b release/1.2.0
# Обновить версию, CHANGELOG...
git commit -m "chore(release): version 1.2.0"
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
</code></pre><h3>Trunk-Based Development — для быстрых команд</h3><p>Все разработчики работают в одной ветке (<code>main</code>), фичи прячутся за feature-флагами. Деплой несколько раз в день. Подходит для опытных команд с хорошим покрытием тестами.</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Только короткоживущие ветки (1-2 дня максимум)
git checkout -b task/PLC-247-fix-counter-overflow
# ... работа ...
git push origin task/PLC-247-fix-counter-overflow
# Pull Request → Review → Merge в main
# Деплой автоматически
</code></pre><h3>GitHub Flow — для непрерывного деплоя</h3><p>Упрощённый Git Flow без <code>develop</code>:</p><ul><li><p><code>main</code> = то, что в проде</p></li><li><p>Feature branches — от main, в main через PR</p></li><li><p>Деплой = merge в main</p></li></ul><hr><h2>Code Review: как делать правильно</h2><p>Code review — не поиск ошибок, это <strong>обмен знаниями и повышение качества</strong>. Хороший review делает команду сильнее.</p><h3>Для автора PR:</h3><pre spellcheck="" class="tmiCode language-markdown" data-language="Markdown"><code>## Описание
Добавлена поддержка записи нескольких coils (FC15) в Modbus slave.

## Мотивация
Клиент запросил управление 16 выходными реле через один Modbus-запрос
вместо 16 отдельных FC05. Уменьшает нагрузку на шину в 16 раз.

## Изменения
- `ModbusSlave::handle_fc15()` — новый обработчик функционального кода 15
- Обновлён маппинг coils на GPIO пины
- Добавлены unit-тесты: 8 тест-кейсов

## Тестирование
- [x] Unit-тесты: все зелёные
- [x] Интеграционный тест с реальным Modbus-мастером (Python pymodbus)
- [x] Проверен на железе: Raspberry Pi + MCP2551

## Breaking Changes
Нет. FC05 продолжает работать.

## Связанные Issues
Closes #247
</code></pre><h3>Для ревьюера:</h3><p><strong>Проверяйте:</strong></p><ol><li><p>Логику — правильно ли реализовано то, что задумано?</p></li><li><p>Граничные случаи — что при пустых данных? При переполнении? При сетевой ошибке?</p></li><li><p>Безопасность — нет ли SQL-инъекций, XSS, незащищённых данных?</p></li><li><p>Производительность — нет ли N+1 запросов, бесконечных циклов?</p></li><li><p>Тесты — покрывают ли они описанную функциональность?</p></li><li><p>Документацию — понятно ли из кода и комментариев что происходит?</p></li></ol><p><strong>Не проверяйте:</strong></p><ul><li><p>Стиль форматирования (для этого есть линтеры и форматтеры)</p></li><li><p>Личные предпочтения (если оба подхода корректны)</p></li></ul><p><strong>Тон комментариев:</strong></p><p></p><p><code><span class="tmiEmoji" title="">❌</span> Это неправильно, так делать нельзя </code></p><p><code><span class="tmiEmoji" title="">✅</span> Здесь возможно переполнение при dlc &gt; 8, как насчёт проверки?  </code></p><p><code><span class="tmiEmoji" title="">❌</span> Почему ты использовал цикл вместо map()? </code></p><p><code><span class="tmiEmoji" title="">✅</span> Можно ли тут использовать list comprehension для читаемости?  </code></p><p><code><span class="tmiEmoji" title="">❌</span> Нет, переделай. </code></p><p><code><span class="tmiEmoji" title="">✅</span> Мне кажется, паттерн Strategy тут подошёл бы лучше — как думаешь? </code></p><hr><h2>GitHub Actions: автоматизация всего</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  # ===== ЛИНТИНГ И СТАТИЧЕСКИЙ АНАЛИЗ =====
  lint:
    name: Lint &amp; Static Analysis
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          cache: 'pip'
      
      - name: Install dependencies
        run: |
          pip install flake8 pylint mypy black isort
          pip install -r requirements.txt
      
      - name: Check formatting (black)
        run: black --check --diff .
      
      - name: Check imports (isort)
        run: isort --check-only --diff .
      
      - name: Lint (flake8)
        run: flake8 . --max-line-length=100 --exclude=.venv,migrations
      
      - name: Type check (mypy)
        run: mypy src/ --strict --ignore-missing-imports

  # ===== ТЕСТИРОВАНИЕ =====
  test:
    name: Unit &amp; Integration Tests
    runs-on: ubuntu-latest
    needs: lint
    
    services:
      # Поднимаем сервисы для интеграционных тестов
      redis:
        image: redis:7
        ports: ['6379:6379']
      
      mosquitto:
        image: eclipse-mosquitto:2
        ports: ['1883:1883']
    
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12']  # Тестируем на всех версиях
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v4
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
      
      - name: Install dependencies
        run: pip install -r requirements.txt -r requirements-dev.txt
      
      - name: Run tests with coverage
        run: |
          pytest tests/ \
            --cov=src \
            --cov-report=xml \
            --cov-report=term-missing \
            --cov-fail-under=80 \
            -v
        env:
          REDIS_URL: redis://localhost:6379
          MQTT_HOST: localhost
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.xml

  # ===== СБОРКА DOCKER ОБРАЗА =====
  build:
    name: Build &amp; Push Docker Image
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64  # Для x86 серверов И Raspberry Pi
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  # ===== ДЕПЛОЙ =====
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: deploy
          key: ${{ secrets.STAGING_SSH_KEY }}
          script: |
            cd /opt/gateway
            docker compose pull
            docker compose up -d --remove-orphans
            docker compose ps
  
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production  # Требует одобрения в GitHub
    
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/gateway
            export IMAGE_TAG=${{ github.ref_name }}
            docker compose pull
            docker compose up -d --remove-orphans
            
            # Smoke test
            sleep 10
            curl -f http://localhost:8080/health || (docker compose logs &amp;&amp; exit 1)
</code></pre><hr><h2>Git Hooks: автоматизация на уровне репозитория</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># .git/hooks/pre-commit (запускается перед каждым коммитом)
#!/bin/bash
set -e

echo "</code></pre><p><code><span class="tmiEmoji" title="">🔍</span> Pre-commit проверки..." # Форматирование Python if command -v black &amp;&gt; /dev/null; then</code></p><p><code>black --check . --quiet</code></p><p><code>if [ $? -ne 0 ]; then</code></p><p><code>echo "<span class="tmiEmoji" title="">❌</span> Форматирование не соответствует black. Запустите: black ."</code></p><p><code>exit 1</code></p><p><code>fi fi # Быстрые тесты (только изменённые файлы)</code></p><p><code>CHANGED_PY=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$') if [ -n "$CHANGED_PY" ]; then pytest tests/unit/ -x -q --tb=short fi</code></p><p><code>echo "<span class="tmiEmoji" title="">✅</span> Все проверки прошли"</code></p><p></p><h3>Лучше использовать pre-commit framework:</h3><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-json
      - id: check-merge-conflict
      - id: detect-private-key    # Не допускаем секреты в коде!
  
  - repo: https://github.com/psf/black
    rev: 23.10.1
    hooks:
      - id: black
  
  - repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
  
  - repo: https://github.com/commitizen-tools/commitizen
    rev: v3.12.0
    hooks:
      - id: commitizen  # Проверяет формат сообщения коммита
</code></pre><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Установка:
pip install pre-commit
pre-commit install  # Устанавливает хуки в .git/hooks/
pre-commit run --all-files  # Запуск вручную
</code></pre><hr><h2>Семантическое версионирование и автоматический релиз</h2><h3>Semantic Versioning: MAJOR.MINOR.PATCH</h3><ul><li><p><strong>PATCH</strong> (1.2.3 → 1.2.4): багфиксы, обратно совместимые изменения</p></li><li><p><strong>MINOR</strong> (1.2.4 → 1.3.0): новая функциональность, обратно совместимая</p></li><li><p><strong>MAJOR</strong> (1.3.0 → 2.0.0): несовместимые изменения API</p></li></ul><h3>Автоматический выпуск с commitizen:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># pyproject.toml:
[tool.commitizen]
name = "cz_conventional_commits"
tag_format = "v$version"
version_scheme = "semver"
version_files = [
    "src/__init__.py:__version__",
    "pyproject.toml:version"
]
update_changelog_on_bump = true

# Команды:
cz bump              # Автоматически определяет тип bumpa из коммитов
cz bump --major      # Принудительно major
cz changelog         # Генерирует CHANGELOG.md
</code></pre><p>Автоматический CHANGELOG.md из conventional commits:</p><pre spellcheck="" class="tmiCode language-markdown" data-language="Markdown"><code>## v1.3.0 (2024-03-15)

### Features
- **modbus**: добавлена поддержка FC15 (write multiple coils) (#247)
- **historian**: реализован deadband-алгоритм сжатия, экономия 78% места

### Bug Fixes
- **uart**: исправлена потеря байт при высоких нагрузках (#251)
- **pid**: устранено интегральное насыщение при длительной работе

### Performance
- **batch-write**: оптимизирован bulk insert в InfluxDB, +340% throughput
</code></pre><hr><h2>Практические советы</h2><h3>.gitignore — не игнорируйте важное</h3><pre spellcheck="" class="tmiCode" data-language="gitignore"><code># Python
__pycache__/
*.pyc
*.pyo
.venv/
.env
venv/

# IDE
.idea/
.vscode/
*.swp
*.swo

# Сборка
dist/
build/
*.egg-info/

# Тесты
.coverage
htmlcov/
.pytest_cache/

# Секреты (НИКОГДА не коммитить!)
*.key
*.pem
.env.local
config.secret.yaml

# OS
.DS_Store
Thumbs.db
</code></pre><h3>Работа с секретами — никогда в репозиторий!</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Плохо: секреты в коде
MQTT_PASSWORD = "supersecret123"

# Хорошо: из переменных окружения
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD")  # В .env локально, в CI — secrets

# Для локальной разработки: .env файл (в .gitignore!)
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()

# Для проверки что секрет не утёк:
git log --all --full-history -- "*.env"  # Поиск в истории
git grep "supersecret"  # Поиск в текущем состоянии
</code></pre><h3>Интерактивный rebase для чистой истории</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Перед мержем PR привести историю в порядок
git rebase -i origin/main

# Редактор покажет:
# pick a1b2c3 WIP fix something
# pick d4e5f6 another fix  
# pick g7h8i9 добавил логирование

# Меняем на:
# reword a1b2c3 fix(modbus): исправлен CRC при DLC=0
# squash d4e5f6  # Объединить с предыдущим
# pick g7h8i9 feat(logging): добавлено структурированное логирование

# Результат: чистая, осмысленная история
</code></pre><hr><h2>Заключение</h2><p>Git — это не инструмент, это язык коммуникации в команде. Правильные коммиты рассказывают историю проекта. Грамотное ветвление изолирует работу. CI/CD устраняет ручной труд и человеческие ошибки при деплое.</p><p>Начните с малого: установите <code>.pre-commit-config.yaml</code> с <code>black</code> и <code>detect-private-key</code>. Перейдите на conventional commits. Добавьте один GitHub Actions workflow с тестами. Каждый из этих шагов принесёт немедленную пользу.</p><p>Инвестиция в культуру работы с кодом возвращается многократно: меньше времени на дебаггинг, меньше страха перед деплоем, больше времени на реальную разработку.</p>]]></description><guid isPermaLink="false">116</guid><pubDate>Sat, 21 Mar 2026 17:30:43 +0000</pubDate></item><item><title>Python &#x434;&#x43B;&#x44F; &#x438;&#x43D;&#x436;&#x435;&#x43D;&#x435;&#x440;&#x43E;&#x432;: &#x430;&#x432;&#x442;&#x43E;&#x43C;&#x430;&#x442;&#x438;&#x437;&#x430;&#x446;&#x438;&#x44F;, &#x43E;&#x431;&#x440;&#x430;&#x431;&#x43E;&#x442;&#x43A;&#x430; &#x434;&#x430;&#x43D;&#x43D;&#x44B;&#x445; &#x438; &#x43F;&#x440;&#x43E;&#x43C;&#x44B;&#x448;&#x43B;&#x435;&#x43D;&#x43D;&#x44B;&#x435; &#x43F;&#x440;&#x438;&#x43B;&#x43E;&#x436;&#x435;&#x43D;&#x438;&#x44F;</title><link>https://ithub.uno/statiarticles/1_articles/python-%D0%B4%D0%BB%D1%8F-%D0%B8%D0%BD%D0%B6%D0%B5%D0%BD%D0%B5%D1%80%D0%BE%D0%B2-%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85-%D0%B8-%D0%BF%D1%80%D0%BE%D0%BC%D1%8B%D1%88%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D0%B5-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F-r125/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/piclumen-1732447895257.webp.d6b8b65d3238c12861edd8fa5dcbef99.webp" /></p>
<h2>Python — второй язык каждого инженера</h2><p>Matlab стоит дорого. LabVIEW — ещё дороже. Excel мощный, но у него есть потолок. Python — бесплатный, открытый, с огромной экосистемой библиотек для инженерных задач. И с каждым годом он глубже проникает в промышленность.</p><p>Инженер-электронщик использует Python для: анализа данных с измерительных приборов, автоматизации рутинных расчётов, создания отчётов, обработки сигналов с АЦП, управления лабораторным оборудованием (VISA/PyVISA), прототипирования алгоритмов перед переносом на микроконтроллер.</p><p>Специалист АСУ ТП — для: работы с Modbus/OPC UA, парсинга логов ПЛК, автоматического тестирования, интеграции различных систем.</p><hr><h2>NumPy: числа быстро</h2><p>NumPy — фундамент научного Python. Массивы NumPy в 10–100 раз быстрее списков Python для математических операций.</p><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>import numpy as np
import time

# ===== БАЗОВЫЕ ОПЕРАЦИИ =====

# Создание массивов
t = np.linspace(0, 10, 1000)    # 1000 точек от 0 до 10
f = np.arange(0, 50, 0.1)       # От 0 до 50 с шагом 0.1
zeros = np.zeros((3, 4))         # Матрица 3×4 из нулей
eye = np.eye(3)                  # Единичная матрица 3×3

# Синтетический сигнал (для теста)
freq_signal = 50.0   # Гц
freq_noise  = 200.0  # Гц (помеха)
sample_rate = 1000.0 # Гц

t = np.arange(0, 1, 1/sample_rate)  # 1 секунда данных

signal_clean = 2.0 * np.sin(2 * np.pi * freq_signal * t)
noise        = 0.5 * np.sin(2 * np.pi * freq_noise * t)
noise       += 0.2 * np.random.randn(len(t))  # Белый шум

signal_noisy = signal_clean + noise

# ===== СКОРОСТЬ =====
def python_rms(data: list) -&gt; float:
    return (sum(x**2 for x in data) / len(data)) ** 0.5

def numpy_rms(data: np.ndarray) -&gt; float:
    return np.sqrt(np.mean(data**2))

# Сравнение скорости:
data_list = list(signal_noisy)
data_arr  = np.array(data_list)

t0 = time.time(); python_rms(data_list); t_py = time.time() - t0
t0 = time.time(); numpy_rms(data_arr);  t_np = time.time() - t0

print(f"Python: {t_py*1000:.2f} мс, NumPy: {t_np*1000:.3f} мс, "
      f"Ускорение: {t_py/t_np:.0f}x")

# ===== ИНЖЕНЕРНЫЕ РАСЧЁТЫ =====

def calculate_power_factor(voltage: np.ndarray, current: np.ndarray,
                             sample_rate: float) -&gt; dict:
    """
    Расчёт коэффициента мощности из осциллограмм тока и напряжения.
    """
    # RMS значения
    V_rms = np.sqrt(np.mean(voltage**2))
    I_rms = np.sqrt(np.mean(current**2))
    
    # Активная мощность (среднее произведение)
    P = np.mean(voltage * current)
    
    # Полная мощность
    S = V_rms * I_rms
    
    # Коэффициент мощности
    pf = P / S if S &gt; 0 else 0
    
    # Реактивная мощность
    Q = np.sqrt(max(0, S**2 - P**2))
    
    return {
        'V_rms':  round(V_rms, 2),
        'I_rms':  round(I_rms, 3),
        'P_kw':   round(P / 1000, 2),
        'Q_kvar': round(Q / 1000, 2),
        'S_kva':  round(S / 1000, 2),
        'PF':     round(abs(pf), 3),
    }

# Пример использования:
# Генерируем тестовые сигналы 220В 50Гц, ток 10А с φ=30°
t = np.linspace(0, 0.04, 400)  # 2 периода
V = 220 * np.sqrt(2) * np.sin(2 * np.pi * 50 * t)
I = 10  * np.sqrt(2) * np.sin(2 * np.pi * 50 * t - np.radians(30))

power = calculate_power_factor(V, I, 10000)
print(f"P={power['P_kw']} кВт, Q={power['Q_kvar']} квар, cos(φ)={power['PF']}")
# Ожидаем: cos(30°) ≈ 0.866
</code></pre><hr><h2>Scipy: анализ сигналов</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>from scipy import signal, fft
import numpy as np

# ===== FFT: СПЕКТРАЛЬНЫЙ АНАЛИЗ =====

def analyze_spectrum(data: np.ndarray, sample_rate: float) -&gt; dict:
    """
    Анализ спектра сигнала через FFT.
    Используется для диагностики вибраций, качества электроэнергии.
    """
    n = len(data)
    
    # Оконная функция (Hanning) для уменьшения спектральных утечек
    window = np.hanning(n)
    data_windowed = data * window
    
    # FFT
    spectrum = np.abs(fft.rfft(data_windowed))
    freqs    = fft.rfftfreq(n, 1.0/sample_rate)
    
    # Нормировка (учёт оконной функции)
    spectrum = spectrum / (n / 2)
    
    # THD (Total Harmonic Distortion) — для качества сетевого напряжения
    # Находим основную частоту (50 Гц)
    fundamental_idx = np.argmin(np.abs(freqs - 50.0))
    fundamental_amp = spectrum[fundamental_idx]
    
    # Гармоники 2-я...7-я
    harmonic_power = sum(
        spectrum[np.argmin(np.abs(freqs - 50.0 * n))]**2
        for n in range(2, 8)
    )
    
    thd = np.sqrt(harmonic_power) / fundamental_amp * 100  # %
    
    # Топ-5 пиков спектра
    peak_indices = np.argsort(spectrum)[-10:][::-1]
    top_peaks = [(round(freqs[i], 1), round(spectrum[i], 4)) for i in peak_indices]
    
    return {
        'freqs':    freqs,
        'spectrum': spectrum,
        'thd_pct':  round(thd, 2),
        'top_peaks': top_peaks[:5],
        'rms':       round(np.sqrt(np.mean(data**2)), 4),
    }


# ===== ФИЛЬТРАЦИЯ СИГНАЛОВ =====

def design_lowpass_filter(cutoff_hz: float, sample_rate: float,
                            order: int = 4) -&gt; tuple:
    """
    Проектирование фильтра нижних частот Баттерворта.
    Используется для сглаживания зашумлённых данных датчиков.
    """
    nyquist = sample_rate / 2
    normalized_cutoff = cutoff_hz / nyquist
    
    b, a = signal.butter(order, normalized_cutoff, btype='low', analog=False)
    return b, a


def apply_filter(data: np.ndarray, b: np.ndarray, a: np.ndarray,
                  zero_phase: bool = True) -&gt; np.ndarray:
    """
    Применение фильтра к сигналу.
    zero_phase=True: filtfilt (нет фазового сдвига, требует данных полностью)
    zero_phase=False: lfilter (реального времени, есть фазовый сдвиг)
    """
    if zero_phase:
        return signal.filtfilt(b, a, data)  # Двупроходной (офлайн-обработка)
    else:
        return signal.lfilter(b, a, data)   # Однопроходной (онлайн-обработка)


# Пример: фильтрация зашумлённого датчика температуры
sample_rate = 100.0  # 100 Гц
t = np.arange(0, 10, 1/sample_rate)

# Реальная температура (медленно меняется)
true_temp = 75.0 + 5.0 * np.sin(2 * np.pi * 0.1 * t)  # 0.1 Гц

# С шумом (50Гц помеха от сети + белый шум)
noisy_temp = true_temp + 2.0 * np.sin(2 * np.pi * 50 * t) + \
             0.5 * np.random.randn(len(t))

# Фильтр НЧ с частотой среза 1 Гц (убираем всё выше 1 Гц)
b, a = design_lowpass_filter(cutoff_hz=1.0, sample_rate=sample_rate)
filtered_temp = apply_filter(noisy_temp, b, a)

print(f"Шум до фильтрации:  {np.std(noisy_temp - true_temp):.3f}°C")
print(f"Шум после фильтра: {np.std(filtered_temp - true_temp):.3f}°C")


# ===== КОРРЕЛЯЦИЯ И ОБНАРУЖЕНИЕ СИГНАЛА =====

def find_pattern_in_signal(signal_data: np.ndarray, 
                             pattern: np.ndarray) -&gt; list[int]:
    """
    Поиск паттерна в сигнале через кросс-корреляцию.
    Применение: нахождение пакетов в потоке данных, обнаружение событий.
    """
    correlation = np.correlate(signal_data, pattern, mode='valid')
    threshold = 0.8 * np.max(np.abs(correlation))
    
    peaks, _ = signal.find_peaks(correlation, height=threshold, distance=len(pattern))
    return list(peaks)
</code></pre><hr><h2>Pandas: анализ промышленных данных</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# ===== ЗАГРУЗКА И ОЧИСТКА ДАННЫХ =====

def load_plc_log(filepath: str) -&gt; pd.DataFrame:
    """
    Загрузка и нормализация лога ПЛК.
    Типичный формат: CSV с временной меткой и значениями тегов.
    """
    df = pd.read_csv(filepath, 
                      parse_dates=['timestamp'],
                      index_col='timestamp')
    
    # Нормализация имён колонок
    df.columns = df.columns.str.lower().str.replace(' ', '_').str.replace('.', '_')
    
    # Приведение типов
    numeric_cols = ['temperature', 'pressure', 'current', 'flow']
    for col in numeric_cols:
        if col in df.columns:
            df[col] = pd.to_numeric(df[col], errors='coerce')
    
    bool_cols = ['running', 'fault', 'alarm']
    for col in bool_cols:
        if col in df.columns:
            df[col] = df[col].astype(bool, errors='ignore')
    
    # Удаление дубликатов
    df = df[~df.index.duplicated(keep='first')]
    
    # Сортировка по времени
    df = df.sort_index()
    
    # Интерполяция пропущенных значений (не более 5 пропусков подряд)
    df[numeric_cols] = df[numeric_cols].interpolate(
        method='time', limit=5, limit_direction='forward'
    )
    
    return df


def analyze_production_data(df: pd.DataFrame) -&gt; dict:
    """
    Анализ производственных данных: KPI, простои, отклонения.
    """
    results = {}
    
    # ===== ДОСТУПНОСТЬ ОБОРУДОВАНИЯ =====
    if 'running' in df.columns:
        total_time   = (df.index[-1] - df.index[0]).total_seconds() / 3600  # часы
        running_time = df['running'].mean() * total_time
        
        results['availability'] = {
            'total_hours':   round(total_time, 1),
            'running_hours': round(running_time, 1),
            'availability_pct': round(df['running'].mean() * 100, 1),
        }
    
    # ===== АНАЛИЗ ПРОСТОЕВ =====
    if 'running' in df.columns:
        # Нахождение периодов простоя
        running_changes = df['running'].astype(int).diff()
        stop_times  = df.index[running_changes == -1]  # Моменты остановки
        start_times = df.index[running_changes == 1]   # Моменты пуска
        
        downtimes = []
        for stop in stop_times:
            # Найти следующий пуск после остановки
            next_start = start_times[start_times &gt; stop]
            if len(next_start) &gt; 0:
                duration = (next_start[0] - stop).total_seconds() / 60  # минуты
                downtimes.append({'stop': stop, 'start': next_start[0],
                                   'duration_min': round(duration, 1)})
        
        if downtimes:
            dt_df = pd.DataFrame(downtimes)
            results['downtimes'] = {
                'count':        len(dt_df),
                'total_min':    round(dt_df['duration_min'].sum(), 1),
                'avg_min':      round(dt_df['duration_min'].mean(), 1),
                'max_min':      round(dt_df['duration_min'].max(), 1),
                'longest_stop': dt_df.loc[dt_df['duration_min'].idxmax(), 'stop'].isoformat(),
            }
    
    # ===== СТАТИСТИКА ПАРАМЕТРОВ =====
    numeric_cols = df.select_dtypes(include=np.number).columns.tolist()
    if numeric_cols:
        stats = df[numeric_cols].describe()
        results['parameters'] = stats.to_dict()
    
    # ===== ОБНАРУЖЕНИЕ ВЫБРОСОВ (метод IQR) =====
    outliers = {}
    for col in numeric_cols:
        Q1 = df[col].quantile(0.25)
        Q3 = df[col].quantile(0.75)
        IQR = Q3 - Q1
        
        mask = (df[col] &lt; Q1 - 1.5 * IQR) | (df[col] &gt; Q3 + 1.5 * IQR)
        outlier_count = mask.sum()
        
        if outlier_count &gt; 0:
            outliers[col] = {
                'count': int(outlier_count),
                'pct':   round(outlier_count / len(df) * 100, 2),
                'examples': df[mask][col].head(3).tolist(),
            }
    
    results['outliers'] = outliers
    
    return results


# ===== ГЕНЕРАЦИЯ ОТЧЁТОВ =====

def generate_daily_report(df: pd.DataFrame, date: str = None) -&gt; pd.DataFrame:
    """Сводная таблица по часам за день"""
    if date:
        df = df[df.index.date == pd.Timestamp(date).date()]
    
    # Агрегация по часам
    hourly = df.resample('1h').agg({
        'temperature': ['mean', 'min', 'max'],
        'current':     ['mean', 'max'],
        'pressure':    ['mean', 'min', 'max'],
        'running':     'mean',  # Доступность за час
        'fault':       'any',   # Были ли аварии
    }).round(2)
    
    # Плоские имена колонок
    hourly.columns = ['_'.join(col) for col in hourly.columns]
    hourly['availability_pct'] = (hourly['running_mean'] * 100).round(1)
    hourly['had_fault'] = hourly['fault_any']
    
    return hourly


# ===== EXCEL ОТЧЁТ =====

def export_to_excel(df: pd.DataFrame, hourly: pd.DataFrame,
                     kpi: dict, filepath: str):
    """Красивый Excel-отчёт с несколькими листами"""
    
    with pd.ExcelWriter(filepath, engine='xlsxwriter') as writer:
        workbook = writer.book
        
        # Форматы
        header_fmt = workbook.add_format({
            'bold': True, 'bg_color': '#2C3E50', 'font_color': 'white',
            'border': 1
        })
        number_fmt = workbook.add_format({'num_format': '0.0#', 'border': 1})
        pct_fmt    = workbook.add_format({'num_format': '0.0%', 'border': 1})
        bad_fmt    = workbook.add_format({'bg_color': '#FFB3B3', 'border': 1})
        
        # ===== Лист 1: KPI =====
        ws_kpi = workbook.add_worksheet('KPI')
        ws_kpi.write('A1', 'Показатель', header_fmt)
        ws_kpi.write('B1', 'Значение',   header_fmt)
        
        avail = kpi.get('availability', {})
        row = 1
        for key, val in avail.items():
            ws_kpi.write(row, 0, key)
            ws_kpi.write(row, 1, val)
            row += 1
        
        ws_kpi.set_column('A:A', 25)
        ws_kpi.set_column('B:B', 15)
        
        # ===== Лист 2: Почасовой отчёт =====
        hourly.to_excel(writer, sheet_name='Почасовой отчёт', startrow=1)
        ws = writer.sheets['Почасовой отчёт']
        
        # Условное форматирование: красим аварийные часы
        ws.conditional_format('A2:Z1000', {
            'type': 'formula',
            'criteria': '=$G2=TRUE',  # Если был fault
            'format': bad_fmt
        })
        
        # ===== Лист 3: Сырые данные (последние 1000 строк) =====
        df.tail(1000).to_excel(writer, sheet_name='Данные')
        
    print(f"Отчёт сохранён: {filepath}")
</code></pre><hr><h2>FastAPI: REST API для промышленных данных</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code># pip install fastapi uvicorn
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
import uvicorn

app = FastAPI(
    title="Industrial Data API",
    description="API для доступа к данным производственного оборудования",
    version="1.0.0"
)

# Модели данных
class TelemetryPoint(BaseModel):
    device:      str
    temperature: float
    current:     float
    pressure:    float
    running:     bool
    timestamp:   datetime

class DeviceCommand(BaseModel):
    device:   str
    command:  str    # "start", "stop", "set_setpoint"
    value:    Optional[float] = None
    operator: str

# Имитация БД (в реальности — запросы к InfluxDB/TimescaleDB)
telemetry_db = []

@app.get("/api/v1/devices", summary="Список устройств")
async def get_devices():
    return {
        "devices": [
            {"id": "pump1", "name": "Насос 1", "location": "Линия 1", "online": True},
            {"id": "pump2", "name": "Насос 2", "location": "Линия 1", "online": True},
            {"id": "valve1","name": "Клапан 1","location": "Линия 2","online": False},
        ]
    }

@app.get("/api/v1/telemetry/{device_id}", summary="Телеметрия устройства")
async def get_telemetry(
    device_id: str,
    hours:     int   = Query(default=1,  ge=1, le=720, description="Глубина истории"),
    resample:  str   = Query(default="1min", description="Гранулярность: 1s, 1min, 5min, 1h")
):
    # Проверка устройства
    valid_devices = ["pump1", "pump2", "valve1"]
    if device_id not in valid_devices:
        raise HTTPException(status_code=404, detail=f"Устройство '{device_id}' не найдено")
    
    # Запрос к historian (имитация)
    # В реальности: query_influxdb(device_id, hours, resample)
    return {
        "device":    device_id,
        "from":      (datetime.now().replace(hour=0, minute=0)).isoformat(),
        "to":        datetime.now().isoformat(),
        "resample":  resample,
        "points":    [
            {"time": datetime.now().isoformat(), "temperature": 85.3,
             "current": 15.2, "pressure": 5.8, "running": True}
        ]
    }

@app.post("/api/v1/commands", summary="Отправить команду устройству",
          status_code=202)
async def send_command(cmd: DeviceCommand):
    # Валидация команды
    valid_commands = ["start", "stop", "set_setpoint"]
    if cmd.command not in valid_commands:
        raise HTTPException(status_code=400,
                           detail=f"Неизвестная команда: {cmd.command}")
    
    if cmd.command == "set_setpoint" and cmd.value is None:
        raise HTTPException(status_code=400,
                           detail="set_setpoint требует параметр value")
    
    # Аудит-лог (обязательно для промышленных систем!)
    print(f"[AUDIT] {datetime.now()} | Operator: {cmd.operator} | "
          f"Device: {cmd.device} | Command: {cmd.command} | Value: {cmd.value}")
    
    # Отправить команду (через очередь, Modbus, OPC UA...)
    # command_queue.put(cmd)
    
    return {"status": "accepted", "command_id": "cmd_123456"}

@app.get("/api/v1/health", summary="Healthcheck")
async def health():
    return {"status": "ok", "timestamp": datetime.now().isoformat()}

# Запуск: uvicorn main:app --host 0.0.0.0 --port 8080 --reload
</code></pre><hr><h2>asyncio: асинхронный опрос оборудования</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>import asyncio
import aiohttp
import json
from datetime import datetime

async def poll_device_modbus(device_id: str, host: str, interval: float = 1.0):
    """Асинхронный опрос устройства через Modbus TCP"""
    from pymodbus.client import AsyncModbusTcpClient
    
    async with AsyncModbusTcpClient(host=host, port=502) as client:
        print(f"Подключён к {device_id} ({host})")
        
        while True:
            start = asyncio.get_event_loop().time()
            
            try:
                result = await client.read_input_registers(address=0, count=4, slave=1)
                
                if not result.isError():
                    data = {
                        'device':      device_id,
                        'timestamp':   datetime.now().isoformat(),
                        'temperature': result.registers[0] / 10.0,
                        'current':     result.registers[1] / 10.0,
                        'pressure':    result.registers[2] / 100.0,
                        'running':     bool(result.registers[3] &amp; 1),
                    }
                    # Публикуем данные (в очередь, БД, MQTT...)
                    print(f"{device_id}: T={data['temperature']}°C")
                else:
                    print(f"{device_id}: Ошибка Modbus")
            
            except Exception as e:
                print(f"{device_id}: {e}")
                await asyncio.sleep(5)  # Пауза перед повтором
                continue
            
            # Точный интервал опроса
            elapsed = asyncio.get_event_loop().time() - start
            await asyncio.sleep(max(0, interval - elapsed))


async def main():
    """Параллельный опрос нескольких устройств"""
    
    devices = [
        ("pump1",  "192.168.1.10"),
        ("pump2",  "192.168.1.11"),
        ("valve1", "192.168.1.12"),
    ]
    
    # Запускаем все опросы параллельно
    tasks = [poll_device_modbus(dev_id, host, interval=1.0)
             for dev_id, host in devices]
    
    await asyncio.gather(*tasks)  # Все работают одновременно!

asyncio.run(main())
</code></pre><hr><h2>Полезные однострочники для инженера</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>import subprocess, json, struct, serial
from pathlib import Path

# Быстрый Modbus опрос из командной строки:
# python -c "from pymodbus.client import ModbusTcpClient; c=ModbusTcpClient('192.168.1.10'); c.connect(); print(c.read_input_registers(0,4,slave=1).registers)"

# Конвертация hex-дампа в float:
def hex_to_float(hex_str: str) -&gt; float:
    return struct.unpack('&gt;f', bytes.fromhex(hex_str.replace(' ','')))[0]

print(hex_to_float("42 48 00 00"))  # → 50.0

# Поиск COM-портов:
import serial.tools.list_ports
for p in serial.tools.list_ports.comports():
    print(f"{p.device}: {p.description}")

# Быстрый парсинг CSV с временными метками:
df = pd.read_csv('data.csv', parse_dates=['time'], index_col='time')
print(df.resample('5min').mean())

# Сохранение данных в Parquet (быстрее CSV в 10-50 раз):
df.to_parquet('data.parquet', compression='snappy')
df2 = pd.read_parquet('data.parquet')
</code></pre><hr><h2>Заключение</h2><p>Python — это не замена C для микроконтроллеров и не замена SQL для баз данных. Это клей, который соединяет всё: читает данные из любого источника, анализирует, визуализирует, отправляет куда надо.</p><p>Для инженера ключевые библиотеки: NumPy (быстрые вычисления), Pandas (анализ данных), SciPy (сигналы и системы), Matplotlib/Plotly (визуализация), pymodbus (Modbus), pyserial (UART), asyncua (OPC UA), FastAPI (REST API).</p><p>Вложите неделю в изучение NumPy и Pandas — окупится сотнями часов сэкономленного времени на анализе данных, отчётах и автоматизации рутины.</p>]]></description><guid isPermaLink="false">125</guid><pubDate>Sat, 21 Mar 2026 17:37:15 +0000</pubDate></item><item><title>Docker &#x438; &#x43A;&#x43E;&#x43D;&#x442;&#x435;&#x439;&#x43D;&#x435;&#x440;&#x438;&#x437;&#x430;&#x446;&#x438;&#x44F;: &#x43F;&#x440;&#x430;&#x43A;&#x442;&#x438;&#x447;&#x435;&#x441;&#x43A;&#x43E;&#x435; &#x440;&#x443;&#x43A;&#x43E;&#x432;&#x43E;&#x434;&#x441;&#x442;&#x432;&#x43E; &#x434;&#x43B;&#x44F; &#x440;&#x430;&#x437;&#x440;&#x430;&#x431;&#x43E;&#x442;&#x447;&#x438;&#x43A;&#x430;</title><link>https://ithub.uno/statiarticles/1_articles/docker-%D0%B8-%D0%BA%D0%BE%D0%BD%D1%82%D0%B5%D0%B9%D0%BD%D0%B5%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-%D0%B4%D0%BB%D1%8F-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%87%D0%B8%D0%BA%D0%B0-r134/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/MD-2054-1.webp.140d467b01e4e113590c9b30d98aee64.webp" /></p>
<h2>Зачем Docker: "работает на моей машине"</h2><p>История, знакомая каждому разработчику. Приложение работает на вашем ноутбуке. Выкладываете на сервер — падает. Отличия: версия Python 3.9 vs 3.11, разные системные библиотеки, другая переменная PATH, конфликт зависимостей с другим приложением.</p><p>Docker решает эту проблему радикально: <strong>упаковывает приложение вместе со всем окружением</strong> — операционной системой, библиотеками, конфигурацией. Контейнер запускается одинаково везде: на ноутбуке разработчика, в CI/CD, на production-сервере, в облаке.</p><p>Дополнительные бонусы:</p><ul><li><p><strong>Изоляция:</strong> одно приложение не мешает другому</p></li><li><p><strong>Воспроизводимость:</strong> одинаковая среда у всей команды</p></li><li><p><strong>Быстрое развёртывание:</strong> <code>docker pull</code> + <code>docker run</code> вместо часа установки</p></li><li><p><strong>Масштабирование:</strong> запустить 10 экземпляров так же легко, как 1</p></li><li><p><strong>Чистота:</strong> удалить контейнер = никаких следов на хосте</p></li></ul><hr><h2>Основные концепции</h2><p><strong>Image (образ):</strong> Слоёная файловая система со всем необходимым. Неизменяемый шаблон. Хранится в Registry (Docker Hub, GitHub Container Registry, ваш собственный).</p><p><strong>Container (контейнер):</strong> Запущенный экземпляр образа. Изолированный процесс с собственной файловой системой, сетью, PID-пространством. Контейнеры ephemeral — данные исчезают при удалении (если нет Volume).</p><p><strong>Volume (том):</strong> Постоянное хранилище данных, переживает удаление контейнера.</p><p><strong>Network (сеть):</strong> Изолированная виртуальная сеть. Контейнеры в одной сети видят друг друга по имени.</p><p><strong>Dockerfile:</strong> Инструкции для сборки образа. Каждая инструкция — новый слой.</p><p><strong>Registry:</strong> Хранилище образов. Docker Hub — публичный. Можно развернуть свой (Harbor, Nexus).</p><hr><h2>Dockerfile: пишем правильно</h2><h3>Базовый пример (Python приложение):</h3><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code># Начинаем с официального образа Python
# ВАЖНО: всегда указывайте точную версию, не :latest!
FROM python:3.11-slim

# Метаданные
LABEL maintainer="your@email.com"
LABEL version="1.0"
LABEL description="Industrial IoT Gateway"

# Устанавливаем рабочую директорию
WORKDIR /app

# Копируем ТОЛЬКО файлы зависимостей первыми!
# Слои кешируются — если requirements.txt не изменился,
# pip install не запустится при следующей сборке
COPY requirements.txt .

# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt

# Копируем исходный код (изменяется чаще — поэтому последним)
COPY src/ ./src/
COPY config/ ./config/

# Создаём непривилегированного пользователя (безопасность!)
RUN groupadd -r appuser &amp;&amp; useradd -r -g appuser appuser
RUN chown -R appuser:appuser /app
USER appuser

# Переменные окружения
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    LOG_LEVEL=INFO

# Открываем порт (документация, не публикует сам по себе)
EXPOSE 8080

# Healthcheck: Docker проверяет живость контейнера
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# Команда запуска
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080"]
</code></pre><h3>Многоэтапная сборка (Multi-stage build):</h3><p>Критически важна для production. Финальный образ не содержит инструментов сборки (gcc, make, pip), что уменьшает размер и поверхность атаки:</p><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code># ===== ЭТАП 1: Сборка =====
FROM python:3.11 AS builder

WORKDIR /build

# Устанавливаем зависимости для сборки
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    gcc \
    libffi-dev \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

# Устанавливаем в отдельную директорию
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ===== ЭТАП 2: Production образ =====
FROM python:3.11-slim AS production

# Копируем только установленные пакеты из builder
COPY --from=builder /install /usr/local

WORKDIR /app

# Только runtime зависимости ОС
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    curl \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; apt-get clean

# Безопасность
RUN groupadd -r app &amp;&amp; useradd -r -g app -d /app -s /sbin/nologin app
COPY --chown=app:app src/ ./src/
USER app

ENV PYTHONUNBUFFERED=1
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8080/health
CMD ["python", "src/main.py"]

# Результат: образ ~180MB вместо ~900MB!
</code></pre><h3>.dockerignore — что НЕ копировать в образ:</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code># .dockerignore
**/__pycache__
**/*.pyc
**/*.pyo
.git
.gitignore
.venv
venv
*.env
.env.*
tests/
docs/
*.md
.github/
node_modules/
dist/
*.log
.DS_Store

# Секреты — никогда в образ!
*.key
*.pem
*secret*
config.local.*
</code></pre><hr><h2>Docker Compose: многосервисные приложения</h2><p>Docker Compose — инструмент для запуска нескольких связанных контейнеров.</p><h3>Полный пример: IoT-платформа</h3><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># docker-compose.yml
version: '3.9'

# Общие настройки через YAML anchors (DRY)
x-common-env: &amp;common-env
  TZ: Europe/Moscow
  LOG_LEVEL: ${LOG_LEVEL:-INFO}

x-restart-policy: &amp;restart-policy
  restart: unless-stopped

services:

  # ===== MQTT БРОКЕР =====
  mosquitto:
    image: eclipse-mosquitto:2.0.18
    &lt;&lt;: *restart-policy
    volumes:
      - ./mosquitto/config:/mosquitto/config:ro
      - mosquitto_data:/mosquitto/data
      - mosquitto_logs:/mosquitto/log
    ports:
      - "1883:1883"     # MQTT
      - "9001:9001"     # WebSocket
    networks:
      - iot_network
    healthcheck:
      test: ["CMD", "mosquitto_sub", "-t", "$$SYS/#", "-C", "1", "-i", "healthcheck"]
      interval: 30s
      timeout: 10s
      retries: 3

  # ===== БАЗА ДАННЫХ ВРЕМЕННЫХ РЯДОВ =====
  influxdb:
    image: influxdb:2.7
    &lt;&lt;: *restart-policy
    environment:
      &lt;&lt;: *common-env
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USER:-admin}
      DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD:?INFLUX_PASSWORD required}
      DOCKER_INFLUXDB_INIT_ORG: factory
      DOCKER_INFLUXDB_INIT_BUCKET: telemetry
      DOCKER_INFLUXDB_INIT_RETENTION: 30d
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN:?INFLUX_TOKEN required}
    volumes:
      - influxdb_data:/var/lib/influxdb2
      - influxdb_config:/etc/influxdb2
    ports:
      - "8086:8086"
    networks:
      - iot_network
      - monitoring_network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8086/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s

  # ===== GRAFANA =====
  grafana:
    image: grafana/grafana:10.3.1
    &lt;&lt;: *restart-policy
    depends_on:
      influxdb:
        condition: service_healthy
    environment:
      &lt;&lt;: *common-env
      GF_SECURITY_ADMIN_USER: ${GF_ADMIN_USER:-admin}
      GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD:?required}
      GF_SERVER_ROOT_URL: http://localhost:3000
      GF_SMTP_ENABLED: "true"
      GF_SMTP_HOST: ${SMTP_HOST:-localhost:25}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    ports:
      - "3000:3000"
    networks:
      - monitoring_network
    user: "472"  # grafana user

  # ===== IOT ШЛЮЗ (наш сервис) =====
  gateway:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
      args:
        BUILD_DATE: ${BUILD_DATE:-unknown}
        GIT_COMMIT: ${GIT_COMMIT:-unknown}
    &lt;&lt;: *restart-policy
    depends_on:
      mosquitto:
        condition: service_healthy
      influxdb:
        condition: service_healthy
    environment:
      &lt;&lt;: *common-env
      MQTT_HOST: mosquitto        # Имя сервиса = DNS-имя внутри сети!
      MQTT_PORT: 1883
      INFLUX_URL: http://influxdb:8086
      INFLUX_TOKEN: ${INFLUX_TOKEN}
      INFLUX_ORG: factory
      INFLUX_BUCKET: telemetry
      MODBUS_PORT: /dev/ttyUSB0   # Реальный порт с хоста
    volumes:
      - ./config:/app/config:ro
      - gateway_logs:/app/logs
    devices:
      - "/dev/ttyUSB0:/dev/ttyUSB0"  # Проброс USB-устройства
    ports:
      - "8080:8080"
    networks:
      - iot_network
      - monitoring_network
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 128M

  # ===== NGINX: реверс-прокси =====
  nginx:
    image: nginx:1.25-alpine
    &lt;&lt;: *restart-policy
    depends_on:
      - grafana
      - gateway
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro  # TLS сертификаты
    ports:
      - "80:80"
      - "443:443"
    networks:
      - monitoring_network

# ===== ТОМА =====
volumes:
  mosquitto_data:
  mosquitto_logs:
  influxdb_data:
  influxdb_config:
  grafana_data:
  gateway_logs:

# ===== СЕТИ =====
networks:
  iot_network:        # Для IoT-компонентов
    driver: bridge
  monitoring_network: # Для мониторинга и UI
    driver: bridge
</code></pre><h3>.env файл для Compose:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># .env — НЕ коммитить в git!
INFLUX_USER=admin
INFLUX_PASSWORD=super_secret_influx_pass
INFLUX_TOKEN=my-super-secret-token-change-this
GF_ADMIN_USER=admin
GF_ADMIN_PASSWORD=super_secret_grafana_pass
LOG_LEVEL=INFO
BUILD_DATE=2024-03-15
GIT_COMMIT=abc123
</code></pre><hr><h2>Docker: ключевые команды</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># ===== ОБРАЗЫ =====
docker build -t myapp:1.0 .           # Собрать образ
docker build --no-cache -t myapp:1.0 . # Без кеша
docker images                          # Список образов
docker rmi myapp:1.0                   # Удалить образ
docker image prune                     # Удалить неиспользуемые

# История слоёв (анализ размера):
docker history myapp:1.0

# ===== КОНТЕЙНЕРЫ =====
docker run -d \
  --name gateway \
  -p 8080:8080 \
  -e MQTT_HOST=192.168.1.100 \
  -v $(pwd)/config:/app/config:ro \
  --restart unless-stopped \
  myapp:1.0

docker ps                              # Запущенные контейнеры
docker ps -a                           # Все (включая остановленные)
docker logs gateway -f                 # Логи в реальном времени
docker logs gateway --tail 100         # Последние 100 строк
docker exec -it gateway bash           # Войти внутрь контейнера
docker stop gateway                    # Остановить
docker start gateway                   # Запустить
docker restart gateway                 # Перезапустить
docker rm gateway                      # Удалить (сначала stop)
docker stats                           # Потребление ресурсов

# ===== COMPOSE =====
docker compose up -d                   # Запустить все сервисы
docker compose up -d gateway           # Запустить только gateway
docker compose down                    # Остановить и удалить контейнеры
docker compose down -v                 # + удалить тома (ОСТОРОЖНО!)
docker compose logs -f                 # Логи всех сервисов
docker compose logs -f gateway         # Логи конкретного сервиса
docker compose ps                      # Статус сервисов
docker compose pull                    # Обновить образы
docker compose build --no-cache        # Пересобрать
docker compose restart gateway         # Перезапустить сервис
docker compose exec gateway bash       # Войти в контейнер

# ===== REGISTRY =====
docker tag myapp:1.0 ghcr.io/myorg/myapp:1.0
docker push ghcr.io/myorg/myapp:1.0
docker pull ghcr.io/myorg/myapp:1.0

# ===== ОЧИСТКА =====
docker system prune -a                 # Удалить ВСЁ неиспользуемое
docker volume prune                    # Удалить неиспользуемые тома
</code></pre><hr><h2>Volumes и persistence: не теряем данные</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># Типы монтирования:

services:
  app:
    volumes:
      # 1. Named Volume (рекомендуется для данных БД)
      - db_data:/var/lib/postgresql/data
      
      # 2. Bind Mount (для конфигов и разработки)
      - ./config:/app/config:ro    # :ro = read-only
      - ./src:/app/src             # Для hot-reload при разработке
      
      # 3. tmpfs (только в RAM, для временных данных)
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 100M

volumes:
  db_data:
    driver: local
    # Для production: внешние тома
    # external: true
    # name: prod_db_data
</code></pre><h3>Backup томов:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Резервная копия тома influxdb_data
docker run --rm \
  -v influxdb_data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine:3 \
  tar czf /backup/influxdb_$(date +%Y%m%d).tar.gz -C /source .

# Восстановление
docker run --rm \
  -v influxdb_data:/target \
  -v $(pwd)/backups:/backup:ro \
  alpine:3 \
  tar xzf /backup/influxdb_20240315.tar.gz -C /target
</code></pre><hr><h2>Оптимизация размера образа</h2><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code># </code></pre><p><code><span class="tmiEmoji" title="">❌</span> ПЛОХО: большой образ FROM ubuntu:22.04 RUN apt-get update RUN apt-get install -y python3 RUN apt-get install -y python3-pip RUN pip3 install flask COPY app.py . CMD ["python3", "app.py"] # Размер: ~480 МБ, 5 лишних слоёв  # </code></p><p><code><span class="tmiEmoji" title="">✅</span> ХОРОШО: оптимизированный образ FROM python:3.11-slim RUN pip install --no-cache-dir flask COPY app.py . CMD ["python", "app.py"] # Размер: ~85 МБ, чистая сборка  # </code></p><p><code><span class="tmiEmoji" title="">✅</span> ЕЩЁ ЛУЧШЕ: alpine (минимальный дистрибутив) FROM python:3.11-alpine # Некоторые C-расширения нужно собирать RUN apk add --no-cache gcc musl-dev &amp;&amp; \     pip install --no-cache-dir flask &amp;&amp; \     apk del gcc musl-dev COPY app.py . CMD ["python", "app.py"] # Размер: ~45 МБ </code></p><p><strong>Правило минимума слоёв для apt/apk:</strong></p><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code># Объединяйте RUN команды в одну для минимизации слоёв
RUN apt-get update &amp;&amp; \
    apt-get install -y --no-install-recommends \
        curl \
        libpq5 \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; apt-get clean
# ОДИН слой вместо нескольких, и сразу очистка кеша
</code></pre><hr><h2>Безопасность Docker</h2><h3>1. Никогда не запускать от root:</h3><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code># Создаём непривилегированного пользователя
RUN addgroup -S appgroup &amp;&amp; adduser -S appuser -G appgroup
USER appuser
</code></pre><h3>2. Read-only файловая система:</h3><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>services:
  app:
    read_only: true  # Файловая система только для чтения
    tmpfs:
      - /tmp         # Разрешаем запись только в tmpfs
      - /var/run
</code></pre><h3>3. Ограничение capabilities:</h3><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>services:
  app:
    cap_drop:
      - ALL          # Убираем ВСЕ capabilities
    cap_add:
      - NET_BIND_SERVICE  # Добавляем только необходимые
</code></pre><h3>4. Сканирование образов на уязвимости:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Trivy — бесплатный сканер (Aqua Security)
docker run --rm aquasec/trivy image myapp:1.0

# Или встроенный Docker Scout
docker scout cves myapp:1.0
</code></pre><h3>5. Secrets — не в переменных окружения production:</h3><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># docker-compose.yml с Docker secrets
services:
  app:
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password  # Читаем из файла!

secrets:
  db_password:
    file: ./secrets/db_password.txt  # или external: true для Swarm/K8s
</code></pre><hr><h2>Healthcheck и зависимости между сервисами</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>services:
  postgres:
    image: postgres:15
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s  # Даём время на инициализацию

  app:
    depends_on:
      postgres:
        condition: service_healthy  # Ждём пока postgres healthy!
      redis:
        condition: service_healthy
</code></pre><h3>Entrypoint-скрипт для ожидания зависимостей:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/bin/sh
# entrypoint.sh

set -e

echo "Ожидание готовности базы данных..."

until nc -z -w5 ${DB_HOST} ${DB_PORT}; do
    echo "База данных недоступна, ждём..."
    sleep 2
done

echo "База данных готова!"

# Запуск миграций
python manage.py migrate --no-input

# Запуск приложения
exec "$@"
</code></pre><pre spellcheck="" class="tmiCode language-dockerfile" data-language="Dockerfile"><code>COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "app:application"]
</code></pre><hr><h2>Docker в CI/CD (GitHub Actions)</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># .github/workflows/docker.yml
name: Build and Deploy

on:
  push:
    tags: ['v*.*.*']

jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64  # Multi-arch!
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.ref_name }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            GIT_COMMIT=${{ github.sha }}
            BUILD_DATE=${{ github.event.head_commit.timestamp }}

  deploy:
    needs: build-push
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/iot-platform
            
            # Обновляем образ
            docker compose pull gateway
            
            # Обновляем с нулевым downtime
            docker compose up -d --no-deps gateway
            
            # Ждём healthcheck
            sleep 15
            docker compose ps gateway | grep -q "healthy" || exit 1
            
            echo "Деплой успешен!"
</code></pre><hr><h2>Заключение: когда Docker, когда нет</h2><p><strong>Используйте Docker:</strong></p><ul><li><p>Приложения с множеством зависимостей</p></li><li><p>Многосервисные приложения (backend + БД + брокер + мониторинг)</p></li><li><p>CI/CD с гарантированной воспроизводимостью</p></li><li><p>Несколько приложений на одном сервере</p></li><li><p>Нужна возможность быстрого масштабирования</p></li></ul><p><strong>Не обязательно Docker:</strong></p><ul><li><p>Простые скрипты без зависимостей</p></li><li><p>Приложения с прямым доступом к оборудованию (хотя <code>--device</code> помогает)</p></li><li><p>Жёсткое реальное время (latency контейнера ~1 мкс, но это не ноль)</p></li><li><p>Команда незнакома с Docker — сначала обучение, потом внедрение</p></li></ul><p>Docker — это не серебряная пуля, но инвестиция в него окупается быстро. Начните с <code>docker-compose.yml</code> для локальной разработки — уже это даст ощутимый результат: одна команда <code>docker compose up</code> поднимает весь стек вместо часа настройки.</p>]]></description><guid isPermaLink="false">134</guid><pubDate>Sat, 21 Mar 2026 20:46:18 +0000</pubDate></item><item><title>SQL &#x434;&#x43B;&#x44F; &#x440;&#x430;&#x437;&#x440;&#x430;&#x431;&#x43E;&#x442;&#x447;&#x438;&#x43A;&#x43E;&#x432;: &#x43E;&#x442; &#x43E;&#x441;&#x43D;&#x43E;&#x432; &#x434;&#x43E; &#x43E;&#x43F;&#x442;&#x438;&#x43C;&#x438;&#x437;&#x430;&#x446;&#x438;&#x438; &#x437;&#x430;&#x43F;&#x440;&#x43E;&#x441;&#x43E;&#x432;</title><link>https://ithub.uno/statiarticles/1_articles/sql-%D0%B4%D0%BB%D1%8F-%D1%80%D0%B0%D0%B7%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D1%87%D0%B8%D0%BA%D0%BE%D0%B2-%D0%BE%D1%82-%D0%BE%D1%81%D0%BD%D0%BE%D0%B2-%D0%B4%D0%BE-%D0%BE%D0%BF%D1%82%D0%B8%D0%BC%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2-r143/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/sql-1024x536.jpg.d680d6f1d2254766993a360d21103c1b.jpg" /></p>
<h2>SQL: язык, которому 50 лет, но он не устарел</h2><p>SQL изобрели в IBM в 1974 году. С тех пор появились NoSQL, NewSQL, GraphQL, временны́е базы данных, документные хранилища. Но SQL не умер — он стал стандартом для большинства задач работы с данными.</p><p>Реляционные СУБД (PostgreSQL, MySQL, SQLite, MS SQL, Oracle) хранят данные в большинстве корпоративных систем мира. И даже "NoSQL" системы (ClickHouse, DuckDB, BigQuery) используют SQL-диалект.</p><p>Знание SQL — это инвестиция с гарантированной отдачей для любого разработчика.</p><hr><h2>Архитектура запроса: как PostgreSQL исполняет SQL</h2><p>Понимание этого даёт инсайт, почему одни запросы быстрые, а другие — нет:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Текст запроса
    ↓
[Parser] — проверка синтаксиса
    ↓
[Rewriter] — разворачивание Views, правила
    ↓
[Planner/Optimizer] — КЛЮЧЕВОЙ ЭТАП!
  - Оценка стоимости разных планов
  - Выбор порядка JOIN-ов
  - Выбор алгоритма соединения (Hash Join, Nested Loop, Merge Join)
  - Решение: использовать индекс или seq scan
    ↓
[Executor] — выполнение выбранного плана
    ↓
Результат
</code></pre><p>Планировщик работает на основе <strong>статистики</strong> (pg_statistics). Устаревшая статистика → неоптимальный план → медленный запрос. Поэтому важен <code>ANALYZE</code> или автовакуум.</p><hr><h2>EXPLAIN ANALYZE: видим что происходит</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Всегда используйте ANALYZE для реального времени (но он выполняет запрос!)
-- Для SELECT это безопасно. Для DML используйте ROLLBACK:
-- BEGIN; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.name, COUNT(o.id) as order_count, SUM(o.total) as total_sum
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.created_at &gt; '2024-01-01'
  AND o.status = 'completed'
GROUP BY u.id, u.name
ORDER BY total_sum DESC
LIMIT 20;

-- Типичный вывод и как его читать:
/*
Limit  (cost=1250.45..1250.50 rows=20 width=48) (actual time=45.231..45.234 rows=20 loops=1)
  -&gt;  Sort  (cost=1250.45..1253.95 rows=1400 width=48) (actual time=45.228..45.231 rows=20 loops=1)
        Sort Key: (sum(o.total)) DESC
        Sort Method: top-N heapsort  Memory: 27kB
        -&gt;  HashAggregate  (cost=1190.23..1204.73 rows=1400 width=48) (actual time=44.123..45.012 rows=1823 loops=1)
              Group Key: u.id, u.name
              Batches: 1  Memory Usage: 657kB
              -&gt;  Hash Join  (cost=485.30..1148.73 rows=8300 width=24) (actual time=2.341..38.201 rows=9843 loops=1)
                    Hash Cond: (o.user_id = u.id)
                    Buffers: shared hit=342 read=891   ← ВАЖНО! 891 блоков с диска!
                    -&gt;  Seq Scan on orders o  (cost=0.00..456.23 rows=12800 width=16)
                                              ↑ SEQ SCAN на большой таблице = тревожный сигнал!
                          Filter: ((status)::text = 'completed'::text)
                          Rows Removed by Filter: 23456
                    -&gt;  Hash  (cost=423.55..423.55 rows=4940 width=16) (actual time=2.103..2.103 rows=4892 loops=1)
                          -&gt;  Index Scan using idx_users_created on users u
                                Index Cond: (created_at &gt; '2024-01-01'::date)

Planning Time: 0.523 ms
Execution Time: 45.789 ms   ← Реальное время выполнения
*/
</code></pre><h3>Что искать в EXPLAIN:</h3><div class="tmiRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Сигнал</p></th><th colspan="1" rowspan="1"><p>Что значит</p></th><th colspan="1" rowspan="1"><p>Решение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>Seq Scan</code> на большой таблице</p></td><td colspan="1" rowspan="1"><p>Нет индекса или не используется</p></td><td colspan="1" rowspan="1"><p>Добавить индекс</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Rows Removed by Filter: N</code> (N &gt;&gt; результата)</p></td><td colspan="1" rowspan="1"><p>Фильтр работает после scan</p></td><td colspan="1" rowspan="1"><p>Индекс на колонку фильтра</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>shared read: N</code> (N &gt; 1000)</p></td><td colspan="1" rowspan="1"><p>Много чтений с диска</p></td><td colspan="1" rowspan="1"><p>Индекс, увеличить shared_buffers</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Nested Loop</code> при большом N</p></td><td colspan="1" rowspan="1"><p>Плохой алгоритм JOIN</p></td><td colspan="1" rowspan="1"><p>Статистика, индексы, rewrite</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Sort</code> без <code>using index</code></p></td><td colspan="1" rowspan="1"><p>Сортировка в памяти/диске</p></td><td colspan="1" rowspan="1"><p>Индекс на ORDER BY колонку</p></td></tr></tbody></table></div><hr><h2>Индексы: типы и когда применять</h2><h3>B-Tree (по умолчанию)</h3><p>Подходит для: =, &lt;, &gt;, BETWEEN, LIKE 'prefix%', ORDER BY, диапазоны дат.</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Обычный индекс
CREATE INDEX idx_orders_user_id ON orders(user_id);

-- Составной индекс (порядок важен!)
-- Покрывает: WHERE user_id = X AND status = Y
-- Покрывает: WHERE user_id = X (только первая колонка)
-- НЕ покрывает: WHERE status = Y (без user_id)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);

-- Частичный индекс (только для подмножества строк)
-- Гораздо меньше, работает быстрее для частых запросов с фильтром
CREATE INDEX idx_orders_active ON orders(created_at)
WHERE status IN ('pending', 'processing');

-- Индекс с включёнными колонками (covering index)
-- SELECT user_id, total FROM orders WHERE status = 'completed'
-- будет выполнен только из индекса, без обращения к таблице!
CREATE INDEX idx_orders_status_covering ON orders(status)
INCLUDE (user_id, total);
</code></pre><h3>Hash индекс</h3><p>Только для = (равенство). Быстрее B-Tree для равенства, но нет диапазонов:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>CREATE INDEX idx_sessions_token ON sessions USING HASH (token);
-- Отлично для: WHERE token = 'abc123' (авторизация)
</code></pre><h3>GIN (Generalized Inverted Index)</h3><p>Для массивов, JSONB, полнотекстового поиска, pg_trgm:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Полнотекстовый поиск
CREATE INDEX idx_articles_fts ON articles
USING GIN (to_tsvector('russian', title || ' ' || body));

-- Поиск по JSONB
CREATE INDEX idx_devices_meta ON devices USING GIN (metadata jsonb_path_ops);
-- Запрос: WHERE metadata @&gt; '{"type": "sensor"}'

-- pg_trgm для LIKE '%substring%' (иначе seq scan!)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE INDEX idx_products_name_trgm ON products USING GIN (name gin_trgm_ops);
-- Запрос: WHERE name ILIKE '%насос%'
</code></pre><h3>BRIN (Block Range INdex)</h3><p>Для очень больших таблиц с монотонно возрастающими данными (временны́е метки):</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Таблица телеметрии: 10 миллиардов строк
-- B-Tree индекс займёт 200 ГБ
-- BRIN займёт 1 МБ! (хранит мин/макс по блокам)
CREATE INDEX idx_telemetry_time_brin ON telemetry USING BRIN (measured_at)
WITH (pages_per_range = 128);

-- Работает только если данные ФИЗИЧЕСКИ упорядочены по времени
-- (INSERT в хронологическом порядке)
</code></pre><hr><h2>Оконные функции: SQL нового уровня</h2><p>Оконные функции — одна из самых мощных возможностей SQL, которую многие не знают.</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Задача: для каждого заказа показать его номер в последовательности
-- заказов этого клиента и общее количество заказов клиента

SELECT
    id,
    user_id,
    created_at,
    total,
    
    -- Номер строки в партиции (по каждому user_id отдельно)
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at) AS order_number,
    
    -- Ранг (при одинаковых значениях — одинаковый ранг, следующий пропускается)
    RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rank_by_total,
    
    -- Dense Rank (без пропусков)
    DENSE_RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS dense_rank,
    
    -- Количество строк в партиции
    COUNT(*) OVER (PARTITION BY user_id) AS total_orders,
    
    -- Нарастающая сумма
    SUM(total) OVER (PARTITION BY user_id ORDER BY created_at
                     ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total,
    
    -- Скользящее среднее (последние 3 заказа)
    AVG(total) OVER (PARTITION BY user_id ORDER BY created_at
                     ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3,
    
    -- Предыдущее и следующее значение
    LAG(total, 1)  OVER (PARTITION BY user_id ORDER BY created_at) AS prev_order_total,
    LEAD(total, 1) OVER (PARTITION BY user_id ORDER BY created_at) AS next_order_total,
    
    -- Процент от общей суммы клиента
    ROUND(total / SUM(total) OVER (PARTITION BY user_id) * 100, 2) AS pct_of_customer_total,
    
    -- Процентиль
    PERCENT_RANK() OVER (PARTITION BY user_id ORDER BY total) AS percentile

FROM orders
ORDER BY user_id, created_at;
</code></pre><h3>Практический пример: анализ телеметрии</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Обнаружение аномалий: значения &gt; avg + 2*stddev
WITH stats AS (
    SELECT
        device_id,
        AVG(temperature)    AS avg_temp,
        STDDEV(temperature) AS std_temp
    FROM telemetry
    WHERE measured_at &gt; NOW() - INTERVAL '7 days'
    GROUP BY device_id
),
windowed AS (
    SELECT
        t.*,
        s.avg_temp,
        s.std_temp,
        -- Z-score
        (t.temperature - s.avg_temp) / NULLIF(s.std_temp, 0) AS z_score,
        -- Скользящее среднее за 5 измерений
        AVG(t.temperature) OVER (
            PARTITION BY t.device_id
            ORDER BY t.measured_at
            ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
        ) AS moving_avg_5,
        -- Предыдущее значение (для расчёта скорости изменения)
        LAG(t.temperature) OVER (
            PARTITION BY t.device_id ORDER BY t.measured_at
        ) AS prev_temp,
        LAG(t.measured_at) OVER (
            PARTITION BY t.device_id ORDER BY t.measured_at
        ) AS prev_time
    FROM telemetry t
    JOIN stats s ON s.device_id = t.device_id
    WHERE t.measured_at &gt; NOW() - INTERVAL '24 hours'
)
SELECT
    device_id,
    measured_at,
    temperature,
    ROUND(z_score::numeric, 2)      AS z_score,
    ROUND(moving_avg_5::numeric, 2) AS moving_avg,
    -- Скорость изменения (°C/мин)
    ROUND(
        (temperature - prev_temp) /
        NULLIF(EXTRACT(EPOCH FROM (measured_at - prev_time)) / 60.0, 0)
    , 2) AS rate_per_min,
    
    CASE
        WHEN ABS(z_score) &gt; 3 THEN 'КРИТИЧЕСКАЯ АНОМАЛИЯ'
        WHEN ABS(z_score) &gt; 2 THEN 'Аномалия'
        ELSE 'Норма'
    END AS status

FROM windowed
WHERE ABS(z_score) &gt; 2
ORDER BY ABS(z_score) DESC;
</code></pre><hr><h2>CTE: читаемые и повторно используемые запросы</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- CTE (Common Table Expression) — именованные подзапросы
-- Делают сложные запросы читаемыми

WITH
-- Шаг 1: активные устройства за последние 24 часа
active_devices AS (
    SELECT DISTINCT device_id
    FROM telemetry
    WHERE measured_at &gt; NOW() - INTERVAL '24 hours'
),

-- Шаг 2: статистика по каждому устройству
device_stats AS (
    SELECT
        t.device_id,
        COUNT(*)                          AS reading_count,
        AVG(t.temperature)                AS avg_temp,
        MAX(t.temperature)                AS max_temp,
        MIN(t.temperature)                AS min_temp,
        SUM(CASE WHEN t.fault THEN 1 ELSE 0 END) AS fault_count
    FROM telemetry t
    INNER JOIN active_devices ad ON ad.device_id = t.device_id
    WHERE t.measured_at &gt; NOW() - INTERVAL '24 hours'
    GROUP BY t.device_id
),

-- Шаг 3: ранжирование по количеству аварий
ranked AS (
    SELECT
        *,
        RANK() OVER (ORDER BY fault_count DESC) AS fault_rank
    FROM device_stats
)

-- Финальный запрос
SELECT
    r.device_id,
    d.name,
    d.location,
    r.reading_count,
    ROUND(r.avg_temp::numeric, 2) AS avg_temp,
    r.max_temp,
    r.fault_count,
    r.fault_rank,
    CASE WHEN r.fault_count &gt; 10 THEN '</code></pre><p><code><span class="tmiEmoji" title="">🔴</span></code></p><p><code> Требует внимания' ELSE '<span class="tmiEmoji" title="">🟢</span> OK' END AS status FROM ranked r JOIN devices d ON d.id = r.device_id ORDER BY r.fault_rank; </code></p><h3>Рекурсивные CTE: для деревьев и графов</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Дерево категорий оборудования
WITH RECURSIVE category_tree AS (
    -- Базовый случай: корневые категории
    SELECT
        id,
        name,
        parent_id,
        1           AS depth,
        name::text  AS path
    FROM categories
    WHERE parent_id IS NULL

    UNION ALL

    -- Рекурсивный шаг: дочерние категории
    SELECT
        c.id,
        c.name,
        c.parent_id,
        ct.depth + 1,
        ct.path || ' &gt; ' || c.name
    FROM categories c
    INNER JOIN category_tree ct ON ct.id = c.parent_id
)
SELECT
    depth,
    REPEAT('  ', depth - 1) || name AS name_indented,
    path
FROM category_tree
ORDER BY path;

-- Результат:
-- Оборудование
--   Насосное оборудование
--     Центробежные насосы
--     Шестерённые насосы
--   Нагреватели
--     Ленточные
</code></pre><hr><h2>Транзакции и ACID</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Пример транзакции: перевод средств
-- ACID: Atomicity, Consistency, Isolation, Durability

BEGIN;

-- Несколько операций — или все, или ничего!
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
INSERT INTO transactions (from_id, to_id, amount, created_at)
    VALUES (1, 2, 1000, NOW());

-- Проверка (если не OK — откатываем всё)
DO $$
DECLARE
    balance DECIMAL;
BEGIN
    SELECT balance INTO balance FROM accounts WHERE id = 1;
    IF balance &lt; 0 THEN
        RAISE EXCEPTION 'Недостаточно средств!';
    END IF;
END;
$$;

COMMIT;  -- Всё OK, фиксируем
-- или ROLLBACK; -- Если что-то пошло не так

-- Уровни изоляции транзакций:
-- READ UNCOMMITTED: видит незафиксированные данные (грязное чтение)
-- READ COMMITTED: видит только зафиксированные (по умолчанию в PG)
-- REPEATABLE READ: повторное чтение даёт тот же результат
-- SERIALIZABLE: полная изоляция, как последовательное выполнение

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
</code></pre><hr><h2>Партиционирование: для больших таблиц</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Партиционирование таблицы телеметрии по месяцам

CREATE TABLE telemetry (
    id          BIGSERIAL,
    device_id   INT NOT NULL,
    measured_at TIMESTAMPTZ NOT NULL,
    temperature FLOAT,
    pressure    FLOAT,
    current     FLOAT
) PARTITION BY RANGE (measured_at);

-- Создаём партиции по месяцам
CREATE TABLE telemetry_2024_01 PARTITION OF telemetry
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE telemetry_2024_02 PARTITION OF telemetry
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- ... и так далее

-- Индексы создаются на каждой партиции отдельно
CREATE INDEX ON telemetry_2024_01 (device_id, measured_at);
CREATE INDEX ON telemetry_2024_02 (device_id, measured_at);

-- Автоматическое создание партиций (pg_partman)
-- SELECT partman.create_parent('public.telemetry', 'measured_at',
--        'native', 'monthly');

-- Преимущества:
-- 1. Partition pruning: запрос за январь сканирует только telemetry_2024_01
-- 2. Быстрое удаление старых данных: DROP TABLE telemetry_2023_01
-- 3. Параллельное сканирование разных партиций
</code></pre><hr><h2>N+1 проблема: самая частая ошибка</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- N+1: вместо одного запроса делаем N+1
-- Типичная ошибка при работе с ORM

-- </code></pre><p><code><span class="tmiEmoji" title="">❌</span></code></p><p><code> ПЛОХО (в Python/PHP коде): -- users = db.execute("SELECT * FROM users LIMIT 100") -- for user in users: --     orders = db.execute("SELECT * FROM orders WHERE user_id = ?", user.id) -- Итого: 1 + 100 = 101 запрос!  -- <span class="tmiEmoji" title="">✅</span> ХОРОШО: один JOIN SELECT     u.id, u.name, u.email,     COUNT(o.id)  AS order_count,     SUM(o.total) AS total_spent FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.name, u.email LIMIT 100;  -- <span class="tmiEmoji" title="">✅</span> ХОРОШО: два запроса с IN (для сложных случаев) -- users = db.execute("SELECT * FROM users LIMIT 100") -- user_ids = [u.id for u in users] -- orders = db.execute("SELECT * FROM orders WHERE user_id = ANY(?)", user_ids) -- Итого: 2 запроса! </code></p><hr><h2>Практические паттерны оптимизации</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 1. UPSERT (INSERT или UPDATE если существует)
INSERT INTO device_status (device_id, status, updated_at)
VALUES (1, 'online', NOW())
ON CONFLICT (device_id) DO UPDATE SET
    status     = EXCLUDED.status,
    updated_at = EXCLUDED.updated_at;

-- 2. Batch INSERT (вместо N отдельных INSERT)
INSERT INTO telemetry (device_id, measured_at, temperature)
VALUES
    (1, '2024-01-01 10:00', 25.3),
    (1, '2024-01-01 10:01', 25.4),
    (2, '2024-01-01 10:00', 22.1)
-- До 1000 строк в одном запросе — намного быстрее!

-- 3. COPY для массовой загрузки (самый быстрый способ)
-- \COPY telemetry FROM '/data/telemetry.csv' CSV HEADER

-- 4. Materialized View для сложных агрегатов
CREATE MATERIALIZED VIEW daily_device_summary AS
SELECT
    device_id,
    DATE(measured_at)  AS day,
    AVG(temperature)   AS avg_temp,
    MAX(temperature)   AS max_temp,
    COUNT(*)           AS readings
FROM telemetry
GROUP BY device_id, DATE(measured_at);

CREATE UNIQUE INDEX ON daily_device_summary(device_id, day);

-- Обновление (можно конкурентно, без блокировки SELECT)
REFRESH MATERIALIZED VIEW CONCURRENTLY daily_device_summary;

-- 5. EXPLAIN сначала, оптимизировать потом!
-- Никогда не оптимизируйте наугад. Всегда смотрите план.
</code></pre><hr><h2>PostgreSQL: важные настройки performance</h2><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Ключевые параметры postgresql.conf для production:

-- Память (зависит от RAM сервера):
-- shared_buffers = 25% RAM (напр. 4GB для 16GB)
-- effective_cache_size = 75% RAM
-- work_mem = 64MB (для сортировок и hash join)
-- maintenance_work_mem = 1GB (для VACUUM, CREATE INDEX)

-- Диск (для SSD):
-- random_page_cost = 1.1 (вместо 4.0)
-- effective_io_concurrency = 200

-- Параллелизм:
-- max_parallel_workers_per_gather = 4
-- max_worker_processes = 8

-- Checkpoint:
-- checkpoint_completion_target = 0.9
-- wal_buffers = 64MB

-- Проверка текущих настроек:
SELECT name, setting, unit, context
FROM pg_settings
WHERE name IN ('shared_buffers', 'work_mem', 'max_connections');

-- Статистика медленных запросов (pg_stat_statements):
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT
    substring(query, 1, 80) AS query_short,
    calls,
    ROUND(total_exec_time::numeric / 1000, 2) AS total_sec,
    ROUND(mean_exec_time::numeric, 2) AS mean_ms,
    rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;
</code></pre><hr><h2>Заключение</h2><p>SQL — не просто "язык запросов", это язык управления данными. Понимание планировщика, правильное использование индексов, оконные функции и CTE — это инструменты, которые превращают "работает" в "работает быстро и масштабируется".</p><p>Практический совет: запустите <code>pg_stat_statements</code> на вашем production-сервере прямо сейчас. Посмотрите топ-20 медленных запросов. С вероятностью 80% — там найдётся очевидная оптимизация, которая ускорит приложение в разы.</p><p>Инвестируйте в "Use The Index, Luke" (use-the-index-luke.com) — лучшее бесплатное руководство по индексам SQL. И всегда: EXPLAIN ANALYZE перед любой "оптимизацией".</p>]]></description><guid isPermaLink="false">143</guid><pubDate>Sat, 21 Mar 2026 20:52:37 +0000</pubDate></item><item><title>REST API: &#x43F;&#x440;&#x43E;&#x435;&#x43A;&#x442;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435;, &#x434;&#x43E;&#x43A;&#x443;&#x43C;&#x435;&#x43D;&#x442;&#x430;&#x446;&#x438;&#x44F; &#x438; &#x43B;&#x443;&#x447;&#x448;&#x438;&#x435; &#x43F;&#x440;&#x430;&#x43A;&#x442;&#x438;&#x43A;&#x438;</title><link>https://ithub.uno/statiarticles/1_articles/rest-api-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F-%D0%B8-%D0%BB%D1%83%D1%87%D1%88%D0%B8%D0%B5-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B8-r149/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/maxresdefault.jpg.328d5a347fb93bc012acf3872a05a7e0.jpg" /></p>
<h2>REST: не просто "HTTP + JSON"</h2><p>REST (Representational State Transfer) — архитектурный стиль, описанный Роем Филдингом в 2000 году. Большинство "REST API" в реальности — это RPC поверх HTTP (CRUD по URL). Настоящий REST имеет шесть ограничений, из которых на практике применяют три-четыре.</p><p>Но это не важно. Важно проектировать API, которым приятно пользоваться: предсказуемым, документированным, обрабатывающим ошибки правильно, безопасным и масштабируемым.</p><hr><h2>Дизайн URL: основные принципы</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Принципы именования ресурсов:</code></pre><p><code><span class="tmiEmoji" title="">✅</span> Существительные во множественном числе:    /api/v1/devices    /api/v1/devices/42    /api/v1/devices/42/sensors    /api/v1/devices/42/sensors/7/readings  </code></p><p><code><span class="tmiEmoji" title="">❌</span> Глаголы в URL:    /api/v1/getDevices         ← нет!    /api/v1/createDevice       ← нет!    /api/v1/device/42/delete   ← нет!  Действие выражается HTTP-методом:    GET    /devices      → список устройств    POST   /devices      → создать устройство    GET    /devices/42   → получить устройство 42    PUT    /devices/42   → полностью заменить устройство 42    PATCH  /devices/42   → частично обновить устройство 42    DELETE /devices/42   → удалить устройство 42  Вложенность — для зависимых ресурсов:    GET /devices/42/sensors          → датчики устройства 42    POST /devices/42/sensors         → добавить датчик к устройству 42    GET /devices/42/sensors/7        → конкретный датчик    DELETE /devices/42/sensors/7     → удалить датчик  Максимум 3 уровня вложенности! Глубже — smell. </code></p><hr><h2>HTTP методы и их семантика</h2><div class="tmiRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Метод</p></th><th colspan="1" rowspan="1"><p>Идемпотентный</p></th><th colspan="1" rowspan="1"><p>Безопасный</p></th><th colspan="1" rowspan="1"><p>Применение</p></th></tr><tr><td colspan="1" rowspan="1"><p>GET</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>Получение данных</p></td></tr><tr><td colspan="1" rowspan="1"><p>POST</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p>Создание, не-идемпотентные действия</p></td></tr><tr><td colspan="1" rowspan="1"><p>PUT</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p>Полная замена ресурса</p></td></tr><tr><td colspan="1" rowspan="1"><p>PATCH</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span>*</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p>Частичное обновление</p></td></tr><tr><td colspan="1" rowspan="1"><p>DELETE</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p>Удаление</p></td></tr><tr><td colspan="1" rowspan="1"><p>HEAD</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>Получение заголовков без тела</p></td></tr><tr><td colspan="1" rowspan="1"><p>OPTIONS</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span></p></td><td colspan="1" rowspan="1"><p>CORS preflight, capabilities</p></td></tr></tbody></table></div><p><strong>Идемпотентность:</strong> повторный вызов даёт тот же результат. PUT /devices/42 с одними данными можно вызвать 100 раз — результат одинаковый. DELETE /devices/42 — тоже (второй вызов: 404, но ресурс всё равно удалён).</p><hr><h2>Коды состояния HTTP: правильное использование</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>2xx — Успех:
  200 OK          — GET, PUT, PATCH успешно
  201 Created     — POST создал ресурс; Location: /api/v1/devices/43
  204 No Content  — DELETE, PATCH без возврата данных
  
3xx — Перенаправление:
  301 Moved Permanently — ресурс переехал навсегда
  304 Not Modified      — кешированный ответ актуален (ETag/If-None-Match)

4xx — Ошибка клиента:
  400 Bad Request       — невалидные данные запроса
  401 Unauthorized      — не аутентифицирован (нет или неверный токен)
  403 Forbidden         — аутентифицирован, но нет прав
  404 Not Found         — ресурс не существует
  405 Method Not Allowed— метод не поддерживается для этого URL
  409 Conflict          — конфликт (дубликат, версионирование)
  422 Unprocessable     — синтаксически корректный JSON, но семантически неверный
  429 Too Many Requests — rate limit превышен

5xx — Ошибка сервера:
  500 Internal Server Error — непредвиденная ошибка
  503 Service Unavailable   — временно недоступен (maintenance, overload)
</code></pre><hr><h2>Структура ответов: единообразие обязательно</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code># Стандартизированный формат ответа

# Успешный список с пагинацией:
{
    "data": [
        {"id": 1, "name": "Насос 1", "status": "active"},
        {"id": 2, "name": "Насос 2", "status": "fault"}
    ],
    "meta": {
        "total": 48,
        "page": 2,
        "page_size": 10,
        "pages": 5,
        "next": "/api/v1/devices?page=3&amp;page_size=10",
        "prev": "/api/v1/devices?page=1&amp;page_size=10"
    }
}

# Единичный объект:
{
    "data": {
        "id": 42,
        "name": "Насос холодной воды",
        "location": "Котельная",
        "created_at": "2024-01-15T10:30:00Z",
        "updated_at": "2024-03-10T08:45:22Z"
    }
}

# Ошибка (ВСЕГДА одна структура!):
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Ошибка валидации входных данных",
        "details": [
            {
                "field": "temperature_max",
                "message": "Значение должно быть больше temperature_min"
            },
            {
                "field": "device_id",
                "message": "Устройство с таким ID не существует"
            }
        ],
        "request_id": "req_abc123xyz",
        "timestamp": "2024-03-15T14:22:33Z"
    }
}
</code></pre><hr><h2>FastAPI: production-ready API за минуты</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code># main.py
from fastapi import FastAPI, HTTPException, Depends, Query, Path, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
import uuid

app = FastAPI(
    title="Industrial IoT API",
    description="API управления промышленными устройствами",
    version="1.0.0",
    docs_url="/api/docs",      # Swagger UI
    redoc_url="/api/redoc",    # ReDoc
    openapi_url="/api/openapi.json"
)

# Middleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://dashboard.factory.com"],
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
)

# ===== МОДЕЛИ =====

class DeviceStatus(str):
    ACTIVE   = "active"
    INACTIVE = "inactive"
    FAULT    = "fault"
    MAINTENANCE = "maintenance"

class DeviceBase(BaseModel):
    name:     str = Field(..., min_length=1, max_length=100, example="Насос 1")
    location: str = Field(..., example="Котельная")
    model:    str = Field(..., example="Grundfos CM5-5")
    tags:     List[str] = Field(default=[], example=["pump", "cooling"])

class DeviceCreate(DeviceBase):
    pass

class DeviceUpdate(BaseModel):
    name:     Optional[str] = Field(None, min_length=1, max_length=100)
    location: Optional[str] = None
    status:   Optional[str] = None
    tags:     Optional[List[str]] = None

class DeviceResponse(DeviceBase):
    id:         int
    status:     str = "active"
    created_at: datetime
    updated_at: datetime
    
    class Config:
        from_attributes = True

class PaginatedResponse(BaseModel):
    data:  List[DeviceResponse]
    meta:  dict

# ===== ЗАВИСИМОСТИ =====

# Имитация БД
fake_db = {}
device_counter = 0

def get_device_or_404(device_id: int = Path(..., ge=1)) -&gt; dict:
    device = fake_db.get(device_id)
    if not device:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error": {
                    "code": "DEVICE_NOT_FOUND",
                    "message": f"Устройство с ID {device_id} не найдено",
                    "request_id": str(uuid.uuid4())
                }
            }
        )
    return device

# ===== ENDPOINTS =====

@app.get(
    "/api/v1/devices",
    response_model=PaginatedResponse,
    summary="Список устройств",
    tags=["Devices"]
)
async def list_devices(
    page:      int   = Query(default=1, ge=1, description="Номер страницы"),
    page_size: int   = Query(default=20, ge=1, le=100, description="Размер страницы"),
    status:    Optional[str] = Query(default=None, description="Фильтр по статусу"),
    search:    Optional[str] = Query(default=None, description="Поиск по имени"),
    sort_by:   str   = Query(default="created_at", description="Поле сортировки"),
    sort_desc: bool  = Query(default=True, description="По убыванию"),
):
    """
    Возвращает список устройств с пагинацией и фильтрацией.
    
    - **page**: страница (начиная с 1)
    - **page_size**: количество на странице (макс. 100)
    - **status**: фильтр по статусу (active, inactive, fault, maintenance)
    - **search**: поиск по имени и местоположению
    """
    # Имитация запроса к БД
    all_devices = list(fake_db.values())
    
    if status:
        all_devices = [d for d in all_devices if d.get("status") == status]
    
    if search:
        q = search.lower()
        all_devices = [d for d in all_devices
                      if q in d.get("name", "").lower()
                      or q in d.get("location", "").lower()]
    
    total = len(all_devices)
    pages = (total + page_size - 1) // page_size
    
    start = (page - 1) * page_size
    items = all_devices[start:start + page_size]
    
    base_url = f"/api/v1/devices?page_size={page_size}"
    
    return {
        "data": items,
        "meta": {
            "total":     total,
            "page":      page,
            "page_size": page_size,
            "pages":     pages,
            "next": f"{base_url}&amp;page={page+1}" if page &lt; pages else None,
            "prev": f"{base_url}&amp;page={page-1}" if page &gt; 1 else None,
        }
    }


@app.get(
    "/api/v1/devices/{device_id}",
    response_model=DeviceResponse,
    summary="Получить устройство",
    tags=["Devices"],
    responses={404: {"description": "Устройство не найдено"}}
)
async def get_device(device: dict = Depends(get_device_or_404)):
    return device


@app.post(
    "/api/v1/devices",
    response_model=DeviceResponse,
    status_code=status.HTTP_201_CREATED,
    summary="Создать устройство",
    tags=["Devices"]
)
async def create_device(device_data: DeviceCreate):
    global device_counter
    device_counter += 1
    
    now = datetime.utcnow()
    device = {
        "id":         device_counter,
        "status":     "active",
        "created_at": now,
        "updated_at": now,
        **device_data.dict()
    }
    
    fake_db[device_counter] = device
    
    return device


@app.patch(
    "/api/v1/devices/{device_id}",
    response_model=DeviceResponse,
    summary="Обновить устройство",
    tags=["Devices"]
)
async def update_device(
    update_data: DeviceUpdate,
    device: dict = Depends(get_device_or_404)
):
    # PATCH — обновляем только переданные поля
    updates = update_data.dict(exclude_unset=True)  # Только явно переданные поля!
    
    device.update(updates)
    device["updated_at"] = datetime.utcnow()
    
    return device


@app.delete(
    "/api/v1/devices/{device_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    summary="Удалить устройство",
    tags=["Devices"]
)
async def delete_device(device: dict = Depends(get_device_or_404)):
    fake_db.pop(device["id"])
    # 204 — нет тела ответа


# ===== ОБРАБОТЧИКИ ОШИБОК =====

@app.exception_handler(404)
async def not_found_handler(request, exc):
    return JSONResponse(
        status_code=404,
        content={"error": {"code": "NOT_FOUND", "message": "Ресурс не найден"}}
    )

@app.exception_handler(500)
async def server_error_handler(request, exc):
    # Логируем, но не раскрываем детали клиенту!
    import logging
    logging.exception(f"Unhandled error: {exc}")
    
    return JSONResponse(
        status_code=500,
        content={
            "error": {
                "code": "INTERNAL_ERROR",
                "message": "Внутренняя ошибка сервера",
                "request_id": str(uuid.uuid4())
            }
        }
    )
</code></pre><hr><h2>JWT авторизация</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta

SECRET_KEY  = "super-secret-key-change-in-production"
ALGORITHM   = "HS256"
TOKEN_EXPIRE = 60 * 24  # минуты

security = HTTPBearer()

def create_access_token(data: dict, expire_minutes: int = TOKEN_EXPIRE) -&gt; str:
    payload = {
        **data,
        "exp": datetime.utcnow() + timedelta(minutes=expire_minutes),
        "iat": datetime.utcnow(),
        "type": "access"
    }
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -&gt; dict:
    try:
        payload = jwt.decode(credentials.credentials, SECRET_KEY,
                             algorithms=[ALGORITHM])
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail={
            "error": {"code": "TOKEN_EXPIRED", "message": "Токен истёк"}
        })
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail={
            "error": {"code": "INVALID_TOKEN", "message": "Неверный токен"}
        })

# Использование:
@app.get("/api/v1/protected")
async def protected_route(current_user: dict = Depends(verify_token)):
    return {"user_id": current_user["sub"], "message": "OK"}

@app.post("/api/v1/auth/login")
async def login(username: str, password: str):
    # Проверка пользователя (в реальности — из БД)
    if username != "admin" or password != "secret":
        raise HTTPException(status_code=401,
                           detail={"error": {"code": "INVALID_CREDENTIALS"}})
    
    token = create_access_token({"sub": username, "role": "admin"})
    return {"access_token": token, "token_type": "bearer", "expires_in": TOKEN_EXPIRE * 60}
</code></pre><hr><h2>Rate Limiting</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>from fastapi import Request
from collections import defaultdict
import time

class InMemoryRateLimiter:
    """Простой rate limiter (для production используйте Redis)"""
    
    def __init__(self, requests_per_minute: int = 60):
        self.rpm   = requests_per_minute
        self.store = defaultdict(list)
    
    def is_allowed(self, key: str) -&gt; tuple[bool, dict]:
        now = time.time()
        window = 60.0
        
        # Очищаем устаревшие записи
        self.store[key] = [t for t in self.store[key] if now - t &lt; window]
        
        count = len(self.store[key])
        
        headers = {
            "X-RateLimit-Limit":     str(self.rpm),
            "X-RateLimit-Remaining": str(max(0, self.rpm - count - 1)),
            "X-RateLimit-Reset":     str(int(now + window)),
        }
        
        if count &gt;= self.rpm:
            return False, headers
        
        self.store[key].append(now)
        return True, headers

rate_limiter = InMemoryRateLimiter(requests_per_minute=100)

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    # Ключ: IP + User-Agent (или user_id после авторизации)
    client_ip = request.client.host
    key = f"{client_ip}:{request.headers.get('user-agent', '')[:50]}"
    
    allowed, headers = rate_limiter.is_allowed(key)
    
    if not allowed:
        return JSONResponse(
            status_code=429,
            content={"error": {"code": "RATE_LIMIT_EXCEEDED",
                               "message": "Слишком много запросов. Повторите через 60 секунд."}},
            headers={**headers, "Retry-After": "60"}
        )
    
    response = await call_next(request)
    response.headers.update(headers)
    return response
</code></pre><hr><h2>Версионирование API</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code># Способ 1: В URL (самый простой и видимый)
# /api/v1/devices
# /api/v2/devices

# Способ 2: В заголовке
# Accept: application/vnd.myapi.v2+json

# Способ 3: В параметре запроса
# /api/devices?version=2

# Рекомендуется: URL-версионирование для публичных API

# Правила обратной совместимости:
# </code></pre><p><code><span class="tmiEmoji" title="">✅</span> МОЖНО добавлять новые поля в ответ (клиент игнорирует незнакомые) # </code></p><p><code><span class="tmiEmoji" title="">✅</span> МОЖНО добавлять новые необязательные параметры # </code></p><p><code><span class="tmiEmoji" title="">✅</span> МОЖНО добавлять новые endpoints # </code></p><p><code><span class="tmiEmoji" title="">❌</span> НЕЛЬЗЯ удалять поля из ответа # </code></p><p><code><span class="tmiEmoji" title="">❌</span> НЕЛЬЗЯ менять тип поля # </code></p><p><code><span class="tmiEmoji" title="">❌</span> НЕЛЬЗЯ делать необязательный параметр обязательным # </code></p><p><code><span class="tmiEmoji" title="">❌</span> НЕЛЬЗЯ менять семантику существующих полей # </code></p><p><code>Любое из запрещённого → новая мажорная версия!  from fastapi import APIRouter  # v1 router v1_router = APIRouter(prefix="/api/v1", tags=["v1"])  @v1_router.get("/devices") async def list_devices_v1():     return {"version": "v1", "data": []}  # v2 router (новая логика, breaking changes) v2_router = APIRouter(prefix="/api/v2", tags=["v2"])  @v2_router.get("/devices") async def list_devices_v2():     return {"version": "v2", "data": [], "meta": {}}  # Новый формат  app.include_router(v1_router) app.include_router(v2_router) </code></p><hr><h2>Документация: OpenAPI и Swagger</h2><p>FastAPI автоматически генерирует OpenAPI spec. Добавьте подробные комментарии:</p><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>@app.get(
    "/api/v1/devices/{device_id}/telemetry",
    tags=["Telemetry"],
    summary="Телеметрия устройства",
    description="""
    Возвращает исторические данные телеметрии устройства.
    
    ## Параметры времени
    
    Используйте ISO 8601 формат: `2024-03-15T10:30:00Z`
    
    Или относительные значения: `-1h`, `-24h`, `-7d`, `-30d`
    
    ## Агрегация
    
    - `raw`: сырые данные (ограничено 10 000 точек)
    - `1min`: агрегация по минутам
    - `5min`, `1h`, `1d`: другие интервалы
    """,
    response_description="Список точек телеметрии с временными метками",
    responses={
        200: {"description": "Успешно"},
        404: {"description": "Устройство не найдено"},
        400: {"description": "Неверные параметры времени"},
    }
)
async def get_telemetry(
    device_id: int = Path(..., description="ID устройства", example=42),
    from_time: str = Query(..., description="Начало периода (ISO 8601 или -Nh/-Nd)", example="-24h"),
    to_time:   str = Query(default="now", description="Конец периода"),
    resample:  str = Query(default="5min", description="Интервал агрегации",
                           regex="^(raw|1min|5min|1h|1d)$"),
):
    ...
</code></pre><hr><h2>Чеклист для production API</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Безопасность:
□ HTTPS только (HTTP редирект на HTTPS)
□ JWT с разумным TTL (1-24 часа)
□ Rate limiting по IP и по user
□ Валидация всех входных данных
□ SQL-инъекции: параметризованные запросы (ORM)
□ Секреты не в коде (env vars, Vault)
□ CORS настроен правильно (не allow_origins=["*"]!)

Надёжность:
□ Timeouts на все внешние запросы
□ Circuit Breaker для сервисов-зависимостей
□ Graceful shutdown (SIGTERM обработан)
□ Health endpoint /health или /api/health

Observability:
□ Структурированные логи (JSON) с request_id
□ Метрики: latency, error rate, throughput
□ Distributed tracing (OpenTelemetry)

Документация:
□ OpenAPI/Swagger автоматически
□ Примеры запросов в описаниях
□ CHANGELOG с версиями
</code></pre><hr><h2>Заключение</h2><p>Хороший REST API — это API, которым приятно пользоваться. Предсказуемые URL, понятные коды ошибок, единообразная структура ответов, версионирование без сюрпризов, документация которая не врёт.</p><p>FastAPI делает большую часть работы автоматически: валидацию, сериализацию, документацию. Но архитектурные решения — за вами. Потратьте время на проектирование URL до написания кода. Нарисуйте ресурсы и операции. Согласуйте с командой стандарты ошибок. Это окупится при первом обращении клиентской команды к вашему API.</p>]]></description><guid isPermaLink="false">149</guid><pubDate>Sat, 21 Mar 2026 20:57:46 +0000</pubDate></item></channel></rss>
