Изучение Go (Golang) с нуля до практики

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

1. Введение в Go: установка, инструменты, структура проекта

Введение в Go: установка, инструменты, структура проекта

Go (или Golang) — компилируемый язык программирования, который хорошо подходит для создания сетевых сервисов, утилит, бэкенда и инструментов автоматизации. В этом курсе мы будем идти от базового синтаксиса к практике: научимся писать читаемый код, собирать проекты, подключать зависимости и тестировать.

В этой статье вы:

  • установите Go и проверите, что всё работает
  • разберётесь с ключевой командой go
  • поймёте, что такое модуль и как устроены зависимости
  • увидите рекомендуемую структуру проекта на Go
  • Установка Go

    Скачивание и установка

    Устанавливайте Go только из официального источника:

  • Установка Go (официальная документация)
  • После установки у вас должна появиться команда go в терминале.

    Проверка установки

  • Откройте терминал.
  • Выполните команду:
  • Вы увидите версию, например go version go1.22.x .... Это значит, что Go установлен.

    Переменные окружения, которые важно понимать

    Go старается работать из коробки, но несколько понятий полезно знать заранее.

  • GOROOT — место, где установлен Go (обычно настраивается установщиком автоматически).
  • GOPATH — рабочая директория старого подхода (до модулей). Сейчас обычно не нужна для повседневной работы, но может встречаться.
  • GOMOD — путь к файлу go.mod текущего модуля (появляется, когда вы находитесь внутри проекта с модулями).
  • Посмотреть настройки Go можно так:

    Первый запуск: минимальная программа

    Создадим папку проекта, инициализируем модуль и запустим программу.

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

  • Создайте файл main.go:
  • Запустите:
  • Если вы увидели Hello, Go!, значит окружение готово.

    Полезный следующий шаг из официальной документации:

  • Tutorial: Getting started
  • Основные инструменты Go

    Команда go как центр экосистемы

    В Go большинство ежедневных задач решаются одной утилитой — go.

    Наиболее важные команды:

  • go run — запускает программу без явной сборки бинарника
  • go build — собирает бинарный файл
  • go test — запускает тесты
  • go fmt — форматирует код по стандарту Go
  • go mod — управляет модулями и зависимостями
  • go get — добавляет или обновляет зависимости (в модульном режиме)
  • go list — показывает информацию о пакетах и зависимостях
  • Форматирование: gofmt и go fmt

    В Go принято, что форматирование кода едино для всех проектов.

  • gofmt — форматтер
  • go fmt — удобная команда, которая применяет gofmt к пакетам
  • Обычно вы либо запускаете go fmt ./..., либо настраиваете редактор так, чтобы он форматировал файл при сохранении.

    Зависимости и модульный режим

    С появлением модулей Go перестал требовать строгой привязки проектов к GOPATH. Теперь зависимости описываются в go.mod.

    Что вы увидите в проекте:

  • go.mod — имя модуля, версия Go, список зависимостей
  • go.sum — контрольные суммы, которые фиксируют точные версии зависимостей
  • Официальная документация по модулям:

  • Go Modules Reference
  • Инструменты качества

    Минимальный набор, который стоит знать:

  • go test — тесты
  • go vet — статические проверки частых ошибок
  • Также в Go распространён внешний агрегатор линтеров:

  • golangci-lint
  • Важно: линтеры не заменяют тесты, но помогают поддерживать единый стиль и находить подозрительные места.

    IDE и подсказки: gopls

    Для автодополнения, переходов к определению и рефакторинга в Go используется языковой сервер gopls.

  • Репозиторий инструментов Go (включая gopls): golang.org/x/tools
  • Популярные варианты редакторов:

  • Go extension для Visual Studio Code
  • GoLand (JetBrains)
  • Структура проекта на Go

    В Go нет единственного обязательного шаблона структуры каталогов, но есть сложившиеся практики, которые упрощают поддержку.

    Сначала базовое правило:

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

    Для небольшого проекта достаточно:

  • go.mod
  • main.go или папка с main-пакетом
  • Пример:

    Практичная структура для прикладного проекта

    Часто используют такие папки:

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

    Пример дерева:

    Почему internal важен

    Папка internal — это механизм языка Go. Любой пакет внутри internal/ можно импортировать только из:

  • родительской папки internal и её подпапок
  • Это помогает явно отделять внутреннюю реализацию от публичного API проекта.

    Где хранить main-пакет

    Если у вас один исполняемый файл, допустимы два варианта:

  • main.go в корне (для небольших учебных и простых проектов)
  • cmd/<имя-приложения>/main.go (удобно, когда проект растёт)
  • Для реальной практики чаще выбирают cmd/, потому что:

  • проще добавлять второй бинарник (например, мигратор, воркер)
  • понятнее, где точки входа
  • Как Go находит пакеты и зависимости

    В модульном режиме Go использует:

  • имя модуля из go.mod как базовый префикс импортов внутри вашего проекта
  • версии зависимостей, зафиксированные в go.mod и go.sum
  • Пример:

  • модуль называется example.com/hello-go
  • пакет лежит в internal/app
  • импорт из другого пакета внутри модуля будет выглядеть так: import "example.com/hello-go/internal/app"
  • Справочник по пакетам и документации:

  • pkg.go.dev
  • Типичный рабочий цикл

  • Создать модуль: go mod init ...
  • Писать код и поддерживать форматирование: go fmt ./...
  • Подтягивать зависимости: go get ...
  • Проверять качество: go vet ./...
  • Запускать тесты: go test ./...
  • Собирать: go build ./...
  • !Базовый цикл разработки на Go от создания модуля до сборки

    Итоги

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

    2. Основы синтаксиса: типы, функции, управляющие конструкции

    Основы синтаксиса: типы, функции, управляющие конструкции

    После установки Go и знакомства с go mod, go run, go fmt и базовой структурой проекта, пора перейти к тому, на чём строится любой Go-код: переменные и типы, функции и управление потоком выполнения.

    В этой статье вы научитесь:

  • объявлять переменные и константы
  • понимать базовые типы Go и нулевые значения
  • писать функции (в том числе с несколькими возвращаемыми значениями)
  • использовать if, for, switch и связанные операторы
  • Полезные официальные источники по теме:

  • A Tour of Go
  • The Go Programming Language Specification
  • Структура Go-файла: package, import, main

    Минимальная программа обычно выглядит так:

  • package main означает, что пакет предназначен для сборки исполняемого файла.
  • import подключает пакеты стандартной библиотеки или зависимости.
  • func main() — точка входа: выполнение начинается отсюда.
  • Переменные и константы

    Объявление переменных: var

    Можно опускать тип, если есть значение справа:

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

    Внутри функций чаще всего используют короткую форму:

    Важно:

  • := одновременно объявляет новую переменную и присваивает ей значение.
  • := работает только внутри функций.
  • Константы: const

    Константы вычисляются на этапе компиляции и не могут изменяться.

    Нулевые значения

    Если переменная объявлена, но ей не присвоено значение, Go установит нулевое значение для её типа.

    | Тип | Нулевое значение | | --- | --- | | int, int64, float64 | 0 | | bool | false | | string | "" (пустая строка) | | pointer, map, slice, func, chan, interface | nil |

    Пример:

    Базовые типы Go

    Часто используемые типы

    | Тип | Для чего обычно используют | | --- | --- | | bool | логика (true/false) | | int | целые числа (размер зависит от платформы) | | int64 | целые числа фиксированного размера | | float64 | числа с плавающей точкой | | string | строки в UTF-8 | | byte | байт (это псевдоним uint8) | | rune | символ Unicode (это псевдоним int32) | | error | интерфейс для ошибок (часто вторым результатом функции) |

    Приведение типов (явное)

    В Go нет неявного приведения между многими числовыми типами. Нужно приводить явно:

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

    Строки, byte, rune и range

    string в Go хранит байты UTF-8. Важный практический момент: индексирование строки даёт байт, а не «символ».

    Чтобы корректно проходить по символам Unicode, используйте for range, который даёт руны:

    Здесь:

  • i — индекс байта в строке
  • rrune (кодовая точка Unicode)
  • Функции

    Объявление функции

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

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

    Это одна из ключевых особенностей Go. Часто функции возвращают результат и ошибку:

    Типичный шаблон обработки:

    Именованные результаты

    Можно дать имена возвращаемым значениям:

    Используйте умеренно: в простых функциях это читаемо, но в сложных может запутывать.

    Вариативные функции

    Функция может принимать переменное число аргументов:

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

    Функции — значения, их можно присваивать переменным:

    Если анонимная функция использует переменную снаружи, это называется замыкание:

    defer

    defer откладывает выполнение вызова до момента, когда текущая функция завершится (даже если завершится из-за ошибки, которую вы обработали через return).

    Частый пример — закрытие файла:

    Управляющие конструкции

    !Схема показывает, как в Go организуется ветвление и циклы

    if

    if может содержать короткое выражение перед условием (часто используется для получения значения и проверки ошибки):

    Переменная n будет видна только внутри if и else.

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

    В Go нет отдельного while: его роль выполняет for.

    Классический цикл:

    Цикл как while:

    Бесконечный цикл:

    Перебор коллекции через range:

    Если индекс или значение не нужно, используйте _:

    switch

    switch удобен, когда есть несколько веток. В Go не нужно писать break в конце каждого case: переход не «проваливается» по умолчанию.

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

    Если вам нужно намеренно «провалиться» в следующий case, используйте fallthrough, но делайте это осторожно — код становится менее очевидным.

    break, continue, метки

  • break выходит из цикла или switch
  • continue переходит к следующей итерации цикла
  • Для выхода из вложенных циклов можно использовать метку:

    Область видимости и экспорт имён

    В Go видимость идентификаторов (имён переменных, функций, типов) между пакетами определяется первой буквой:

  • Name — экспортируется (доступно из других пакетов)
  • name — не экспортируется (доступно только внутри пакета)
  • Пример:

    Это напрямую связано со структурой проекта из прошлой статьи: код в internal/ дополнительно защищён правилами импорта, а экспорт/неэкспорт регулирует API внутри доступного набора пакетов.

    Мини-пример: маленькая программа с функцией и управлением

    Файл main.go:

    Что здесь важно:

  • sumArgs возвращает (int, error) и прекращает работу при первой ошибке
  • for range проходит по аргументам
  • if использован для проверки условий и ошибок
  • switch выбран для ветвления по результату
  • Запуск:

    Итоги

    Теперь вы знаете основу синтаксиса Go: как объявлять переменные и константы, какие типы используются чаще всего, как писать функции (включая возврат нескольких значений и error), и как управлять выполнением через if, for, switch.

    В следующих материалах эти конструкции будут закрепляться на практике, а также появятся структуры данных (срезы, карты, структуры) и работа с пакетами на более прикладном уровне.

    3. Структуры данных и работа с ошибками: struct, interface, error

    Структуры данных и работа с ошибками: struct, interface, error

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

  • структуры (struct) для моделирования данных
  • методы (функции, привязанные к типу)
  • интерфейсы (interface) для описания поведения
  • ошибки (error) как стандартный способ сообщать о проблемах
  • Официальные источники по теме:

  • Effective Go
  • Tour of Go: Methods and interfaces
  • Документация пакета errors
  • Документация пакета fmt
  • Статья Errors are values
  • Структуры

    Структура — это пользовательский тип, который объединяет несколько значений (полей) под одним именем. Обычно struct используют для сущностей предметной области: пользователь, заказ, конфигурация приложения.

    Объявление структуры и доступ к полям

    Что важно:

  • Поля имеют имена и типы.
  • Имена полей с заглавной буквы экспортируются (доступны из других пакетов), со строчной — нет.
  • Литералы struct и нулевые значения

    Создание структуры чаще всего делают через литерал:

    Нулевое значение структуры — это структура, где каждое поле имеет своё нулевое значение (например, 0, "", false, nil). Это свойство позволяет писать код без обязательных конструкторов.

    Указатели на struct и изменение полей

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

    Синтаксис u.Name работает и для указателя, и для значения: Go автоматически разыменовывает указатель там, где это безопасно.

    Вложенные структуры и встраивание

    Можно использовать структуру как поле другой структуры:

    Ещё есть встраивание (embedding): вы указываете тип без имени поля. Тогда поля и методы встроенного типа “поднимаются” наружу.

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

    Методы

    Метод — это функция, привязанная к типу (например, к User). Методы помогают группировать логику рядом с данными.

    Метод со значением-получателем и указателем-получателем

    Получатель (receiver) — это параметр слева от имени метода.

    Практическое правило:

  • Используйте получатель-значение, если метод не меняет объект и тип небольшой.
  • Используйте получатель-указатель, если метод меняет поля, или структура большая (чтобы не копировать).
  • Интерфейсы

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

    Как тип “реализует” интерфейс

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

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

    Зачем интерфейсы в реальном коде

    Интерфейсы помогают:

  • писать код, который зависит от поведения, а не от конкретной реализации
  • удобно тестировать (подменять реальную реализацию на фейковую)
  • уменьшать связность между пакетами
  • Пример: вы хотите логировать, но не хотите привязываться к конкретному логгеру.

    Любой тип с методом Printf(string, ...any) подходит.

    Пустой интерфейс и any

    Интерфейс без методов записывается как interface{}. Начиная с Go 1.18 есть алиас any.

  • any означает “значение любого типа”
  • используйте его аккуратно: чем больше any, тем меньше проверок компилятора
  • Утверждение типа и type switch

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

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

    Переключатель типов (type switch):

    Важная ловушка: nil в интерфейсе

    Интерфейсное значение внутри хранит две части:

  • конкретный тип
  • конкретное значение
  • Если у вас внутри интерфейса лежит “тип T и значение nil”, то сам интерфейс не равен nil.

    Практический вывод: если функция возвращает error, то при “ошибки нет” возвращайте именно nil, а не (*MyErr)(nil).

    Ошибки

    В Go ошибки — это обычные значения, которые возвращаются из функций. Стандартный контракт: функция возвращает результат и error.

    Тип error

    Встроенный интерфейс error выглядит так:

    То есть “ошибка” — это любой тип с методом Error() string.

    Создание ошибок

    Чаще всего используют:

  • errors.New("message")
  • fmt.Errorf("...", ...)
  • ErrNotFound здесь — сентинел-ошибка (заранее объявленная ошибка, которую можно сравнивать и проверять).

    Обработка ошибок: ранний возврат

    В Go принято обрабатывать ошибку сразу и делать ранний return.

    Так основной “счастливый путь” остаётся без лишних вложенностей.

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

    Чтобы добавить контекст и не потерять исходную причину ошибки, применяют оборачивание (wrapping) через %w в fmt.Errorf.

    Зачем это нужно:

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

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

    Пакет errors даёт два важных инструмента:

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

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

    Тогда можно извлечь такую ошибку через errors.As:

    Практический совет:

  • если вам достаточно “класса” ошибки, часто хватает сентинел-ошибки и errors.Is
  • если нужны данные, используйте свой тип ошибки и errors.As
  • Объединение нескольких ошибок: errors.Join

    Если нужно вернуть сразу несколько причин (например, валидация нескольких полей), в Go есть errors.Join.

    Если ошибок нет, errors.Join вернёт nil (это удобно).

    Документация: errors.Join

    panic и recover

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

    recover позволяет “поймать” панику, но обычно его применяют только на границе программы (например, в верхнем уровне HTTP-сервера или воркера), чтобы не уронить весь процесс.

    Практическое правило для начинающих:

  • для ожидаемых ошибок используйте error
  • panic оставьте для действительно невозможных ситуаций или прототипов
  • Мини-пример: struct + interface + error в одном месте

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

    Что здесь закрепляется:

  • Config как структура данных
  • Logger как интерфейс поведения
  • LoadConfig возвращает (Config, error) и использует Config{} как “нулевой” результат при ошибке
  • ошибки оборачиваются через %w, а “класс” ошибки проверяется через errors.Is
  • Итоги

    Теперь у вас есть базовый набор инструментов, на котором строится большая часть Go-проектов:

  • struct для данных и композиции
  • методы для логики рядом с типом
  • interface для зависимостей по поведению и тестируемости
  • error для корректной обработки проблем без исключений
  • Дальше эти знания будут постоянно использоваться в практике: при разбиении кода на пакеты, при проектировании API и при работе с вводом-выводом и сетью.

    4. Конкурентность в Go: goroutine, channel, контекст и синхронизация

    Конкурентность в Go: goroutine, channel, контекст и синхронизация

    В прошлых материалах вы познакомились со структурами, интерфейсами и ошибками: это основа прикладного Go-кода. Теперь добавим ещё один «столп» Go — конкурентность: умение выполнять несколько задач одновременно, безопасно разделять ресурсы и корректно отменять работу.

    В этой статье вы разберёте:

  • что такое goroutine и как они запускаются
  • как обмениваться данными через channel
  • как управлять временем жизни операций через context
  • как синхронизировать доступ к общим данным с помощью sync
  • Полезные официальные источники:

  • A Tour of Go: Concurrency
  • Go Blog: Concurrency is not parallelism
  • Документация пакета context
  • Документация пакета sync
  • Effective Go
  • !Диаграмма: goroutine выполняются поверх потоков ОС и обмениваются данными через каналы

    Конкурентность и параллелизм

    Конкурентность — это про структуру программы: несколько независимых задач «продвигаются» вперёд, переключаясь (например, пока одна ждёт сеть, другая работает).

    Параллелизм — это про физическое одновременное выполнение на нескольких ядрах.

    Go даёт удобную модель конкурентности (goroutine + channel), а параллельность зависит от среды и настроек рантайма.

    Goroutine

    Goroutine — это лёгкая единица выполнения в Go. Запустить функцию в новой goroutine можно ключевым словом go.

    Запуск goroutine

    Что важно понимать:

  • go f() запускает f() конкурентно и сразу возвращает управление.
  • Если main завершится, процесс завершится вместе со всеми goroutine.
  • Порядок выполнения не гарантирован.
  • Типичная ошибка новичков: «программа ничего не вывела»

    Если main завершился раньше, чем успела выполниться фоновая goroutine, вы можете не увидеть результат. Правильное решение — синхронизировать завершение, а не добавлять Sleep «наугад». Для этого скоро используем sync.WaitGroup.

    Channel: обмен данными без общих переменных

    Channel (канал) — типизированный механизм передачи значений между goroutine.

    Создание канала и отправка/получение

    Если канал небуферизированный, то:

  • отправка ch <- v блокируется, пока кто-то не начнёт получать
  • получение <-ch блокируется, пока кто-то не отправит
  • Это делает небуферизированный канал удобным для синхронизации: «встретились и обменялись».

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

    Канал может иметь буфер:

    Правило:

  • буфер помогает сгладить разницу скоростей между отправителем и получателем
  • но буфер не отменяет необходимости проектировать завершение и отмену работы
  • Закрытие канала

    Закрытие означает: «новых значений больше не будет».

    Что происходит после close:

  • отправка в закрытый канал вызывает panic
  • получение из закрытого канала возвращает нулевое значение и ok=false
  • Важно:

  • канал обычно закрывает тот, кто отправляет, а не тот, кто читает
  • закрывают канал не «чтобы освободить память», а чтобы корректно завершить потребителей
  • range по каналу

    Если канал закрыт, цикл range завершается сам:

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

    select позволяет ждать сразу несколько операций с каналами.

    Таймаут через time.After

    time.After(d) возвращает канал, который «сработает» через d.

    Неблокирующая попытка: default

    default выполнится сразу, если ни одна ветка не готова.

    !Диаграмма: select выбирает первый готовый канал (или ждёт)

    Контекст: отмена, дедлайны и «время жизни» операции

    context — стандартный способ передавать:

  • сигнал отмены
  • дедлайн/таймаут
  • значения, связанные с запросом (использовать умеренно)
  • Чаще всего контекст нужен в коде, который делает I/O: HTTP-запросы, работу с БД, чтение из сети.

    Базовый паттерн: WithCancel

    Проверка отмены обычно выглядит так:

    ctx.Done() — канал, который закрывается при отмене.

    Таймаут и дедлайн

    При таймауте ctx.Err() будет context.DeadlineExceeded, при ручной отмене — context.Canceled.

    Ключевое правило: context передаётся первым параметром

    Типичный стиль функций в Go:

    И ещё правило:

  • не храните context внутри struct «навсегда»
  • контекст отражает жизнь запроса/операции, а не жизнь приложения
  • Синхронизация: когда без общих данных не обойтись

    Каналы хорошо подходят для обмена сообщениями. Но иногда проще и эффективнее защитить общий ресурс.

    WaitGroup: дождаться завершения goroutine

    sync.WaitGroup позволяет корректно дождаться, пока группа goroutine закончит работу.

    Обратите внимание на строку i := i:

  • это защита от распространённой ошибки замыкания, когда все goroutine видят одно и то же изменившееся i
  • Mutex и RWMutex: защита общего ресурса

    Если несколько goroutine читают и пишут в одну map или структуру, нужен sync.Mutex.

    sync.RWMutex полезен, когда чтений значительно больше, чем записей:

  • RLock/RUnlock для чтений
  • Lock/Unlock для записей
  • Правила безопасности:

  • держите блокировку как можно меньше по времени
  • не делайте внутри Lock долгие операции (сеть, диск)
  • избегайте ситуации «заблокировал A и ждёшь B, а другой поток заблокировал B и ждёт A» (взаимная блокировка)
  • sync.Once: выполнить инициализацию один раз

    atomic: очень точечные случаи

    Пакет sync/atomic позволяет делать атомарные операции без Mutex, но для новичков практичнее:

  • сначала освоить каналы и Mutex
  • использовать atomic только если понимаете модель памяти и у вас есть причина
  • Практический пример: worker pool с context, каналами и ошибками

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

    Пример ниже показывает сразу несколько идей:

  • context.WithCancel для отмены
  • WaitGroup для ожидания завершения воркеров
  • jobs и results как каналы
  • возврат ошибок как значений (error) и оборачивание через %w
  • Что здесь закрепляется из прошлых тем курса:

  • ошибки возвращаются как error, а не через panic
  • причина ошибки сохраняется через %w, а класс ошибки проверяется errors.Is
  • конкурентность не отменяет дисциплину ошибок: она делает её ещё важнее
  • Как выбирать инструмент: channel или mutex

    Оба подхода нормальны. Практическая шпаргалка:

  • используйте channel, когда у вас поток событий или задача «передай работу/результат»
  • используйте mutex, когда у вас общий ресурс и нужно защитить инварианты структуры данных
  • И главное:

  • сначала сделайте код правильным и понятным
  • только потом думайте о микрооптимизациях
  • Итоги

    Теперь у вас есть базовый набор для конкурентного Go-кода:

  • go для запуска goroutine
  • chan и select для коммуникации и координации
  • context для отмены и таймаутов
  • sync.WaitGroup для ожидания завершения
  • sync.Mutex/RWMutex для защиты общих данных
  • Дальше в курсе эти инструменты будут встречаться постоянно: в сетевых сервисах, воркерах, пайплайнах обработки данных и при тестировании конкурентного кода.

    5. Практика разработки: HTTP, работа с JSON, тестирование и сборка

    Практика разработки: HTTP, работа с JSON, тестирование и сборка

    В предыдущих статьях вы освоили синтаксис Go, структуры, интерфейсы, работу с ошибками и основы конкурентности (goroutine, channel, context, sync). Теперь соберём это в практический набор навыков, который нужен почти в любом бэкенд-проекте:

  • HTTP-сервер на стандартной библиотеке net/http
  • обмен данными через JSON (encoding/json)
  • тестирование (пакет testing, httptest)
  • сборка и запуск проекта через go test и go build
  • Полезные источники:

  • Документация net/http
  • Документация encoding/json
  • Документация net/http/httptest
  • Документация testing
  • Команда go
  • !Поток обработки HTTP-запроса в net/http

    Минимальный HTTP-сервер на net/http

    HTTP-сервер в Go обычно строится вокруг трёх понятий:

  • endpoint: конкретный URL-путь, например /health
  • handler: функция или объект, который обрабатывает запросы
  • router (mux): компонент, который выбирает handler по пути
  • Минимальный сервер:

    Что важно:

  • http.NewServeMux() создаёт простой роутер.
  • HandleFunc регистрирует функцию-обработчик.
  • http.Server позволяет настраивать сервер (таймауты, обработчик).
  • Корректная работа с методами и статус-кодами

    HTTP-метод (GET, POST, PUT, DELETE) — часть протокола, которая обозначает действие.

    Хорошая практика: проверять метод и возвращать 405 Method Not Allowed, если метод не подходит.

    Статус-коды, которые вы будете использовать чаще всего:

  • 200 OK: успешный запрос
  • 201 Created: ресурс создан
  • 204 No Content: успешно, но тело ответа пустое
  • 400 Bad Request: клиент прислал некорректные данные
  • 404 Not Found: ресурс не найден
  • 405 Method Not Allowed: метод не поддерживается для этого пути
  • 500 Internal Server Error: ошибка на стороне сервера
  • JSON в Go: кодирование и декодирование

    JSON обычно используют для обмена данными между клиентом и сервером.

    В Go за JSON отвечает пакет encoding/json.

    Кодирование (ответ сервера)

    Что здесь происходит:

  • Заголовок Content-Type помогает клиенту понять формат ответа.
  • json.NewEncoder(w).Encode(v) сериализует значение в JSON и пишет в ResponseWriter.
  • Декодирование (тело запроса)

    Пример чтения JSON из r.Body:

    Пояснения:

  • r.Body — поток данных (его читают один раз).
  • DisallowUnknownFields() полезен, чтобы не принимать лишние поля (это снижает шанс “тихих” ошибок клиента).
  • Практический пример: небольшой JSON API (TODO)

    Сделаем маленький сервис “задач” с двумя endpoint:

  • GET /todos — вернуть список
  • POST /todos — создать задачу
  • Данные будем хранить в памяти. Это учебно, но отлично показывает полный цикл HTTP + JSON + ошибки + синхронизация.

    Структуры и хранилище

    Почему здесь нужен sync.Mutex:

  • HTTP-сервер обрабатывает запросы конкурентно.
  • Если два запроса одновременно меняют общий срез todos, без защиты легко получить гонки данных.
  • HTTP-слой: handler’ы и маршрутизация

    Обратите внимание на стиль обработки ошибок:

  • Ошибка JSON — это 400, потому что запрос некорректный.
  • Валидация “пустой текст” — тоже 400.
  • Неподдерживаемый метод — 405.
  • Точка входа: cmd

    Структура проекта, согласованная с идеями из первой статьи (про cmd/ и internal/):

    cmd/todoapi/main.go:

    Здесь появляется важная настройка ReadHeaderTimeout: она ограничивает время чтения заголовков запроса и помогает защититься от очень медленных клиентов.

    Middleware: обёртка вокруг handler

    Middleware — это функция, которая оборачивает http.Handler и добавляет общее поведение: логирование, проверку авторизации, заголовки.

    Пример простого логирования:

    Подключение:

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

    В Go принято тестировать:

  • чистую логику (функции и методы)
  • HTTP-обработчики через net/http/httptest
  • Тест handler через httptest

    Идея: мы создаём “фейковый” HTTP-запрос, отправляем его в handler и проверяем статус-код и тело.

    Пример теста для GET /todos:

    Что здесь используется:

  • httptest.NewRequest создаёт запрос.
  • httptest.NewRecorder записывает ответ так, чтобы его можно было проверить.
  • json.Unmarshal проверяет, что ответ — валидный JSON.
  • Table-driven tests (табличные тесты)

    Когда нужно проверить много сценариев, удобно хранить кейсы в срезе структур.

    Пример идеи (коротко):

    Команды go для тестирования

    Полезные команды:

  • go test ./... запускает тесты во всех пакетах модуля
  • go test -race ./... включает детектор гонок данных (особенно полезно для HTTP-кода с sync.Mutex)
  • go test -run TestName ./... запускает тесты по шаблону имени
  • Сборка и запуск

    Быстрый запуск для разработки

  • go run ./cmd/todoapi компилирует и запускает программу из точки входа
  • Сборка бинарника

  • go build ./cmd/todoapi соберёт бинарник в текущую директорию (имя обычно совпадает с папкой)
  • go build -o todoapi ./cmd/todoapi соберёт бинарник с заданным именем
  • Кросс-компиляция

    Go умеет собирать бинарники под другую ОС и архитектуру через переменные окружения:

    Пояснение:

  • GOOS — целевая операционная система (например, linux, windows, darwin)
  • GOARCH — архитектура (например, amd64, arm64)
  • Минимальный релизный цикл

    Обычно перед сборкой делают проверки:

  • go fmt ./... форматирование
  • go test ./... тесты
  • go vet ./... базовые статические проверки
  • go build ./... сборка
  • !Типичный пайплайн проверки и сборки

    Итоги

    Теперь у вас есть практический “скелет” Go-бэкенда на стандартной библиотеке:

  • HTTP-сервер и роутинг через net/http
  • JSON-ввод/вывод через encoding/json
  • обработка ошибок через статус-коды и понятные сообщения
  • защита общего состояния через sync.Mutex
  • тестирование endpoint’ов через httptest
  • уверенный запуск и сборка через go test и go build
  • Следующий шаг практики обычно такой: добавить хранение в БД, конфигурацию (env/flags), логирование и более строгую обработку ошибок, но фундамент у вас уже есть.