Курс

Backend Developer Go

Глубокое понимание языка Go и его среды выполнения для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение рантайма: модель типов, слайсы и мапы, интерфейсы и ошибки как значения, конкурентность (горутины, каналы, планировщик, модель памяти), сборку мусора и escape-анализ, а также прикладной слой — HTTP-сервисы, доступ к данным, тестирование и продакшн-практики. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.

27модулей66уроков

Программа

Что внутри курса

  1. 01

    Инструменты и модули Go

    3 урока

    Единый инструментарий Go: сборка, запуск, форматирование и проверка кода, управление зависимостями через модули и осмысленная структура проекта. База, с которой начинается любой сервис на Go.

    1. Команды go: build, run, test, vet, fmt и модель сборкиПрограмма выполнилась, вывела результат — и на диске ничего нового не появилось. Это сбивает с толку: если код скомпилировался и запустился, где исполняемый файл? Ответа два, и оба важны для модели инструмента. go run действительно компилирует программу — но в бинарник во временном каталоге, запускает его и удаляет. Артефакта не остаётся намеренно: go run рассчитан на быстрый цикл правки и запуска во время разработки, а не на выпуск программы.
    2. Модули: go.mod, go.sum, версии и выбор минимальной версииРазработчик, пришедший из npm, pip или Maven, сразу пытается уложить их в знакомую пару «манифест + lock»: go.mod — это package.json, а go.sum — это package-lock.json. Аналогия кажется очевидной и почти сразу ломается. go.sum не фиксирует, какие версии использовать; он не является lock-файлом. А go.mod не перечисляет точные версии — он перечисляет минимально требуемые. Точный набор версий для сборки Go вычисляет заново при каждой команде. Чтобы это не выглядело магией, нужно разобрать три вещи по отдельности: что описывает go.mod, как из него выводится набор версий, и что на самом деле гарантирует go.sum.
    3. Пакеты, internal и видимость, структура проектаРазница — одна буква: M против m. В Go нет ключевых слов public и private, но инкапсуляция есть, и она полностью держится на регистре первой буквы имени. Marshal с заглавной — экспортированное имя, доступное снаружи пакета. marshal со строчной было бы внутренним и снаружи не существовало бы. Чтобы это не выглядело причудой синтаксиса, надо понять, что именно в Go является границей инкапсуляции — а это пакет.
  2. 02

    Язык Go

    5 уроков

    Семантика языка Go: базовые типы и нулевые значения, внутреннее устройство слайсов и мап, строки и руны в UTF-8, функции, замыкания и defer. Основа для чтения production-кода и понимания стоимости абстракций.

    1. Базовые типы, нулевые значения и приведенияРазработчик из C ждёт здесь мусора из памяти, а из JavaScript — undefined. В Go не будет ни того, ни другого: n равно 0, s — пустая строка, ok — false. Это не случайность и не «повезло с памятью» — это гарантия языка. У каждого типа есть определённое нулевое значение, и переменная без явной инициализации получает именно его. Понять эту гарантию важно, потому что на ней построена значительная часть идиом Go.
    2. Слайсы: заголовок, len/cap, append и разделяемый массивМы записали в b, а поменялся a. Если считать слайс самостоятельным списком, это необъяснимо. Но слайс — не список. Это маленький заголовок, описывающий окно в массив, а a и b смотрят в один и тот же массив. Пока это не встроено в модель, поведение append и срезов выглядит набором исключений; с моделью — всё выводится из одного устройства.
    3. Мапы: устройство, итерация без порядка и nil-мапаПорядок строк будет меняться от запуска к запуску. Это не баг и не «неустойчивость хеширования, которую надо обойти» — Go намеренно рандомизирует порядок обхода мапы. Разработчик, привыкший к словарю со стабильным или отсортированным порядком, первым делом пытается этот порядок вернуть. Но прежде чем бороться с поведением, стоит понять, откуда оно и что мапа такое на самом деле — тогда и случайный порядок, и паника при записи в пустую мапу окажутся следствиями одной модели.
    4. Строки, руны и байты: UTF-8 и преобразованияДля английского слова len совпадает с числом букв, а для русского даёт вдвое больше. Разработчик, привыкший к строке как к массиву символов, тут же заподозрит ошибку кодировки. Ошибки нет: len считает байты, а не символы, и русская буква в UTF-8 занимает два байта. Чтобы работать с текстом в Go осознанно — резать, считать, перебирать, — нужно понять, что строка хранит именно байты, а «символ» — это отдельное понятие поверх них.
    5. Функции, замыкания, множественный возврат и deferdefer выполняется в конце функции, когда x уже равен 2 — но печатает 1. Причина не в порядке выполнения, а в моменте, когда вычисляются аргументы отложенного вызова. Чтобы defer, замыкания и множественный возврат перестали давать сюрпризы, нужно разобрать функции Go как значения и точно описать, что и когда происходит при defer.
  3. 03

    Структуры, методы и встраивание

    3 урока

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

    1. Структуры, поля, сравнимость и пустая структураРазработчик из Java или JavaScript ждёт, что b := a сделает b вторым именем того же объекта, и b.X = 99 изменит и a. В Go этого не происходит: b — независимая копия. Структура в Go — это значение, а не ссылка, и понимание этого определяет всё остальное: как передаются структуры в функции, когда нужны указатели и почему одни структуры можно сравнивать через ==, а другие нет.
    2. Методы, получатели по значению и указателю, метод-сетыМетод Inc вроде бы увеличивает n, но после двух вызовов c.n остался нулём. Для пришедшего из Java или Python это выглядит сломанным: метод же меняет поле. На самом деле он меняет поле копии. Всё дело в том, как объявлен получатель метода — и это одно из самых частых мест, где Go-код ведёт себя не так, как ждут. Разберём, что такое метод, чем отличаются два вида получателя и как это связано с интерфейсами.
    3. Встраивание, продвижение методов и композицияУ Server не объявлено ни одного метода, но s.Log(...) компилируется и работает. Разработчик из Java немедленно прочитает это как наследование: Server extends Logger. Аналогия соблазнительна и ведёт к ошибкам, потому что встраивание в Go — это композиция, а не наследование. Разберём, что здесь на самом деле происходит и чем это принципиально отличается от иерархий классов.
  4. 04

    Интерфейсы

    3 урока

    Неявная реализация, маленькие интерфейсы, объявление интерфейса на стороне потребителя и подводные камни вроде типизированного nil. Основной механизм полиморфизма и развязки в Go.

    1. Неявная реализация, маленькие интерфейсы и интерфейс у потребителяPoint можно присвоить переменной типа Stringer, хотя нигде — ни у Point, ни у Stringer — не написано, что они связаны. Разработчик из Java ищет class Point implements Stringer и не находит. Этого и не будет: в Go соответствие интерфейсу неявное. Это не мелкая синтаксическая разница — она переворачивает то, как проектируют пакеты, зависимости и тесты. Разберём механизм и его следствия.
    2. Устройство интерфейса и ловушка типизированного nildoWork вернула nil-указатель, а err != nil оказалось истинным — код сообщает об ошибке, которой нет. Это не баг компилятора и не мистика: это прямое следствие того, как устроено значение интерфейса. Пока модель интерфейса как «просто ссылки» не заменена на верную, этот случай необъясним.
    3. Type assertion, type switch и anyПервая строка достаёт из интерфейса строку и работает. Вторая — паникует и роняет программу. Разработчик из Java, привыкший к кастам и исключению ClassCastException, ждёт чего-то похожего, но не аварии всей программы. В Go у извлечения типа из интерфейса есть две формы с разным поведением, и выбрать правильную — значит не уронить сервис на неожиданном типе. Разберём обе, плюс type switch и роль any.
  5. 05

    Ошибки, panic и recover

    2 урока

    Ошибка как значение, оборачивание через %w и разбор цепочки через errors.Is/As, sentinel-ошибки, а также роль panic и recover. Обработка ошибок в Go явная и живёт в возвращаемых значениях, а не в исключениях.

    1. Ошибка-значение, %w, errors.Is/As и sentinelРазработчик из Java или Python ищет try/catch и не находит. Вместо этого функция возвращает два значения — результат и ошибку, — а ошибку проверяют тут же, обычным if. Это не «Go ещё не завёз исключения»: это принципиальный выбор. Ошибка в Go — обычное значение, которое возвращают, проверяют и передают дальше как данные. Пока эта модель не встала на место исключений, Go-код кажется многословным ритуалом из if err != nil; с ней — становится явным и предсказуемым путём ошибки.
    2. panic, recover и defer: когда и какpanic останавливает нормальное выполнение, печатает сообщение и трейс стека и завершает программу с ненулевым кодом. Но отложенный defer при этом успел выполниться. Разработчик из Java видит в panic/recover знакомую пару throw/try-catch и тянется использовать её для обычных ошибок. Это и есть главная ошибка. Разберём, что panic делает на самом деле, чем recover отличается от catch и почему для обычных ошибок он не нужен.
  6. 06

    Указатели и значения

    2 урока

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

    1. Указатели и передача по значениюОдин и тот же вызов: изменение слайса «прошло» наружу, а изменение структуры — нет. Разработчик из Java объяснит это привычно: «слайсы передаются по ссылке, структуры — по значению». Это объяснение неверно и разваливается на следующем же примере. В Go правило одно, без исключений: всё передаётся по значению. Понять его — значит перестать заучивать, что «по ссылке», а что «по значению», и выводить поведение из одной модели.
    2. Семантика значений и ссылочных типовb := a скопировала структуру, и правка b.Name не задела a — как и ожидается от значения. Но b.Roles[0] = "guest" изменила a.Roles. Половина полей ведёт себя как независимая копия, половина — как общая. Из прошлого урока мы знаем правило «всё по значению», но здесь нужен практический компас: какие типы при копировании становятся независимыми, а какие продолжают делить данные. Без него копирование структур раскладывает мины.
  7. 07

    Дженерики

    2 урока

    Параметры типов и ограничения, встроенный comparable, обобщённые контейнеры и алгоритмы из пакетов slices и maps. Дают типобезопасность без приведений там, где раньше плодили копипасту или any.

    1. Параметры типов, ограничения и comparableОдна функция работает и для чисел, и для строк, возвращает точный тип и проверяется компилятором. До Go 1.18 такого не было: приходилось либо копировать MaxInt, MaxString, MaxFloat, либо принимать interface{} и терять типобезопасность с приведениями. Дженерики убрали эту дилемму. Но у них своя модель — параметры типов и ограничения, — и её надо понять, чтобы применять к месту, а не превращать весь код в машину параметров типов.
    2. Обобщённые алгоритмы и пакеты slices/mapsslices.Sort — обобщённая функция: она принимает слайс любого упорядочиваемого типа и сортирует его на месте, без реализации интерфейсов. Это прикладная сторона дженериков из прошлого урока: стандартная библиотека Go 1.21 принесла пакеты slices и maps — типобезопасные операции, заменяющие ручные циклы. Разберём, что там есть, как писать своё и где мера.
  8. 08

    Модель конкурентности

    2 урока

    Как из горутин, каналов и синхронизации собирается конкурентная программа и чем конкурентность отличается от параллелизма. Модель памяти Go и правило happens-before — фундамент, на котором держатся остальные темы конкурентности.

    1. Конкурентность против параллелизма и подход Go«Конкурентность» и «параллелизм» в разговоре используют как синонимы, и это первое, что мешает понять конкурентность в Go. Роб Пайк, один из авторов языка, сформулировал разницу так: конкурентность — это про структуру, параллелизм — про исполнение. Конкурентность — способ построить программу как набор независимо выполняемых задач. Параллелизм — их реально одновременное выполнение на нескольких ядрах. Это не одно и то же, и путаница между ними приводит к неверным ожиданиям («добавлю горутин — станет быстрее») и к неправильной модели синхронизации. Разведём понятия и посмотрим, как Go подходит к конкурентности.
    2. Модель памяти Go и happens-beforeКажется очевидным: горутина выставит done = true, цикл в main это заметит и напечатает msg. На практике эта программа может зациклиться навсегда, а если и завершится — может напечатать пустую строку вместо «привет». Здесь нет опечатки: без синхронизации нет гарантии, что запись одной горутины вообще станет видна другой, и в каком порядке. Это управляется моделью памяти Go и понятием happens-before. Пока последовательная интуиция «записал — прочитал» не заменена на неё, конкурентный код будет «иногда работать» — худший вид багов.
  9. 09

    Горутины и планировщик

    3 урока

    Лёгкие горутины и модель планировщика G-P-M: локальные очереди, work stealing, вытеснение и обработка блокирующих вызовов. Объясняет, почему Go держит миллионы горутин на горстке потоков.

    1. Горутины: запуск, стек, жизненный цикл и стоимостьЗапустите сто тысяч потоков операционной системы — и машина почти наверняка встанет: каждый поток резервирует стек порядка мегабайта, сто тысяч потоков — это десятки гигабайт только под стеки, плюс нагрузка на планировщик ядра. А сто тысяч горутин Go спокойно живут на обычном ноутбуке. Разница не в том, что «Go быстрее» — а в том, что горутина устроена принципиально иначе, чем поток ОС. Понять её устройство нужно, чтобы не бояться создавать горутины там, где это уместно, и одновременно не плодить утечки.
    2. Планировщик G-P-M: очереди, work stealing, вытеснениеМы знаем, что горутины дёшевы и их можно завести сотни тысяч, а исполняются они на небольшом числе потоков ОС. Но кто и как решает, какая горутина на каком потоке в какой момент бежит? Запустите конкурентную Go-программу на восьмиядерной машине — и она без всякой настройки загрузит все восемь ядер. За этим стоит планировщик Go и его модель из трёх сущностей — G, P и M. Понимать её нужно, чтобы объяснять и масштабирование на ядра, и то, почему один тяжёлый цикл больше не «вешает» всю программу.
    3. Блокирующие вызовы, netpoller и GOMAXPROCSВ языках с thread-per-request такой код упёрся бы в число потоков: десять тысяч соединений — десять тысяч потоков, заблокированных на Read. Поэтому там пишут async/await и колбэки. А Go-сервер держит десятки тысяч таких горутин с синхронным Read — и не тонет. Секрет в том, что «блокирующий» c.Read на самом деле не занимает поток ОС на всё время ожидания. За этим стоит netpoller, и понять его нужно, чтобы доверять простому синхронному стилю Go под нагрузкой.
  10. 10

    Каналы и select

    2 урока

    Буферизированные и небуферизированные каналы, правила блокировки и закрытия, мультиплексирование через select. Основной способ Go передавать данные и координировать горутины вместо общей памяти.

    1. Каналы: буфер, блокировка, закрытие, направление, nilЭта программа не напечатает «отправлено» — она заблокируется навсегда (а рантайм сообщит fatal error: all goroutines are asleep - deadlock!). Разработчик, думающий о канале как об обычной очереди («положил сообщение и пошёл дальше»), удивится: почему простая отправка повисла? Потому что канал в Go — не просто очередь, а синхронизирующий примитив, и у него строгие правила блокировки, закрытия и работы с nil. Разберём их — иначе каналы будут то «зависать», то паниковать без видимой причины.
    2. select, default, таймауты и мультиплексированиеПриём из ch1 заблокирует горутину, даже если в ch2 уже что-то есть, — мы ждём не тот канал. Последовательный приём не умеет «ждать любой из». Для этого в Go есть select — оператор, который ждёт готовности сразу на нескольких каналах. Это рабочая лошадь конкурентности: на нём строят таймауты, отмену и мультиплексирование. Разберём его правила.
  11. 11

    Примитивы синхронизации

    3 урока

    Mutex и RWMutex, WaitGroup, Once, Pool и атомарные операции, а также когда что выбирать. Нужны, чтобы защитить общее состояние без гонок там, где каналы избыточны.

    1. Mutex и RWMutex: защита общего состоянияТысяча горутин наращивает counter, но итог почти никогда не равен 1000, а go run -race кричит о гонке данных. Причина в том, что counter++ — это не одна операция, а «прочитать, прибавить, записать», и горутины затирают чтения друг друга. Из модуля о конкурентности мы знаем: Go советует «share memory by communicating». Но иногда общее состояние всё же есть, и его надо защитить — для этого служит sync.Mutex. Разберём, как он работает, чем отличается RWMutex и когда мьютекс уместнее канала.
    2. WaitGroup, Once, Pool и CondМьютекс защищает общее состояние — но у конкурентного кода есть и другие потребности: дождаться, пока завершится группа горутин; выполнить инициализацию ровно один раз, даже под гонкой; переиспользовать временные объекты, чтобы не мусорить; подождать выполнения условия. Под каждую в пакете sync есть свой примитив: WaitGroup, Once, Pool и Cond. Их часто применяют не по назначению — используют Cond там, где нужен канал, или Pool как пул соединений. Разберём, что каждый решает и где его границы.
    3. Атомарные операции и sync/atomicНи Lock, ни Unlock — а гонки нет, и -race молчит. Это атомарные операции из пакета sync/atomic. Они выглядят как «мьютекс, только быстрее», и отсюда соблазн заменять ими блокировки везде. Это ошибка: у атомиков узкая область применения, и понять её границу важнее, чем выучить методы.
  12. 12

    Пакет context

    2 урока

    Отмена, дедлайны и request-scoped значения через дерево контекстов. Несущая конструкция таймаутов и graceful-остановки во всех сетевых API Go.

    1. Отмена, дедлайны и таймауты через contextПредставьте HTTP-обработчик, который делает долгий запрос к базе. Клиент закрыл соединение через секунду — но обработчик об этом не знает и продолжает молотить базу ещё минуту, впустую тратя ресурсы. Или наоборот: вы задали операции таймаут в 2 секунды, а она всё равно висит бесконечно. Оба случая — про отмену, и в Go её несёт context.Context. Многие думают, что context «сам останавливает» такую работу. Это не так, и понимание того, как отмена устроена и почему она кооперативная, — ключ к корректным таймаутам и graceful-остановке. Разберём модель.
    2. Request-scoped значения и распространение контекстаВыглядит удобно — можно прокинуть что угодно куда угодно без «загромождения» параметров. Именно эта кажущаяся универсальность и делает WithValue самым злоупотребляемым местом context. Разберём, как он работает, и — главное — где проходит граница между уместным и неуместным применением.
  13. 13

    Паттерны конкурентности и гонки

    4 урока

    Worker pool, fan-in/fan-out, конвейеры, а также гонки данных, дедлоки и утечки горутин. Паттерны дают форму конкурентному коду, а детектор гонок и правильное завершение — его надёжность.

    1. Worker pool и ограничение параллелизмаСами горутины это переживут — их стеки маленькие. Ломается другое: ресурсы, к которым они обращаются, не бесконечны. 100 000 горутин, одновременно открывающих соединение к базе, исчерпают пул соединений; одновременно бьющих во внешний API — упрутся в rate limit или получат отказ; читающих файлы — исчерпают файловые дескрипторы. Дешевизна горутины ≠ дешевизна того, что она делает.
    2. Fan-in/fan-out, конвейеры и errgroupWorker pool из прошлого урока решает одну задачу: разобрать пачку однородных задач с ограничением параллелизма. Но часто обработка — это последовательность этапов: сгенерировать → преобразовать → агрегировать, где каждый следующий этап питается выходом предыдущего. Городить это на разрозненных go и общих переменных быстро превращается в кашу. Go предлагает форму: конвейер (pipeline) из стадий, соединённых каналами.
    3. Гонки данных и детектор -raceНапрашивается вывод: «гонка — это когда из-за одновременности иногда теряется часть инкрементов, и сумма выходит меньше». Это описание симптома, и оно опаснее, чем кажется: оно внушает, что результат хоть и неверный, но осмысленный («потеряли N инкрементов»), и что если сумма вдруг сошлась — гонки нет. Оба вывода ложны. Разберём, что такое гонка на самом деле.
    4. Дедлоки и утечки горутинНовичок обычно знает только первый и уверен, что рантайм поймает любое «зависание». Это опасное упрощение — разберём, где оно ломается.
  14. 14

    Память и сборщик мусора

    3 урока

    Стек и куча, конкурентный трёхцветный сборщик мусора, барьер записи и паузы stop-the-world, настройка через GOGC и GOMEMLIMIT. Понимание рантайма отделяет гарантии языка от поведения под нагрузкой.

    1. Стек, куча и аллокацииКто пришёл из C или C++, вздрагивает: возврат адреса локальной переменной — это висячий указатель, классический undefined behaviour, ведь кадр функции уничтожается при возврате. В Go этот код абсолютно корректен и повсеместен. Значит, интуиция «локальная переменная живёт на стеке и умирает с кадром» здесь работает иначе. Чтобы понять почему, надо развести две области памяти и — главное — понять, кто решает, куда попадёт объект.
    2. Трёхцветный конкурентный GC и паузы stop-the-worldМногие приходят в Go с опаской, вынесенной из ранних сборщиков мусора: GC периодически останавливает весь мир (stop-the-world), обходит кучу, и чем куча больше, тем дольше программа стоит колом — отсюда знаменитые «GC-паузы» на сотни миллисекунд. С такой моделью Go-сервис с большой кучей должен регулярно замирать.
    3. GOGC, GOMEMLIMIT и настройка сборщикаТипичная производственная загадка: Go-сервис в контейнере с лимитом 512 МБ периодически убивается с OOMKilled, хотя по метрикам живого набора данных ему хватает и половины. Память будто бы есть — а процесс валится.
  15. 15

    Escape-анализ

    1 урок

    Как компилятор решает, попадёт значение на стек или в кучу, и как это увидеть через флаги сборки. Меньше побегов в кучу — меньше аллокаций и нагрузки на сборщик мусора.

    1. Escape-анализ: стек или куча и как это увидетьВ a переменная n не покидает кадр — компилятор разместит её на стеке. В b адрес n возвращается наружу, значит n должна пережить возврат функции — компилятор вынужден разместить её в куче. Говорят: n убегает (escapes) в кучу. Escape-анализ — это и есть тот проход компилятора, который для каждого значения решает, убегает оно или нет.
  16. 16

    Профилирование

    3 урока

    Поиск узких мест через pprof и трассировку выполнения, диагностика аллокаций и пауз сборщика мусора, бенчмарки с учётом памяти. Оптимизация начинается с измерения, а не с догадок.

    1. pprof: CPU- и heap-профилиПредыдущие модули не раз заканчивались одним и тем же предостережением: сначала измерять, потом оптимизировать. Пришло время инструмента, который делает измерение возможным. Сервис тормозит или раздувается по памяти — куда смотреть? Интуиция почти всегда врёт: узкое место оказывается не там, где вы «чувствовали», а оптимизация по догадке тратит время и портит код ради выигрыша, которого нет.
    2. Профили goroutine, block и mutexruntime.SetBlockProfileRate(rate) // rate > 0 включает; например, 1 = каждое событие runtime.SetMutexProfileFraction(rate) // rate > 0 включает; сэмплирует долю событий контеншна
    3. go tool trace и бенчмарки с -benchmemgo tool trace trace.out # открыть в браузере func BenchmarkEncode(b testing.B) { data := setupData() // дорогой setup — не должен попасть в измерение b.ResetTimer() // сбрасываем таймер после setup for range b.N { // testing сам подбирает b.N для стабильности sink = encode(data) // результат «потребляем» (см. ниже) } } go test -bench=. -benchmem BenchmarkEncode-8 1234567 950 ns/op 128 B/op 3 allocs/op
  17. 17

    HTTP-сервисы на net/http

    2 урока

    Встроенный HTTP-сервер и клиент Go: обработчики, маршрутизация, таймауты и работа с запросом. Для многих сервисов стандартной библиотеки достаточно без внешнего фреймворка.

    1. HTTP-сервер: Handler, ServeMux и таймаутыРаботает сразу. Но именно эта простота — ловушка: скопированный из туториала, такой сервер несёт две проблемы, невидимые до продакшена (глобальный роутер и отсутствие таймаутов), а под ним скрыта модель, которую стоит понимать целиком. Разберём, что здесь на самом деле происходит.
    2. HTTP-клиент: запросы, тело и соединенияВ туториале — идеально. В продакшене этот код несёт три мины: он может висеть вечно, он течёт соединениями, и он не переиспользует установленные TCP-соединения. Разберём каждую и как их обезвредить.
  18. 18

    Middleware и маршрутизация

    2 урока

    Маршрутизация запросов и цепочки middleware поверх http.Handler: логирование, аутентификация, восстановление после паники. Порядок обёрток определяет, как обрабатывается каждый запрос.

    1. Middleware как обёртка Handler: порядок и композицияВ любом сервисе есть задачи, общие для всех эндпоинтов: залогировать каждый запрос, поймать панику и вернуть 500 вместо обрыва, проверить аутентификацию, поставить таймаут. Копировать этот код в каждый обработчик — путь к дублированию и ошибкам. Во фреймворках для этого есть «middleware», и часто оно выглядит как магия. В Go никакой магии нет: middleware — это обычная функция-декоратор поверх уже знакомого интерфейса http.Handler.
    2. Маршрутизация и паттерны ServeMuxДолгие годы совет был единодушным: стандартный ServeMux слишком примитивен, для любого реального сервиса тяните сторонний роутер — chi, gorilla/mux, httprouter. И это было правдой: до Go 1.22 ServeMux не различал HTTP-методы (GET и POST на один путь — вручную) и не умел переменные в пути (/users/{id}). Работать с REST на нём было больно.
  19. 19

    JSON и сериализация

    2 урока

    Кодирование и декодирование через encoding/json, теги структур, omitempty и собственные маршалеры. Формат обмена большинства HTTP-API, где важно контролировать форму и совместимость данных.

    1. encoding/json: Marshal, Unmarshal и тегиПриватное поле password бесследно исчезло из JSON. Это не баг и не «безопасность по умолчанию» — это фундаментальное правило encoding/json, которое сразу задаёт модель: пакет сериализует не всё подряд, а работает по конкретным правилам, полным неочевидных деталей. Разберём их.
    2. Собственные маршалеры и потоковый Encoder/Decoderjson.Marshal из прошлого урока покрывает большинство случаев, но упирается в две стены. Первая: ему нужно специфичное представление типа — время в нестандартном формате, enum как строка вместо числа, деньги как строка с двумя знаками. Базовая рефлексия таких правил не знает. Вторая: он работает с []byte в памяти — весь JSON целиком буферизуется, что расточительно для HTTP-тел и больших потоков. Два инструмента снимают эти ограничения: интерфейсы Marshaler/Unmarshaler и потоковые Encoder/Decoder.
  20. 20

    HTTP и REST-семантика

    2 урока

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

    1. Методы, коды состояния и идемпотентностьРеальная история, повторяющаяся снова и снова: разработчик сделал ссылку GET /items/42/delete, удаляющую элемент, — так проще, чем возиться с формой. Через день данные начали пропадать сами собой. Причина — браузер (или корпоративный прокси, или антивирус) предзагрузил ссылку, чтобы ускорить навигацию, и тем самым выполнил удаление. Никто на неё не нажимал.
    2. REST-ограничения, кэширование и версионированиеСервис отлично работал на одном инстансе. Пользователи логинились, сессия хранилась в памяти сервера, всё летало. Потом нагрузка выросла, за балансировщик поставили второй инстанс — и пользователей начало случайно «разлогинивать»: запрос попадал на инстанс, где их сессии в памяти нет. Добавление серверов не помогло, а навредило.
  21. 21

    Роутеры и фреймворки

    1 урок

    Лёгкие роутеры и фреймворки (chi, gin, echo) поверх понимания net/http: удобная маршрутизация, группы, middleware. Берут на себя рутину, но не заменяют знание стандартного сервера.

    1. Роутеры и фреймворки поверх net/http: когда и что выбратьРазработчик, пришедший из Python или Java, начинает новый веб-сервис с одного и того же вопроса: «какой фреймворк взять?» Django, Flask, Spring — там без большого фреймворка проекта не начинают, он даёт роутинг, ORM, шаблоны, всё сразу. Тот же рефлекс переносят в Go: «покажи мне Go-аналог Spring, с него и начну».
  22. 22

    Доступ к данным

    3 урока

    Работа с базой данных из Go: database/sql и драйвер pgx, пул соединений, транзакции, миграции и генерация типобезопасного кода через sqlc. Нужно понимать, какой SQL выполняется и как управляется соединение.

    1. database/sql, драйверы, pgx и пул соединенийsql.Open вернул nil-ошибку, хотя базы может не быть вовсе. Это сразу показывает: database/sql устроен не так, как «открыть файл и работать». Здесь три вещи, которые нужно понять с самого начала: что такое database/sql относительно драйвера, почему sql.DB — это пул, а не соединение, и какие дисциплины (закрытие Rows, NULL, параметры) обязательны.
    2. Транзакции и отмена запросов через contextСо счёта списали 100, но не зачислили — деньги исчезли. Каждый Exec на db — это отдельная автозакоммиченная операция; «выполнить подряд» не делает их одним целым. Чтобы два изменения были атомарны (оба или ни одного), нужна явная транзакция. А чтобы запрос можно было прервать по таймауту или отмене — нужен context. Разберём оба.
    3. Миграции схемы и типобезопасные запросы через sqlcПрошлые уроки дали механику: пул, запросы, транзакции. Но в реальном сервисе всплывают две операционные проблемы, которые механика не решает.
  23. 23

    Готовность к продакшну

    1 урок

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

    1. Конфигурация сервиса и принципы twelve-factorНа проде база не на localhost, пароль другой, — значит, нужно пересобрать бинарь под каждое окружение. А при обновлении kill -9 обрывает запросы на середине: пользователь получает ошибку, транзакция повисает. «Скомпилировался и запустился» — это ещё не production-готовность.
  24. 24

    Тестирование

    4 урока

    Пакет testing, table-driven тесты и подтесты, httptest для HTTP-обработчиков, бенчмарки, fuzzing и детектор гонок. Тесты фиксируют поведение и защищают от регрессий при изменениях.

    1. testing: table-driven, подтесты и t.ParallelКопипаста растёт, а вместе с ней — соблазн потянуть assert-фреймворк «как в JUnit», чтобы было «солиднее». Оба инстинкта в Go лишние. Стандартный пакет testing вместе с идиомой table-driven покрывает это чище и без зависимостей. Разберём модель.
    2. httptest и интерфейсные мокиНаивный тест HTTP-обработчика выглядит так: поднять сервер, послать реальный запрос, проверить ответ. А тест сервиса, ходящего в БД или внешний API, — подключиться к настоящей базе. Оба варианта работают, но оба медленные, хрупкие и недетерминированные: заняты порты, база не поднята, внешний API отвалился или изменил данные — тест «мигает» (flaky) не из-за вашего кода.
    3. Бенчмарки, fuzzing и -raceTable-driven тесты из первого урока проверяют случаи, которые вы придумали: положительное, отрицательное, ноль, пустая строка. Но продакшн-баги часто прячутся во входах, о которых никто не подумал: строка из одного эмодзи, число на границе переполнения, полупустой UTF-8, вложенность в 10000 уровней. Ваш список примеров их не покрывает по определению — вы же о них не подумали.
    4. Покрытие и организация тестовКоманда гордится: покрытие тестами 100%. Через неделю — баг на проде, ровно в той функции, что «покрыта». Как так? Тест исполнил каждую строку функции, но не проверил результат — вызвал и выбросил возвращённое значение. Строка засчитана как покрытая, корректность не проверена.
  25. 25

    Наблюдаемость

    2 урока

    Структурное логирование через log/slog, метрики и трассировки по OpenTelemetry, health-проверки. Без них поведение сервиса под нагрузкой остаётся непрозрачным.

    1. Структурное логирование через log/slogЛокально читается прекрасно. Но в проде, где тысячи сервисов пишут миллионы строк в агрегатор, эта строка почти бесполезна для анализа. Хотите ответить на вопрос «сколько неудачных логинов у пользователя 42 за час?» — придётся grep по подстроке и парсить текст регэкспами. А user 42 и userID=42 — уже разные строки, поиск ломается. Сообщение и данные слиплись в один текст, по которому нельзя запрашивать.
    2. Метрики и трассировки по OpenTelemetryПрод-сервис «тормозит». Открываем логи из прошлого урока — там отдельные события: вот ошибка, вот запрос обработан. Но логи не отвечают на главные вопросы: растёт ли задержка последний час или это разовый всплеск? И на каком из пяти сервисов, через которые проходит запрос, он застревает? Логи — это дискретные точки; тренда и пути запроса в них нет.
  26. 26

    Устойчивость и завершение

    2 урока

    Таймауты и отмена через context, повторы и размыкатели цепи, корректное завершение сервиса. Сетевые вызовы падают, и сервис должен переживать частичные отказы без каскада.

    1. Таймауты, отмена и graceful shutdownПервый: обработчик вызывает внешний API, а тот завис. Без таймаута горутина-обработчик ждёт ответа вечно; соединения копятся, пул исчерпывается, и сервис деградирует целиком из-за одной зависшей зависимости. Второй: во время деплоя под убивают (kill), и все запросы, что были в обработке, обрываются с ошибкой у клиентов — платёж наполовину проведён, ответ не отдан.
    2. Повторы с backoff и размыкатель цепиИ это ровно то, что добивает падающий сервис. Он уже перегружен — а сотни клиентов начинают долбить его повторами без пауз, добавляя ещё нагрузки. Сервис не может встать: чем хуже ему, тем больше повторов. Это retry storm — шторм повторов. А если запрос был POST-платежом, наивный повтор ещё и спишет дважды.
  27. 27

    Контейнеры и деплой

    2 урока

    Упаковка сервиса в контейнер: multi-stage сборка, минимальный образ и статический бинарник Go, переменные окружения и кросс-компиляция. Воспроизводимая сборка делает деплой предсказуемым.

    1. Multi-stage Dockerfile и статический бинарникFROM golang:1.24 AS build WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /server .
    2. Кросс-компиляция, build-флаги и embedGOOS=linux GOARCH=amd64 go build -o server .

Готовьтесь по структуре, а не вслепую

Откройте курс в приложении и закрепляйте темы в тренажёре вопросов с AI-разбором.

Backend Developer Go: подготовка к собеседованию — JobJump