Circuit Breaker это защитный паттерн для распределённых систем. Он автоматически «отключает» вызовы к
Аналогия проста: как автоматический выключатель в электрощитке защищает проводку от перегрузки, так и Circuit Breaker защищает систему от цепной реакции ошибок.
Два состояния
Замкнуто — штатный режим. Все запросы
Разомкнуто — защитный режим. После превышения порога ошибок
Реализация:
type Circuit func(context.Context) (string, error)
func Breaker(circuit Circuit, failureThreshold uint) Circuit {
var consecutiveFailures int = 0
var lastAttempt = time.Now()
var m sync.RWMutex
return func(ctx context.Context) (string, error) {
m.RLock()
d := consecutiveFailures - int(failureThreshold)
if d >= 0 {
// Экспоненциальная выдержка: 2, 4, 8... секунд
shouldRetryAt := lastAttempt.Add(time.Second * 2 << d)
if !time.Now().After(shouldRetryAt) {
m.RUnlock()
return "", errors.New("service unreachable")
}
}
m.RUnlock()
response, err := circuit(ctx)
m.Lock()
defer m.Unlock()
lastAttempt = time.Now()
if err != nil {
consecutiveFailures++
return response, err
}
consecutiveFailures = 0 // Успех — сбрасываем счётчик
return response, nil
}
}
Что важно в этой реализации
•
sync.RWMutex защищает общее состояние при конкурентных вызовах• Экспоненциальная выдержка (
2 << d) даёт сервису всё больше времени на восстановление с каждой неудачной попыткой• После успешного вызова счётчик сбрасывается — цепь «замыкается» обратно автоматически
• Функция возвращает тот же тип
Circuit, что позволяет прозрачно встраивать Breaker без изменения кода клиентаPlease open Telegram to view this post
VIEW IN TELEGRAM
👍8😁1
Package aliasing это возможность присвоить псевдоним
Синтаксис:
import fm "fmt"
fm.Println("hello") // вместо fmt.Println
Алиас указывается перед путём к пакету и полностью заменяет его имя в текущем файле.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
📌 Кейсы использования
Два пакета с одинаковым именем. Без алиасов код не скомпилируется.
import (
sqlDB "project/sql/db"
nosqlDB "project/nosql/db"
)
Сокращает шум при частом обращении к пакету с громоздким путём:
import (
mh "myproject/subproject/module/helpers"
)
Актуально для сгенерированного кода, когда имя пакета не совпадает с тем, что ожидает читатель:
import (
validator "github.com/myorg/gen/v2/validate_pb"
)
_Специальный алиас для импорта ради побочных эффектов: регистрация драйвера,
init()-функция; без использования пакета в коде:import _ "github.com/lib/pq" // регистрирует PostgreSQL-драйвер
.Позволяет обращаться к экспортируемым именам пакета без префикса:
import . "math"
r := Sqrt(16) // вместо math.Sqrt(16)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Указатель хранит адрес переменной. Указатель на указатель хранит адрес другого указателя. Каждый уровень добавляет одну «звёздочку» к типу и одно разыменование для доступа к значению.
Пример:
a := 100
var b *int = &a // b хранит адрес a
var c **int = &b // c хранит адрес b
Цепочка в памяти выглядит так:
c → b → a → 100
Разыменование:
fmt.Println(a) // 100 — исходное значение
fmt.Println(b) // 0xc000014090 — адрес a
fmt.Println(*b) // 100 — значение a через b
fmt.Println(c) // 0xc00000e028 — адрес b
fmt.Println(*c) // 0xc000014090 — адрес a через c
fmt.Println(**c) // 100 — значение a через c
Каждая
* это один шаг по цепочке адресов.**c позволяет не только читать значение a, но и менять сам указатель b, то есть переключать его на другую переменную. Именно это делает двойной указатель полезным, а не просто экзотикой.Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5🌚5😁3❤2
Двойной указатель решает конкретную задачу:
Проблема без
**:func resetPointer(p *int) {
newVal := 0
p = &newVal // меняем локальную копию — снаружи ничего не изменится
}p — это копия адреса. Переназначение p внутри функции не затрагивает оригинал.Решение через
**int:func resetPointer(p **int) {
newVal := 0
*p = &newVal // меняем сам указатель — изменение видно снаружи
}
func main() {
a := 42
ptr := &a
resetPointer(&ptr)
fmt.Println(*ptr) // 0
}Теперь функция получает адрес самого указателя и может подменить его цель.
Другие кейсы
func insertHead(head **Node, val int) {
newNode := &Node{val: val, next: *head}
*head = newNode
}func initConfig(cfg **Config) {
*cfg = &Config{Timeout: 30}
}Когда не нужен
Если цель просто изменить значение, на которое уже указывает указатель, достаточно одного
*. Двойной указатель нужен только тогда, когда требуется изменить сам адрес, хранящийся в указателе.Please open Telegram to view this post
VIEW IN TELEGRAM
🤔5👾3❤2🌚2👍1
Нужно реализовать интерфейс
slog.Handler: четыре метода, которые дают контроль над форматом, фильтрацией и транспортом логов:type Handler interface {
Enabled(context.Context, Level) bool
Handle(context.Context, Record) error
WithAttrs(attrs []Attr) Handler
WithGroup(name string) Handler
}Enabled вызывается до того, как рантайм начнёт вычислять аргументы лога. Если возвращает false, то Handle вообще не вызывается. Это точка оптимизации: дорогие вычисления в аргументах не будут выполнены для отфильтрованных уровней.Handle — основной метод. Получает Record с временем, уровнем, сообщением и атрибутами. Атрибуты итерируются через коллбэк r.Attrs(fn), а не через срез и это намеренная оптимизация аллокаций, первые несколько атрибутов хранятся инлайн. Метод должен быть goroutine-safe, поэтому нужен мьютекс на запись в io.Writer.WithAttrs вызывается при logger.With("key", "val"). Должен вернуть новый handler с сохранёнными полями — мутировать текущий нельзя, логгер может использоваться из нескольких горутин.WithGroup вызывается при logger.WithGroup("request"). Все последующие атрибуты должны быть вложены под этим ключом: request.method, request.ip.Please open Telegram to view this post
VIEW IN TELEGRAM
Срез это не массив, а заголовок из трёх полей:
Правило границ. При нарезке
a[low:high] должно выполняться 0 <= low <= high <= cap(a). Примечательно, что high ограничен именно cap, а не len — это позволяет «заглянуть» вперёд за текущую длину, если базовый массив это допускает.Нарушение границ — паника в рантайме. Компилятор не проверяет корректность индексов — это делает рантайм.
a[0:len(a)+1] скомпилируется, но упадёт с slice bounds out of range при выполнении.Пустой срез — не nil.
a[2:2] — валидный срез с длиной 0. Он инициализирован и указывает на память. var s []int — другое: nil-срез, у которого указатель равен nil. len и cap у обоих равны нулю, но s == nil вернёт true только для первого.Разделяемая память. Срезы, нарезанные от одного массива, указывают на те же данные. Запись через один срез изменит то, что видит другой — до тех пор, пока не произошёл рост.
Трёхиндексная нарезка.
a[low:high:max] задаёт ёмкость результата явно: cap = max - low. Используется, чтобы append не «прорвался» за нужный регион и не затронул соседние данные в исходном массиве.Рост при append. Когда
len == cap, Go создаёт новый массив, копирует данные и возвращает срез с новым указателем. С этого момента два среза больше не делят память — и это один из самых частых источников неожиданного поведения на практике.Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Lock-Free алгоритмы это
• Гарантия: система в целом прогрессирует (нет глобальной блокировки).
• Минус: некоторые потоки могут «застревать» в бесконечных retry-циклах (livelock).
• Плюсы: высокая производительность, проще в реализации.
Wait-Free алгоритмы это строгий
• Гарантия: индивидуальный прогресс для всех (полная справедливость, нет голодания ресурсов).
• Минус: сложнее реализовать, ниже производительность из-за оверхеда на координацию.
• Когда использовать: в реал-тайм системах, например, ABA-free структуры.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🥱1
Первый и самый простой это
done := make(chan struct{})
go func() {
// работа выполнена
close(done)
}()
<-done // ждём сигналаclose — лучший способ сигналить завершение, потому что все читатели получат сигнал одновременно.Второй способ это передать
result := make(chan error, 1)
go func() {
err := doSomething()
result <- err
}()
if err := <-result; err != nil {
log.Fatal(err)
}
Третий способ —
context.Context. Это стандарт в продакшн-коде. Контекст несёт в себе сигнал отмены, дедлайн и значения. Внутри он тоже использует канал:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-work(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("timeout:", ctx.Err())
}
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
// +build — старая форма, использовалась до // +build linux darwin
// +build amd64
package main
//go:build это новая форма, появилась в Go 1.17. Синтаксис стал читаемым: используются обычные логические операторы &&, ||, !://go:build (linux || darwin) && amd64
package main
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤1
В большинстве языков
switch требует явного break в каждом case, иначе выполнение провалится в следующий блок. Go сделал иначе.В Go каждый case автоматически завершается — никакого проваливания:
switch status {
case 1:
fmt.Println("one") // выполнится только это
case 2:
fmt.Println("two") // сюда не попадём
}В C/Java без break выполнились бы оба блока.
Есть
fallthrough, C-стайл поведение можно воспроизвести явно:switch status {
case 1:
fmt.Println("one")
fallthrough // явно говорим "провалиться" дальше
case 2:
fmt.Println("two") // выполнится тоже
}Несколько значений в одном case:
switch day {
case "Saturday", "Sunday":
fmt.Println("выходной")
case "Monday", "Friday":
fmt.Println("почти выходной")
}Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5
В первой части постов навалили жесткой базы, чтобы вправить мозги на место. Во второй дали конкретные инструменты, фреймворки и пошаговые инструкции, что нужно кодить прямо сейчас.
Часть 1. Введение, юзкейсы и реальность
Разбираемся с терминами, снимаем розовые очки и смотрим, где ИИ реально приносит бабки, а где только жжет нервы:
1. «Так что вообще считается AI-агентом?»
2. «Где тут бот, а где уже AI-агент?»
3. «Не надо пихать AI-агента в каждую задачу»
4. «Что уже можно спокойно делать через AI-агентов?»
5. «А что через AI-агентов пока лучше не трогать?»
Часть 2. Изнанка, ошибки и архитектура
Как всё это устроено под капотом, чтобы не слить бюджет и не наломать дров на старте:
6. «Можно ли просто сесть вечером и собрать себе AI-агента?»
7. «С чего вообще начать, если хочется попробовать AI-агентов»
8. «Почему AI-агент может внезапно начать творить дичь»
9. «Где AI-агенты реально экономят время, а где только добавляют возни»
10. «Почему они жрут столько денег?»
Часть 3. Хардкорная практика (Что делать руками)
Хватит теории. Открываем ноут, запускаем Cursor и делаем нормальные, отказоустойчивые системы:
11. «Почему одного промпта мало?»
12. «Почему AI-агенту мало просто “дать доступ к данным”»
13. «Если не следить за AI-агентом, он быстро начинает жить своей жизнью»
14. «Собрать демку легко. Но как же сделать нормально»
15. «Как сделать, чтобы это не развалилось через неделю?»
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Когда стека не хватает,
Для производительности важны два момента.
1. Копирование не бесплатно. Если горутина постоянно
2. После копирования все указатели на стековые переменные
Please open Telegram to view this post
VIEW IN TELEGRAM
Два указателя, движущихся навстречу друг другу:
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}Начиная с Go 1.21 в стандартной библиотеке есть готовая функция slices.Reverse(), которая делает то же самое под капотом:
import "slices"
s := []int{1, 2, 3, 4, 5}
slices.Reverse(s) // [5, 4, 3, 2, 1]
Оба варианта работают in-place, без выделения дополнительной памяти.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13
Префиксная сумма это массив, где каждый элемент
prefix[i] хранит 0 до i включительно.Построение:
func buildPrefix(arr []int) []int {
prefix := make([]int, len(arr)+1)
for i, v := range arr {
prefix[i+1] = prefix[i] + v
}
return prefix
}
func rangeSum(prefix []int, l, r int) int {
return prefix[r+1] - prefix[l]
}Задача: количество подмассивов с суммой K
func subarraySum(nums []int, k int) int {
count := 0
prefix := 0
seen := map[int]int{0: 1}
for _, v := range nums {
prefix += v
count += seen[prefix-k] // если ключа нет — вернёт 0, это фича Go
seen[prefix]++
}
return count
}Где использовать:
• Запросы суммы на отрезке — самый очевидный случай. Если массив не меняется, а запросов много, строишь префикс один раз и отвечаешь за O(1).
• Поиск подмассива с заданной суммой — сводишь к задаче "найти два индекса префикса с разностью K", решается через хэшмап за O(n).
• Задачи на чётность/нечётность суммы — считаешь префикс по модулю 2, ищешь совпадения.
• Задачи на матрицах — 2D префикс даёт сумму любого прямоугольника за O(1), используется в задачах с изображениями, тепловыми картами, grid-задачах.
• Sliding window с условием на сумму — иногда проще через префикс, чем двумя указателями, особенно если окно не фиксированное.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
+
Синглтон гарантирует, что во всём приложении существует только один экземпляр объекта — например, пул соединений к базе данных или менеджер конфигурации. Это исключает дублирование ресурсов и случайное создание конкурирующих экземпляров.
+
В Go синглтон удобно реализуется через sync.Once: объект создаётся только при первом обращении. Это экономит память и время запуска, если ресурс вообще не понадобится в ходе работы программы.
+
Синглтон позволяет инкапсулировать общее состояние внутри структуры с методами, избегая «голых» глобальных переменных. Это улучшает читаемость кода и упрощает контроль за изменениями состояния.
+
При использовании sync.Once Go гарантирует, что функция инициализации будет вызвана ровно один раз, даже если несколько горутин обратятся к синглтону одновременно. Разработчику не нужно писать дополнительную логику блокировок.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤3
-
Если синглтон не был правильно реализован с учётом многозадачности, могут возникнуть гонки данных, когда несколько горутин одновременно пытаются создать или получить доступ к экземпляру синглтона.
-
Если приложение становится более сложным и распределённым, синглтон может стать ограничением для масштабируемости. В распределённых системах или микросервисах использование синглтонов может привести к проблемам с состоянием и затруднить масштабирование приложения.
-
Синглтон может стать узким местом в многозадачных приложениях, особенно если доступ к нему синхронизирован с использованием блокировок, таких как мьютексы. Если горутины часто обращаются к синглтону и блокируют его, это может существенно снизить производительность программы.
-
Если синглтон требует сложной инициализации, например, создание нескольких объектов, настройка зависимостей, это может замедлить работу приложения, особенно если инициализация не оптимизирована.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🥱2
init() запускается при OnceValue — ленивая инициализация: вычисление происходит только при Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
✅
Создание нового соединения это дорогостоящая операция. Синглтон гарантирует, что во всём приложении используется один общий пул соединений
(*sql.DB), что снижает накладные расходы и предотвращает исчерпание лимитов на стороне базы данных.✅
Параметры приложения: файл конфигурации, переменные окружения. Они читаются один раз при старте. Синглтон даёт удобный единый доступ к этим данным из любой части программы без повторного чтения и парсинга.
✅
Глобальный логгер: он должен быть один, настроен единожды (уровень логирования, вывод, формат) и доступен из любого пакета. В Go это часто реализуется через log/slog или сторонние библиотеки вроде zap.
✅
Если приложению нужен разделяемый кэш (например, результаты тяжёлых вычислений или ответы внешних API), синглтон обеспечивает единое хранилище для всех горутин. Важно сочетать его с sync.RWMutex или использовать sync.Map для безопасного доступа.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1
This media is not supported in your browser
VIEW IN TELEGRAM
Включайте кружок там личное приглашение от спикера. 👆
Уже завтра в прямом эфире, разбираем архитектуру контекста в мультиагентных системах.
🤫 Секретный лут:
👉 Регистрируйтесь на трансляцию
Please open Telegram to view this post
VIEW IN TELEGRAM