Golang для разработчика на Python и PHP

Курс поможет быстро перейти на Go, опираясь на ваш опыт в Python и PHP. Вы освоите синтаксис, типизацию, работу с пакетами, конкурентность, тестирование и создание практичных сервисов на Go.

1. Старт: установка, инструменты, структура проекта и go mod

Старт: установка, инструменты, структура проекта и go mod

Go удобно изучать, если вы уже работали с Python и PHP: вы узнаете знакомые идеи (пакеты, зависимости, тесты), но в более строгой и инструментально-стандартизированной форме. В этой статье вы настроите окружение, разберётесь с базовыми утилитами, типовой структурой проекта и модульной системой go mod.

Установка Go

Официальная сборка Go включает компилятор, стандартную библиотеку и набор инструментов командной строки.

  • Установите Go с официальной страницы загрузок: Go downloads.
  • Следуйте инструкции под вашу ОС: Install Go.
  • Проверьте установку:
  • Если команда отрабатывает и выводит версию (например, go1.22.x), базовая установка готова.

    Важные переменные окружения

    В отличие от старого подхода с GOPATH как центром всего, современные проекты обычно используют модули (Go Modules). Тем не менее некоторые переменные полезно понимать.

  • GOROOT — где установлен Go (обычно выставлять вручную не нужно).
  • GOPATH — рабочая директория для кешей, установленных инструментов и старых GOPATH-проектов (по умолчанию часто GOPATH/bin (или $HOME/go/bin при стандартных настройках). Убедитесь, что этот каталог находится в PATH.
  • IDE и редакторы

  • VS Code: расширение Go extension for Visual Studio Code.
  • GoLand: официальный продукт JetBrains — GoLand.
  • Минимальный проект: первый модуль

    В Go современная точка входа в проект — это модуль (module), описанный в go.mod.

  • Создайте директорию проекта и перейдите в неё:
  • Инициализируйте модуль:
  • Создайте main.go:
  • Запустите:
  • Здесь важно:

  • package main и func main() означают исполняемую программу.
  • go run . запускает пакет в текущем каталоге (аналог «собрать и выполнить»).
  • Как Go организует код: пакеты и импорты

    В Go пакет — это набор .go файлов в одной директории, которые компилируются вместе.

  • Импорт обычно соответствует пути модуля + пути папки пакета.
  • Внутри одного модуля можно иметь много пакетов.
  • Пример (условно): если модуль example.com/hello-go, а пакет лежит в internal/app, то импорт будет example.com/hello-go/internal/app.

    Типовая структура проекта

    Go не навязывает единственный «скелет» проекта на уровне языка, но есть распространённые соглашения.

    Один из популярных ориентиров: Standard Go Project Layout. Это не официальный стандарт, но полезный набор идей.

    Часто встречающиеся директории:

  • cmd/ — точки входа в приложения (несколько бинарников в одном репозитории).
  • internal/ — пакеты, которые нельзя импортировать извне модуля (языковое правило видимости).
  • pkg/ — пакеты, которые предполагаются как публично переиспользуемые (используйте аккуратно; часто достаточно internal/).
  • api/ — схемы API (например, OpenAPI/Swagger, protobuf), если применимо.
  • configs/ — конфиги (но секреты хранить отдельно).
  • !Пример типовой структуры Go-проекта и привязка пакетов к директориям

    Go Modules: что такое go.mod и зачем он нужен

    Модуль — это единица версионирования и распространения зависимостей в Go. Он описывается файлом go.mod в корне проекта.

    go.mod

    Файл go.mod хранит:

  • путь модуля (module path), например example.com/hello-go;
  • версию Go, на которую ориентируется модуль;
  • список зависимостей и их версии.
  • Пример go.mod:

    Официальная справка: Go Modules Reference.

    go.sum

    go.sum содержит контрольные суммы модулей для воспроизводимости загрузки. Обычно его коммитят в репозиторий вместе с go.mod.

    Почему это отличается от Python/PHP

    В Python и PHP вам почти всегда нужен отдельный инструмент менеджмента окружений и зависимостей (pip/poetry, composer) и отдельное понятие «виртуального окружения». В Go:

  • зависимости подтягиваются автоматически при сборке/тестах;
  • кеш модулей общий (не отдельный на проект), а воспроизводимость обеспечивается go.sum;
  • для исполняемых инструментов используется go install ...@version.
  • Сравнение на высоком уровне:

    | Задача | Python | PHP | Go | |---|---|---|---| | Описать зависимости | requirements.txt / pyproject.toml | composer.json | go.mod | | Зафиксировать целостность | poetry.lock | composer.lock | go.sum | | Установка зависимостей | вручную командой | вручную командой | автоматически при go build/test | | Форматирование | разные инструменты | разные инструменты | стандарт: gofmt / go fmt |

    Полезные команды go mod

  • go mod init <module> — создать go.mod.
  • go mod tidy — привести зависимости в порядок: добавить нужные, удалить неиспользуемые.
  • go mod download — скачать зависимости заранее.
  • go mod graph — показать граф зависимостей.
  • go mod why <module> — объяснить, почему модуль нужен.
  • go mod vendor — положить зависимости в папку vendor/ (иногда требуется в корпоративной среде).
  • replace для локальной разработки

    Иногда нужно временно подменить зависимость на локальную копию (например, вы одновременно правите библиотеку и сервис). Для этого используют replace.

    Пример:

    Важно: replace обычно не коммитят в основную ветку продукта (или коммитят осознанно), потому что он привязывает сборку к локальному пути.

    Приватные репозитории

    Если зависимости лежат в приватных Git-репозиториях, Go может пытаться ходить через публичные прокси/сумм-хранилища. Это настраивается переменными окружения, например GOPRIVATE.

    Официальная документация: Private modules.

    Рабочий цикл: минимум команд на каждый день

  • Форматировать весь проект:
  • Запустить тесты во всех пакетах:
  • Собрать бинарник:
  • Проверить типовые проблемы:
  • Привести зависимости в порядок (часто полезно после добавления/удаления импортов):
  • Итоги

  • Вы установили Go и проверили окружение через go version и go env.
  • Разобрались, что основной интерфейс — это команды go ..., а формат кода стандартизирован через go fmt.
  • Поняли разницу между пакетом (директория с .go файлами) и модулем (единица зависимостей с go.mod).
  • Получили базовые навыки работы с go mod и типовой структурой проекта.
  • 2. Синтаксис и типы: переменные, функции, структуры, методы и интерфейсы

    Синтаксис и типы: переменные, функции, структуры, методы и интерфейсы

    После статьи про установку, инструменты и go mod у вас уже есть рабочее окружение и понимание, как устроен модуль и пакеты. Теперь разберём базовый синтаксис Go и ключевые типы данных, чтобы вы могли уверенно читать и писать код: переменные, функции, структуры, методы и интерфейсы.

    Что важно про Go после Python и PHP

    Go старается быть простым, но при этом строгим:

  • Статическая типизация: типы известны компилятору, многие ошибки ловятся до запуска.
  • Нет классов как в PHP/Python, но есть структуры и методы.
  • Интерфейсы реализуются неявно: не нужно писать implements.
  • Ошибки обычно возвращают как обычное значение (error), а не кидают исключения в типичном потоке исполнения.
  • Официальные источники, к которым полезно периодически возвращаться:

  • The Go Programming Language Specification
  • A Tour of Go
  • Effective Go
  • Пакеты, main и видимость имён

    Go-код живёт в пакетах (package). Пакет обычно соответствует директории.

  • package main + func main() — исполняемая программа.
  • Любой другой package xxx — библиотечный пакет.
  • Видимость определяется первой буквой имени:
  • Name — экспортируемое (публичное) из пакета.
  • name — неэкспортируемое (приватное) внутри пакета.
  • Это похоже на public/private, но проще: правила едины, без ключевых слов.

    Базовые типы и нулевые значения

    В Go у каждого типа есть нулевое значение (zero value). Оно используется, когда переменная объявлена, но явно не инициализирована.

    Таблица базовых нулевых значений:

    | Тип | Пример | Нулевое значение | |---|---|---| | bool | var ok bool | false | | int, int64, uint | var n int | 0 | | float64 | var f float64 | 0 | | string | var s string | "" | | указатели, срезы, мапы, функции, интерфейсы | var p *int | nil |

    Важно: нулевое значение — это валидное значение. Например, пустая строка "" и 0 часто являются нормальными значениями, а nil у среза и мапы требует понимания поведения (см. ниже).

    Переменные: var, :=, область видимости

    Объявление через var

    var полезен, когда вы хотите явно задать тип или объявить переменную заранее.

    Короткое объявление :=

    Самый частый стиль внутри функций — короткое объявление:

    Ограничение: := можно использовать только внутри функций.

    Присваивание vs объявление

    Частая ошибка новичков:

    Если переменная уже существует в текущей области видимости, используйте =.

    Область видимости

    Как и в Python/PHP, переменная видна в своём блоке { ... } и во вложенных блоках. Но в Go есть типичный источник путаницы: оператор := может создать новую переменную во внутреннем блоке и тем самым «затенить» внешнюю.

    Константы: const

    const — значения, известные на этапе компиляции.

    const в Go не равен «неизменяемому объекту» из Python. Это именно компиляторная константа.

    Указатели: когда нужны

    В Go есть указатели, но нет арифметики указателей как в C.

  • &x — взять адрес.
  • *p — разыменовать.
  • Для Python/PHP-разума полезная модель:

  • В Go передача аргументов всегда по значению.
  • Но значением может быть указатель.
  • Для многих типов (срезы, мапы) внутри лежит «дескриптор», и копирование этого дескриптора всё равно ведёт к работе с общими данными (подробнее ниже).
  • Составные типы: массивы, срезы, мапы

    Массивы [N]T

    Массив — фиксированная длина, длина входит в тип.

    На практике массивы используются реже, чем срезы.

    Срезы []T

    Срез — «окно» (view) на массив под капотом. Это самый частый тип коллекции.

    Ключевые моменты:

  • len(s) — длина, cap(s) — ёмкость.
  • var s []int даёт nil-срез. Он ведёт себя почти как пустой: len(s) == 0, его можно append.
  • Сравнивать срезы напрямую нельзя (кроме сравнения с nil).
  • Мапы map[K]V

    map — хеш-таблица.

    Важно:

  • Чтение отсутствующего ключа возвращает нулевое значение типа значения.
  • Чтобы отличить «ключа нет» от «значение равно нулю», используйте форму v, ok := m[key].
  • var m map[string]int создаёт nil-мапу. Читать из неё можно, писать нельзя (будет panic). Для записи нужна make(map[string]int).
  • Функции: параметры, несколько результатов, error

    Обычная функция

    Если несколько параметров одного типа, тип можно писать один раз:

    Несколько возвращаемых значений

    Это одна из вещей, из-за которых Go часто кажется «не Python, но удобно».

    Идиома value, err := ...

    Ошибки обычно обрабатывают сразу:

    defer — отложенный вызов (часто для Close()), выполнится при выходе из функции.

    Анонимные функции и замыкания

    Структуры: свои типы данных

    Структура — набор полей. Это базовый способ описывать данные.

    Полезные детали:

  • Инициализация по именам полей более устойчива к изменениям структуры.
  • Доступ к полю: u.Name.
  • Нулевое значение структуры — структура, где все поля равны своим нулевым значениям.
  • Встраивание (embedding)

    Go позволяет встраивать один тип в другой без отдельного имени поля.

    Тогда Order «получает» доступ к CreatedAt как order.CreatedAt. Это не наследование в ООП-смысле, но удобный механизм композиции.

    Методы: функции с получателем

    Метод — функция, у которой есть получатель (receiver).

    Получатель-значение и получатель-указатель

  • func (c Counter) ... получает копию структуры.
  • func (c *Counter) ... получает указатель и может менять оригинал.
  • Практическое правило:

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

    Интерфейс в Go — это набор методов. Любой тип, у которого есть эти методы, автоматически удовлетворяет интерфейсу.

    !Диаграмма, показывающая, что типы реализуют интерфейс наличием методов без ключевого слова implements

    Почему это важно после Python/PHP:

  • В Go вы чаще проектируете код от интерфейсов, описывающих поведение.
  • Интерфейсы обычно небольшие (1–3 метода). Это соответствует рекомендациям из стандартной экосистемы.
  • Пустой интерфейс и any

    interface{} — «любой тип». Начиная с Go 1.18, для читаемости есть алиас any.

    Использовать any стоит аккуратно: вы теряете статическую информацию о типе и чаще переходите к проверкам во время выполнения.

    Утверждение типа (type assertion)

    Если у вас интерфейсное значение и вы ожидаете конкретный тип:

  • Форма x.(T) без ok может привести к panic, если тип не совпал.
  • Безопаснее использовать value, ok := ....
  • Частые ошибки и «острые углы» новичка

  • Путаница := и = и случайное затенение переменных во вложенных блоках.
  • nil-мапа: чтение работает, запись приводит к panic; используйте make.
  • Ожидание исключений как основного механизма ошибок: в Go типично возвращать error.
  • Сравнение срезов: нельзя сравнивать []T друг с другом напрямую.
  • Неверный получатель метода: хотите изменять структуру — используйте *T.
  • Итоги

  • Вы освоили объявления переменных (var, :=), константы (const) и нулевые значения.
  • Разобрались с базовыми составными типами: массивы, срезы, мапы.
  • Поняли стиль Go-функций: несколько результатов и идиома value, err := ....
  • Научились описывать данные структурами, добавлять методы и выбирать получатель.
  • Увидели, как интерфейсы в Go описывают поведение и реализуются неявно.
  • В следующих темах эти элементы начнут складываться в «прикладной» Go: работа с пакетами стандартной библиотеки, ошибки, тесты и построение небольших сервисов.

    3. Коллекции и управление потоком: срезы, карты, циклы, ошибки и defer

    Коллекции и управление потоком: срезы, карты, циклы, ошибки и defer

    В предыдущей статье вы разобрали базовые типы, функции, структуры, методы и интерфейсы. Теперь соберём это в практический набор, без которого сложно писать реальный Go-код: работа со срезами и картами, управление потоком через if/for/switch, а также типичный для Go подход к ошибкам и освобождению ресурсов через defer.

    Цель этой темы: чтобы вы могли уверенно читать типичный код из стандартной библиотеки и прикладных сервисов.

    Срезы: главный тип коллекций в Go

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

    !Схема, объясняющая что срез хранит ссылку на массив и параметры len/cap

    Создание срезов

  • Литерал:
  • Через make с длиной и ёмкостью:
  • Нулевое значение:
  • nil-срез часто ведёт себя как пустой:

  • len(s) == 0
  • append работает
  • но s == nil будет true, в отличие от пустого, созданного через make([]int, 0)
  • Это отличие важно, когда вы сериализуете данные (например, в JSON) или явно проверяете, инициализировали ли коллекцию.

    append: добавление элементов и рост

    Добавление в срез делается через append:

    Ключевая деталь: append может вернуть новый срез, который указывает на новый массив.

  • Если ёмкости хватило, новый элемент попадёт в тот же массив.
  • Если ёмкости не хватило, Go выделит новый массив, скопирует данные и вернёт срез на него.
  • Поэтому практически всегда пишут s = append(s, ...), а не просто append(s, ...).

    Подсрезы и общий массив

    Подсрез берётся так:

    b обычно разделяет подлежащий массив с a. Это может приводить к неожиданным эффектам:

    Если b имел достаточную ёмкость, append запишет в общий массив, и исходный a изменится.

    Практическое правило: если вы хотите отделиться от исходных данных, сделайте копию.

    Копирование срезов

    Для копирования используйте copy:

    Если вам нужна копия подсреза:

    Здесь append с nil-срезом создаст новый массив и скопирует элементы.

    Итерация по срезу: for и range

  • Классический цикл:
  • Через range:
  • Если индекс не нужен:

    Карты: map[K]V и идиома value, ok

    map в Go это хеш-таблица.

    Создание карты

  • Литерал:
  • Через make:
  • Нулевое значение:
  • Важное отличие:

  • читать из nil-map можно, вернётся нулевое значение
  • писать в nil-map нельзя, будет panic
  • Проверка наличия ключа

    Чтение отсутствующего ключа возвращает нулевое значение, поэтому используют форму с ok:

    Это очень похоже на проверку isset в PHP или in в Python, но оформлено как часть операции чтения.

    Удаление ключа

    delete безопасен даже если ключа нет.

    Итерация по map

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

  • собирают ключи в срез
  • сортируют
  • обходят по отсортированным ключам
  • Управление потоком: if, for, switch

    Go специально оставляет небольшой набор конструкций, но они закрывают большинство задач.

    if и короткое объявление

    if может содержать короткое объявление перед условием:

    Это один из самых распространённых паттернов в Go: объявили значение и ошибку прямо в условии.

    for: единственный цикл

    В Go нет while и do-while, вместо этого разные формы for.

  • Как while:
  • Бесконечный:
  • С инициализацией:
  • switch: читаемый выбор ветки

    switch по значению:

    switch без выражения часто заменяет цепочки if/else if:

    В Go switch по умолчанию не проваливается в следующую ветку, в отличие от многих языков. Для явного проваливания есть fallthrough, но он используется редко.

    Ошибки: error как значение

    В Python и PHP вы привыкли к исключениям как к основному механизму ошибок. В Go основной путь другой: функция возвращает error как обычное значение.

    Что такое error

    error это интерфейс из стандартной библиотеки:

    Документация: Built-in interface error.

    Типичный стиль обработки

    Смысл в том, что ошибка обрабатывается рядом с местом, где возникла. Это делает поток исполнения более явным.

    Создание и оборачивание ошибок

  • Простая ошибка:
  • Форматирование:
  • Оборачивание с сохранением причины через %w:
  • Это важно, чтобы выше по стеку можно было проверить тип или конкретную причину.

    Проверка причины: errors.Is и errors.As

  • errors.Is(err, target) проверяет, есть ли в цепочке обёрток конкретная ошибка.
  • errors.As(err, &targetType) пытается извлечь ошибку определённого типа.
  • Документация: Package errors.

    panic и почему это не замена error

    panic это аварийное завершение текущего потока исполнения с раскруткой стека. Обычно его используют для:

  • ошибок программиста, которые не должны происходить (нарушение инварианта)
  • действительно невосстановимых ситуаций
  • В прикладном коде и особенно в сервисах вместо panic почти всегда предпочитают возвращать error.

    defer: гарантированное выполнение в конце функции

    defer откладывает вызов функции до момента выхода из текущей функции.

    Классический пример: закрыть файл.

    Порядок выполнения: стек (LIFO)

    Несколько defer выполняются в обратном порядке:

    Это удобно для симметричного освобождения ресурсов.

    Когда вычисляются аргументы

    Аргументы отложенного вызова вычисляются сразу, в момент объявления defer.

    Это частая точка удивления после Python.

    defer и ошибки закрытия

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

    recover: очень осторожно

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

    Документация: Built-in functions panic and recover.

    Сравнение с Python и PHP: быстрый ориентир

    | Тема | Python | PHP | Go | |---|---|---|---| | Динамическая коллекция | list | массив | []T (срез) | | Ассоциативная коллекция | dict | массив | map[K]V | | Проверка ключа | k in d | array_key_exists / isset | v, ok := m[k] | | Цикл | for, while | for, foreach, while | for + range | | Ошибки | исключения | исключения | возвращаемый error | | Освобождение ресурсов | with | try/finally | defer |

    Итоги

  • Срез []T это дескриптор на массив: len/cap и общий подлежащий массив объясняют многие эффекты append и подсрезов.
  • map[K]V требует понимания nil-map и идиомы value, ok.
  • Управление потоком строится вокруг if с коротким объявлением, единственного цикла for и удобного switch.
  • Ошибки в Go это значения типа error, их обычно обрабатывают сразу; для цепочек причин используют %w, errors.Is, errors.As.
  • defer помогает гарантированно освобождать ресурсы и выполняется в обратном порядке, а аргументы вычисляются в момент объявления.
  • Дополнительное чтение:

  • A Tour of Go: For
  • A Tour of Go: Slices
  • A Tour of Go: Maps
  • Effective Go
  • 4. Пакеты и качество кода: стиль, контекст, логирование и обработка ошибок

    Пакеты и качество кода: стиль, контекст, логирование и обработка ошибок

    В предыдущих темах вы настроили рабочее окружение и go mod, освоили базовый синтаксис, структуры, интерфейсы, коллекции, управление потоком, error и defer. Теперь перейдём к тому, что делает Go-код поддерживаемым в реальных сервисах и библиотеках: как проектировать пакеты, придерживаться стилевых конвенций, правильно использовать context.Context, логировать так, чтобы это было полезно в продакшене, и строить обработку ошибок, чтобы её было легко сопровождать.

    Пакеты в Go: как думать о границах кода

    В Go пакет — это базовая единица организации кода. Пакет обычно соответствует директории, а публичное API пакета определяется экспортируемыми идентификаторами (имена с заглавной буквы).

    Принципы хорошего пакета

  • Один пакет — одна ответственность: пакет должен решать одну связанную задачу.
  • Минимальный публичный API: экспортируйте только то, что реально нужно внешнему коду.
  • Зависимости направлены внутрь: прикладные пакеты зависят от общих утилит, но не наоборот.
  • Имена пакетов короткие: чаще всего одно слово, без utils, без заикания.
  • > “The name of a package should be short, concise, evocative.”Effective Go (официальная документация) Effective Go

    internal: важная граница

    Каталог internal/ — это не просто соглашение, а правило компилятора: пакеты внутри internal/ нельзя импортировать извне вашего модуля (и некоторых соседних деревьев).

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

    !Диаграмма границ пакетов и роли internal/pkg/cmd

    cmd: точки входа приложения

    Если у репозитория несколько исполняемых файлов, их обычно кладут в cmd/<имя>/main.go.

  • cmd/api — HTTP API
  • cmd/worker — воркер
  • cmd/migrate — миграции
  • Пакеты стандартной библиотеки и «не изобретать фреймворк»

    Go поощряет использование стандартной библиотеки: net/http, context, database/sql, log/slog, testing. Это уменьшает «зоопарк» зависимостей и упрощает онбординг.

    Стиль и читаемость: что считается качеством в Go

    В Go принято считать качеством не «умный» код, а предсказуемый.

    Базовые инструменты качества

  • gofmt через go fmt — единый формат кода.
  • go vet — типичные проверки ошибок.
  • go test ./... — быстрый и стандартный запуск тестов.
  • Два полезных источника соглашений

  • Go Code Review Comments
  • Uber Go Style Guide
  • Это не «законы языка», но в реальных командах именно на такие документы опираются в ревью.

    Имена и «заикание»

    Имена в Go читаются слева направо и часто включают имя пакета.

    Плохо:

  • пакет user, функция GetUserUser()
  • пакет logger, тип LoggerLogger
  • Хорошо:

  • user.Get(id) или user.ByID(id)
  • log.New(...) или logger.New(...), но без повторов
  • Возвращайте ранние ошибки

    В Go принято уменьшать вложенность через «ранний выход».

    Плохо:

    Хорошо:

    context.Context: отмена, дедлайны и запросы

    context.Context — стандартный механизм для:

  • отмены работы (cancellation)
  • дедлайнов и таймаутов
  • передачи request-scoped значений (очень ограниченно)
  • Документация: Package context

    Главное правило: Context приходит сверху и уходит вниз

    Типичный стиль API в Go:

  • ctx — первый параметр функций, которые могут ждать, ходить в сеть, в БД, вызывать другие сервисы.
  • контекст создаётся на границе (например, HTTP-запрос) и прокидывается вглубь.
  • Пример функции, которая принимает контекст:

    Обратите внимание на QueryRowContext: стандартная библиотека и многие драйверы/клиенты поддерживают контекст напрямую.

    Таймауты

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

  • WithTimeout возвращает новый контекст и функцию cancel.
  • defer cancel() нужен, чтобы освободить ресурсы таймера, даже если таймаут не наступил.
  • Типичные ошибки при работе с контекстом

  • Не хранить context.Context внутри структур как поле на долгое время.
  • Не делать ctx := context.Background() внутри «глубокого» кода, если контекст уже был снаружи.
  • Не использовать context.WithValue как замену параметрам. Его применяют редко и в основном на границе (например, request id), когда договорённость принята во всей кодовой базе.
  • Логирование: от Printf к структурным логам

    Для маленьких утилит достаточно log, но для сервисов лучше использовать структурное логирование.

    Начиная с Go 1.21 в стандартной библиотеке есть пакет log/slog.

    Документация: Package log/slog

    Почему структурные логи лучше

  • легко фильтровать по полям (user_id, request_id, error)
  • проще отправлять в системы логирования
  • меньше «склеек строк» и неоднозначностей
  • Минимальный пример slog

    Где логировать ошибки

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

  • внутри библиотечных пакетов чаще возвращают error, но не логируют
  • в слое обработки запроса (HTTP handler, consumer) ошибку логируют и превращают в ответ/ретрай/метрику
  • Это уменьшает дубли в логах, когда одна и та же ошибка «пропечатывается» на каждом уровне.

    Обработка ошибок как часть дизайна

    Вы уже видели идиому value, err := ... и fmt.Errorf("...: %w", err). Теперь важнее понять, как проектировать ошибки.

    Оборачивайте ошибки с контекстом

    Обёртка добавляет смысл, что именно вы пытались сделать.

  • строка до %w — человекочитаемый контекст
  • %w — сохраняет исходную ошибку, чтобы её можно было проверить через errors.Is и извлечь через errors.As
  • Документация: Package errors

    Sentinel errors: известные причины

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

    Тогда на верхнем уровне:

    Свои типы ошибок, когда нужны поля

    Если вам нужно передать машинно-обрабатываемые детали (например, поле валидации), удобно сделать тип ошибки.

    И извлечение:

    Не используйте panic для обычных ошибок

    panic уместен для нарушений инвариантов и ошибок программиста (например, невозможное состояние), но не для ожидаемых ошибок ввода, сети, БД.

    Документация: Built-in functions panic and recover

    Сборка всего вместе: границы пакетов, контекст, лог и ошибки

    Ниже пример «сквозного» сценария: HTTP-обработчик создаёт контекст с таймаутом, вызывает сервис, логирует один раз и выбирает HTTP-код на основе причины.

    Здесь важно:

  • r.Context() — контекст запроса, который отменится, если клиент отключился.
  • WithTimeout — защита от «вечных» зависаний.
  • логирование происходит в одном месте, потому что именно здесь принимается решение, что отдавать клиенту.
  • ошибки в сервисе должны быть обёрнуты так, чтобы errors.Is(err, user.ErrNotFound) работал.
  • Итоги

  • Хороший дизайн пакетов в Go строится вокруг небольшого публичного API и понятных границ, особенно через internal/ и cmd/.
  • Стиль в Go во многом стандартизирован: gofmt, ранний выход при ошибках, читаемые имена и предсказуемые соглашения.
  • context.Context — обязательная часть кода, который ждёт или делает I/O; контекст прокидывается сверху вниз и управляет таймаутами и отменой.
  • Для сервисов предпочтительно структурное логирование через log/slog, а ошибки логируются обычно один раз на границе.
  • Ошибки проектируются: оборачивайте с %w, используйте errors.Is/As, применяйте sentinel errors и типы ошибок там, где это оправдано.
  • 5. Конкурентность: горутины, каналы, select, race conditions и синхронизация

    Конкурентность: горутины, каналы, select, race conditions и синхронизация

    В прошлых темах вы научились писать поддерживаемый Go-код: разделять пакеты, работать с context.Context, логировать через log/slog и строить понятную обработку ошибок. Следующий шаг к реальным сервисам и утилитам на Go — конкурентность: умение выполнять несколько задач одновременно, безопасно работать с общей памятью и корректно завершать фоновые операции.

    Go отличается от Python и PHP тем, что конкурентность встроена в язык и стандартную библиотеку: горутины и каналы дают простой базовый уровень, а пакет sync закрывает потребности синхронизации.

    Полезные официальные ссылки:

  • Tour of Go: Concurrency
  • Package sync
  • Package sync/atomic
  • The Go Memory Model
  • Data Race Detector
  • Конкурентность и параллелизм

  • Конкурентность — умение организовать выполнение задач так, чтобы они продвигались независимо (например, ожидание сети, обработка очереди, таймауты).
  • Параллелизм — фактическое выполнение нескольких задач одновременно на разных ядрах.
  • Go в первую очередь помогает с конкурентностью. Параллелизм обычно получается “сам” за счёт рантайма и планировщика, если есть доступные ядра.

    Горутины

    Горутина — лёгкий поток исполнения, который запускается ключевым словом go. В отличие от потоков ОС, горутины дешёвые по памяти и переключению контекста, поэтому их обычно создают много.

    Важная мысль: если main завершится, процесс завершится вместе со всеми горутинами. Поэтому в реальном коде вы почти всегда используете координацию завершения: WaitGroup, каналы, контекст.

    !Схема конкурентного выполнения main и горутины и почему важно дожидаться завершения

    Типичная ошибка: захват переменной цикла

    Одна из самых частых ошибок после Python: в Go замыкание захватывает переменную, а не её значение.

    Плохо:

    Хорошо:

    Или так:

    Ожидание завершения: sync.WaitGroup

    WaitGroup — базовый инструмент, чтобы дождаться набора горутин.

    Правила использования:

  • Add должен быть вызван до запуска горутины.
  • Done вызывают ровно один раз на одну “единицу” Add.
  • Wait блокирует вызывающую горутину, пока счётчик не станет равен нулю.
  • Каналы

    Канал (chan T) — типизированный поток значений, который используется для обмена данными и координации.

  • Отправка: ch <- v
  • Получение: v := <-ch
  • Получение с признаком закрытия: v, ok := <-ch
  • Небуферизованный канал

    Небуферизованный канал синхронизирует отправителя и получателя: отправка блокирует, пока кто-то не прочитает.

    Буферизованный канал

    Буферизованный канал позволяет отправить до cap(ch) значений без немедленного получателя.

    Практическая мысль: буфер — это очередь фиксированного размера, а не “ускоритель”. Он помогает сгладить пики и уменьшить связанность производителя и потребителя.

    Закрытие канала и range

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

  • Закрыть: close(ch)
  • Читать до закрытия удобно через for v := range ch { ... }
  • Важно:

  • Отправка в закрытый канал приводит к panic.
  • Получение из закрытого канала возвращает нулевое значение и ok == false.
  • Закрывать канал нужно не “на всякий случай”, а когда это часть протокола общения.
  • Однонаправленные каналы

    Чтобы сделать API безопаснее, можно принимать или возвращать каналы только на чтение или только на запись.

    !Схема обмена данными через канал между горутинами

    select: ожидание нескольких событий

    select позволяет ждать одно из нескольких событий:

  • значение пришло из канала
  • удалось отправить в канал
  • сработал таймер
  • пришла отмена по context.Context
  • Таймаут через time.After

    Отмена через context.Context

    Это связывает конкурентность с темой из предыдущей статьи: context должен управлять долгими операциями и корректным завершением горутин.

    select с default

    default делает select неблокирующим. Это полезно для “попробовать и уйти”, но опасно, если вы случайно превращаете ожидание в активный цикл.

    Если default стоит внутри for, часто требуется time.Sleep или другой механизм, чтобы не сжечь CPU.

    Шаблоны конкурентности

    Fan-out / fan-in (воркеры + сбор результатов)

    Это один из самых распространённых паттернов в сервисах: раздать задачи нескольким воркерам и собрать результаты.

    Ключевая идея: тот, кто “знает”, что значений больше не будет, тот и закрывает канал.

    Race conditions и как их ловить

    Data race (гонка данных) — ситуация, когда несколько горутин обращаются к одной памяти одновременно, хотя бы одна операция — запись, и между ними нет синхронизации. Результат может быть непредсказуемым.

    !Иллюстрация потерянного обновления при конкурентной записи

    Пример гонки

    Race detector

    Go умеет обнаруживать гонки динамически во время тестов и запуска.

  • Запуск тестов с детектором гонок: go test -race ./...
  • Запуск программы: go run -race .
  • Подробности: Data Race Detector

    Важно: детектор гонок не “доказывает”, что гонок нет, но очень эффективно ловит реальные проблемы.

    Синхронизация: основные инструменты

    Есть два базовых стиля:

  • Разделяем память и защищаем её (Mutex, RWMutex, atomic).
  • Не делим память, а передаём данные сообщениями (каналы).
  • Оба подхода используются в реальных проектах, выбор зависит от модели данных и требований к производительности.

    sync.Mutex

    Mutex защищает критическую секцию: в каждый момент времени только одна горутина может выполнять защищённый участок.

    Правила:

  • Держите критическую секцию как можно короче.
  • Не забывайте Unlock, часто используют defer mu.Unlock().
  • Не делайте внутри Lock долгих операций (I/O, сеть), иначе блокировка станет “узким местом”.
  • Документация: Package sync

    sync.RWMutex

    Если у вас много чтений и мало записей, можно использовать RWMutex:

  • RLock допускает параллельные чтения
  • Lock эксклюзивен для записи
  • Используйте его осознанно: иногда обычный Mutex быстрее из-за меньших накладных расходов.

    sync/atomic

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

    Документация: Package sync/atomic

    Синхронизация через каналы

    Иногда удобнее выделить одну горутину-владельца состояния и отправлять ей команды через канал. Тогда состояние не разделяется между горутинами и Mutex не нужен.

    Этот стиль часто хорошо ложится на “акторную” модель и помогает избежать гонок ценой дополнительной очереди сообщений.

    Deadlock, утечки горутин и практические правила

    Deadlock

    Дедлок обычно возникает, когда:

  • все горутины ждут чтения или записи в каналы, но никто не может продолжить
  • Mutex захвачен и не освобождается (например, ранний return до Unlock)
  • Go рантайм может завершить программу сообщением вида fatal error: all goroutines are asleep - deadlock!, если дедлок затрагивает весь процесс.

    Утечка горутин

    Горутина “утекает”, если она навсегда зависла в ожидании (например, ждёт чтения из канала, который никогда не закроется, или ждёт отправки, которую никто не прочитает). Такие утечки в сервисах копятся и приводят к росту памяти.

    Практические приёмы:

  • Всегда продумывайте условие завершения горутины.
  • Для долгих операций используйте context.Context и проверяйте ctx.Done().
  • Явно определяйте, кто закрывает канал и когда.
  • Итоги

  • Горутины — дешёвый способ конкурентного выполнения; для ожидания завершения часто используют sync.WaitGroup.
  • Каналы — типизированный механизм передачи данных и координации; закрывать канал должен отправитель, который знает, что значений больше не будет.
  • select позволяет ждать несколько событий: каналы, таймауты, отмену через context.Context.
  • Гонки данных — частая ошибка; используйте go test -race и защищайте общую память через Mutex, atomic или проектируйте обмен через каналы.
  • Следите за корректным завершением горутин, иначе возможны дедлоки и утечки.
  • 6. Тестирование и производительность: unit-тесты, моки, бенчмарки и профилирование

    Тестирование и производительность: unit-тесты, моки, бенчмарки и профилирование

    После тем про пакеты, качество кода, context.Context, логирование и конкурентность вы уже можете писать реальный Go-код. Следующий шаг, без которого трудно поддерживать сервисы в продакшене: системно проверять корректность через тесты и понимать производительность через бенчмарки и профили.

    Go здесь приятно отличается от многих экосистем тем, что тестирование и бенчмарки встроены в стандартный инструмент go test, а профилирование стандартизировано через pprof.

    Полезные ссылки:

  • Пакет testing
  • Команда go test
  • Пакет net/http/httptest
  • Пакет runtime/pprof
  • Пакет net/http/pprof
  • go tool pprof
  • Как Go видит тесты

    Тесты в Go живут рядом с кодом:

  • файлы имеют суффикс _test.go
  • функции с тестами имеют сигнатуру func TestXxx(t *testing.T)
  • тесты запускаются командой go test
  • Минимальный пример:

    Запуск:

    Unit-тесты: стиль, который чаще всего встречается в продакшене

    Table-driven tests

    Самый распространённый стиль в Go: таблица кейсов и цикл.

    Здесь важно:

  • t.Run делает под-тесты, их удобнее читать в выводе
  • tt := tt защищает от ошибок с захватом переменной цикла в замыкании (особенно если внутри появится t.Parallel())
  • Когда использовать t.Fatalf, а когда t.Errorf

  • t.Fatalf останавливает текущий тест сразу
  • t.Errorf продолжает выполнение теста, собирая несколько ошибок
  • Практическое правило: если дальше тест не имеет смысла (например, не удалось создать объект или результат явно неправильный), используйте t.Fatalf.

    t.Helper() для удобных хелперов

    Если вы пишете функцию-помощник для теста, пометьте её как helper, чтобы при падении строка указывала на место вызова, а не внутрь хелпера.

    Тестирование ошибок: errors.Is и errors.As

    Из предыдущих тем вы уже знаете, что в Go ошибки возвращаются значением и часто оборачиваются через %w. Это влияет на тесты: сравнивать ошибку по строке почти всегда плохая идея.

    Пример с sentinel error:

    Такой тест не сломается, если вы позже добавите контекст через fmt.Errorf("find: %w", err).

    Моки в Go: чаще всего это не фреймворк, а интерфейсы и фейки

    В Python и PHP часто используют мощные mocking-фреймворки. В Go основной подход другой:

  • проектируете код вокруг маленьких интерфейсов
  • в тестах подставляете фейк (ручная реализация интерфейса)
  • !Как интерфейсы позволяют подменять зависимости в тестах

    Пример: сервис зависит от интерфейса

    Фейк для теста:

    Плюсы подхода:

  • не нужен внешний инструмент
  • тест быстро читается
  • вы не тестируете детали реализации, только контракт
  • HTTP: тестирование обработчиков через httptest

    Если вы пишете HTTP API, стандартная библиотека даёт готовые инструменты.

    Это чаще эффективнее, чем поднимать реальный сервер на порту.

    Практичные команды go test

  • Запуск всех тестов:
  • Запуск с подробным выводом:
  • Запуск одного теста по имени (regexp):
  • Покрытие:
  • Отчёт по покрытию в HTML:
  • Поиск гонок данных (важно после темы конкурентности):
  • Бенчмарки: измеряем скорость и аллокации

    Бенчмарк в Go это функция func BenchmarkXxx(b *testing.B).

    Пример: сравним конкатенацию строк.

    Запуск:

    Если хотите ещё и аллокации:

    Важные правила бенчмарков

  • не выводите ничего в консоль внутри цикла
  • избегайте I/O (сеть, файлы) в микробенчмарках
  • если подготовка данных дорогая, вынесите её до измерения и используйте b.ResetTimer()
  • чтобы не дать компилятору выкинуть вычисления, присваивайте результат в пакетную переменную
  • Пример с защитой от оптимизаций:

    Профилирование: находим, куда уходит CPU и память

    Бенчмарк говорит что стало быстрее или медленнее. Профиль объясняет почему: какие функции потребляют CPU, где аллокации, где растёт heap.

    !Итеративный процесс: измеряем, профилируем, улучшаем

    CPU-профиль для бенчмарка

    go test умеет писать профили прямо при запуске бенчмарков:

    Дальше анализ:

    Внутри pprof полезные команды:

  • top показывает самые тяжёлые функции
  • list ИмяФункции показывает аннотированный исходник
  • web строит граф (потребуется Graphviz)
  • Heap-профиль (память)

    Для heap:

    Анализ:

    Практическая интерпретация:

  • CPU-профиль отвечает за время
  • heap/allocs профиль отвечает за память и аллокации
  • Часто оптимизация аллокаций ускоряет код, потому что снижает нагрузку на сборщик мусора.

    Профилирование работающего HTTP-сервиса через net/http/pprof

    Для сервисов важно профилировать не синтетический код, а реальный сценарий под нагрузкой. Стандартный подход: включить pprof endpoint.

    Дальше можно снять профиль CPU на 10 секунд:

    Или посмотреть heap:

    Важно для безопасности:

  • не публикуйте pprof endpoint в интернет без защиты
  • часто его вешают только на localhost или внутреннюю сеть
  • Что тестировать, а что измерять

    Типичная стратегия в Go-проектах:

  • unit-тестами закрывают чистую бизнес-логику и пограничные случаи
  • интеграционными тестами проверяют реальные зависимости (БД, очереди), но реже и тяжелее
  • бенчмарками покрывают критические участки (парсинг, сериализация, горячие циклы)
  • профилированием занимаются, когда есть симптом: задержки, рост CPU, рост памяти, GC pressure
  • Итоги

  • Тесты в Go встроены в стандартный инструментарий: _test.go, go test ./....
  • Самый частый стиль unit-тестов: table-driven + t.Run.
  • Ошибки корректнее проверять через errors.Is и errors.As, а не по строке.
  • Моки в Go часто реализуются как ручные фейки интерфейсов, а для HTTP удобно использовать net/http/httptest.
  • Бенчмарки пишутся на testing.B и запускаются через go test -bench ..., полезно включать -benchmem.
  • pprof помогает понять причины: куда уходит CPU и где происходят аллокации; профили можно снимать как с бенчмарков, так и с работающего сервиса.
  • 7. Практика: HTTP/REST сервис, работа с БД, конфигурация и деплой

    Практика: HTTP/REST сервис, работа с БД, конфигурация и деплой

    В предыдущих темах вы освоили синтаксис, ошибки и defer, проектирование пакетов, context.Context, логирование и конкурентность, а также тестирование и профилирование. Теперь соберём всё это в практический мини-проект: HTTP/REST сервис с доступом к базе данных, конфигурацией и минимально жизнеспособным деплоем.

    В качестве примера сделаем небольшой сервис задач (tasks), который:

  • принимает и отдаёт JSON
  • хранит данные в PostgreSQL
  • корректно работает с context.Context и таймаутами
  • логирует структурно
  • собирается в контейнер и запускается вместе с БД
  • !Общая схема слоёв HTTP сервиса и связи с БД, конфигурацией и деплоем

    Что такое REST и из чего состоит HTTP сервис в Go

    REST в прикладном смысле — это стиль HTTP API, где вы работаете с ресурсами (например, tasks) через стандартные HTTP методы:

  • POST /tasks создать
  • GET /tasks/{id} прочитать
  • GET /tasks список
  • DELETE /tasks/{id} удалить
  • В Go на базе стандартной библиотеки (net/http) сервис обычно строится слоями:

  • handler получает HTTP запрос и формирует HTTP ответ
  • service содержит бизнес-логику
  • repository отвечает за хранение (SQL-запросы)
  • Это не единственно возможная архитектура, но она хорошо помогает разделять ответственность и писать тесты.

    Структура проекта

    Один из удобных вариантов структуры для такого сервиса:

  • cmd/api точка входа HTTP сервера
  • internal/app сборка зависимостей (config, logger, db, router)
  • internal/httpapi handlers и middleware
  • internal/service бизнес-логика
  • internal/storage работа с БД (repository)
  • migrations SQL миграции
  • Пример дерева:

    HTTP слой: роутинг, JSON и обработка ошибок

    Роутинг на стандартной библиотеке

    Начнём с http.ServeMux. В современных версиях Go ServeMux поддерживает шаблоны маршрутов, включая параметры пути.

    Документация: net/http

    Модель данных и JSON

    Для примера используем простую модель Task:

    Для чтения/записи JSON используйте encoding/json.

    Документация: encoding/json

    Практичные правила для handler-ов:

  • ограничивать размер тела запроса через http.MaxBytesReader
  • запрещать неожиданные поля через Decoder.DisallowUnknownFields()
  • проверять обязательные поля вручную (или через отдельную валидацию)
  • Единый формат ошибок для клиентов

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

    Теперь handler может везде отдавать {"error":"..."}.

    Handler + service + маппинг ошибок в HTTP коды

    Идея: service возвращает доменные ошибки (например, ErrNotFound), а HTTP слой превращает это в коды 404/400/500.

    Обратите внимание:

  • r.Context() прокидывается дальше, чтобы отмена запроса или таймауты корректно остановили работу
  • логирование ошибки происходит на границе (в handler), чтобы избежать дублирования логов на каждом слое
  • Service слой: бизнес-логика и контекст

    Service обычно:

  • проверяет входные данные
  • выбирает сценарии (создать, получить, удалить)
  • может задавать таймауты на зависимость (например, БД)
  • Здесь 2time.Second — это ограничение времени работы именно с БД (а не всего HTTP запроса). Таймауты можно настраивать, но важно понимать принцип: контекст управляет временем и отменой*.

    Документация: context

    Хранилище: PostgreSQL через database/sql

    Что такое database/sql

    database/sql — стандартный пакет Go, который даёт общий API для SQL баз. Он работает через драйвер.

    Документация: database/sql

    Для PostgreSQL часто используют драйвер pgx в режиме совместимости с database/sql.

  • драйвер: github.com/jackc/pgx/v5/stdlib
  • Подключение в go.mod делается обычным способом через импорт и go mod tidy.

    Миграция схемы (минимальный вариант)

    Миграция — это изменение схемы БД, которое хранится в виде файлов и применяется последовательно.

    Пример migrations/001_init.sql:

    В реальных проектах миграции часто применяются отдельной командой или отдельным контейнером, чтобы:

  • не смешивать запуск приложения и изменение схемы
  • контролировать, когда и кем применена миграция
  • Реализация репозитория

    Практичные правила работы с sql.DB:

  • sql.DB создаётся один раз и живёт долго
  • sql.DB безопасен для конкурентного использования
  • запросы делаются с QueryContext/ExecContext, чтобы уважать context.Context
  • Конфигурация: переменные окружения и валидация

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

  • os.LookupEnv для чтения
  • дефолты в коде
  • простая валидация
  • Документация: os

    Практичные правила:

  • секреты (пароли, токены) храните в переменных окружения или секрет-хранилищах, а не в репозитории
  • валидируйте конфиг на старте и падайте быстро, если чего-то не хватает
  • Логирование и middleware

    Для продакшена полезно:

  • JSON логи
  • request-scoped поля (например, request id)
  • логирование времени запроса
  • В Go 1.21+ для структурного логирования есть log/slog.

    Документация: log/slog

    Пример middleware:

    Запуск приложения и graceful shutdown

    Graceful shutdown — корректное завершение сервера: перестать принимать новые запросы и дать текущим запросам завершиться в рамках таймаута.

    В Go это делается через http.Server.Shutdown(ctx).

    Документация: http.Server

    Здесь важно:

  • ReadHeaderTimeout защищает от некоторых классов медленных атак и зависаний
  • Shutdown завершает сервер корректно, если контекст не истёк
  • Деплой через Docker

    Dockerfile (multi-stage)

    Multi-stage сборка позволяет собрать бинарник в одном образе и положить только результат в другой.

    Пояснения:

  • CGO_ENABLED=0 помогает получить статический бинарник (полезно для минимальных образов)
  • -trimpath и -ldflags "-s -w" уменьшают размер
  • distroless образ минимальный и безопаснее, чем полный Debian/Ubuntu
  • docker-compose для локального запуска

    Запуск:

    После запуска проверьте:

    Что улучшать дальше

    Когда базовый сервис заработал, обычно добавляют:

  • отдельную команду для миграций (cmd/migrate) и управляемый процесс выката схемы
  • middleware для request id и проброса его в логи
  • метрики (например, Prometheus) и отдельный endpoint
  • интеграционные тесты с тестовой БД
  • ограничения на конкурентность, пулы, ретраи для внешних зависимостей
  • Итоги

  • Вы собрали HTTP/REST сервис на net/http и договорились о JSON форматах
  • Прокинули context.Context от HTTP слоя до БД и добавили таймауты
  • Реализовали репозиторий на database/sql и научились правильно маппить sql.ErrNoRows в доменную ErrNotFound
  • Сделали конфигурацию через переменные окружения с дефолтами и валидацией
  • Подготовили деплой через multi-stage Docker сборку и docker compose для запуска вместе с PostgreSQL