Обучение языку программирования Go

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

1. Введение в Go и настройка окружения

Введение в Go и настройка окружения

Что такое Go

Go (или Golang) — компилируемый язык программирования, созданный в Google. Его проектировали так, чтобы он:

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

    Официальный сайт языка и документации: Go.

    Где используется Go

    Go особенно часто встречается в задачах, где важны параллельная обработка, сеть и простота развёртывания:

  • веб-сервисы и API
  • микросервисы
  • инструменты DevOps и CLI-утилиты
  • высоконагруженные системы обработки событий
  • Важно: Go компилируется в один исполняемый файл (для большинства сценариев), поэтому развёртывание обычно проще: вы собираете бинарник и запускаете его на сервере.

    Что входит в окружение Go

    Чтобы начать работать, нужно понимать базовые элементы экосистемы:

  • go — основная команда (инструментальная цепочка), через которую выполняются сборка, запуск, тесты и управление зависимостями
  • стандартная библиотека — большой набор пакетов “из коробки” (HTTP, JSON, тестирование и другое)
  • модули (Go modules) — система управления зависимостями и версионирования
  • Документация по пакетам (включая стандартную библиотеку): Go Packages.

    Установка Go

    Устанавливайте Go только из официальных источников.

    Windows

  • Скачайте установщик со страницы: Install Go
  • Установите Go
  • Откройте PowerShell и проверьте:
  • macOS

  • Скачайте пакет .pkg со страницы: Install Go
  • Установите Go
  • Проверьте в терминале:
  • Linux

  • Скачайте архив для вашей архитектуры со страницы: Install Go
  • Распакуйте и установите согласно инструкции
  • Проверьте:
  • Если команда go не находится, почти всегда причина в том, что путь к Go не добавлен в PATH.

    Проверка настроек через go env

    Go хранит множество настроек окружения. Их удобно смотреть командой:

    Несколько параметров, которые чаще всего полезны новичку:

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

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

  • имя модуля (обычно похоже на путь репозитория)
  • версия Go для проекта
  • зависимости и их версии
  • Главная идея: вы можете хранить проект в любой папке, а зависимости будут управляться через go.mod и go.sum.

    Официальное вводное руководство (очень полезно сохранить в закладки): Getting started.

    Редактор кода

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

    Популярные варианты:

  • Visual Studio Code и расширение для Go: Go extension for VS Code
  • GoLand: GoLand
  • Рекомендуемая минимальная настройка в редакторе:

  • автоформатирование при сохранении (через gofmt)
  • подсветка ошибок и подсказки
  • удобный запуск go test
  • Первый проект на Go

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

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

  • go run нашёл пакет main в текущем модуле
  • скомпилировал код во временный бинарник
  • сразу запустил его
  • Если вы хотите собрать отдельный исполняемый файл:

    В текущей папке появится бинарник (имя зависит от ОС и настроек).

    !Схема показывает, как обычно запускают форматирование, тесты и сборку в Go

    Форматирование кода: gofmt

    В Go принят единый стандарт форматирования, и инструмент gofmt поставляется вместе с Go.

    Форматировать файл можно так:

    Почему это важно:

  • в команде проще читать код в едином стиле
  • меньше бессмысленных споров о форматировании
  • многие инструменты и ревью ожидают стиль gofmt
  • Проверка установки и типичные проблемы

    Команда go не найдена

    Возможные причины:

  • Go не установлен
  • не настроен PATH
  • Решение — переустановить Go по инструкции и убедиться, что go version работает.

    Зависимости не скачиваются

    Проверьте доступ к интернету и повторите:

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

    Что дальше

    Теперь у вас установлены Go и базовые инструменты, создан первый модуль и вы умеете:

  • проверять установку через go version
  • понимать, что такое модуль и файл go.mod
  • запускать код через go run и собирать через go build
  • форматировать код через gofmt
  • В следующей статье логично перейти к основам синтаксиса Go: переменным, типам, функциям и структуре программы.

    2. Базовый синтаксис: типы, переменные, операторы

    Базовый синтаксис: типы, переменные, операторы

    В предыдущей статье вы установили Go, создали модуль с go mod init и запустили программу через go run .. Теперь разберём базовый синтаксис, на котором держится любой Go-код: типы, переменные и операторы.

    Как устроен файл Go-программы

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

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

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

  • Спецификация языка Go
  • Пакет fmt
  • Имена, ключевые слова и видимость

    Идентификаторы

    Идентификатор — это имя переменной, функции, типа, константы.

    Правило видимости в Go простое:

  • имя, начинающееся с заглавной буквы, экспортируется из пакета (видно снаружи)
  • имя, начинающееся со строчной буквы, не экспортируется (видно только внутри пакета)
  • Пример: fmt.Println — экспортируемая функция (поэтому её можно вызывать из вашего main).

    Комментарии

    Типы в Go: что такое и зачем

    Тип описывает:

  • какие значения может хранить переменная
  • какие операции допустимы над этими значениями
  • Go — язык со статической типизацией: тип известен во время компиляции. Это помогает ловить ошибки раньше.

    Базовые типы

    | Категория | Типы | Пример значения | Комментарий | |---|---|---|---| | Логический | bool | true | Только true или false | | Целые числа | int, int8, int16, int32, int64 | -10 | int зависит от платформы (обычно 64-бит) | | Беззнаковые целые | uint, uint8, uint16, uint32, uint64, uintptr | 10 | Только неотрицательные | | Вещественные | float32, float64 | 3.14 | Дробные числа | | Комплексные | complex64, complex128 | 1 + 2i | Используются реже | | Строки | string | "go" | Неизменяемая последовательность байт |

    Отдельно часто встречаются два специальных целочисленных типа:

  • byte — это псевдоним uint8, обычно используют для работы с байтами
  • rune — это псевдоним int32, обычно используют для Юникод-кода символа
  • Пример, показывающий разницу между байтами и рунами в строке:

    len(s) для строк возвращает количество байт, а не количество символов.

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

    Если переменную объявить, но не присвоить значение, Go задаст нулевое значение (zero value):

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

    nil означает отсутствие значения/ссылки.

    Переменные: объявление и присваивание

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

    Здесь:

  • var age int — объявили переменную типа int (значение будет 0)
  • var ok = true — тип выводится автоматически (вывод типа)
  • Короткое объявление :=

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

    Это одновременно:

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

    Несколько переменных сразу

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

    Переменная существует только внутри блока { ... }, где она объявлена.

    Пример:

    Здесь внутри блока печатается 20, а снаружи — 10.

    Пустой идентификатор _

    _ используют, когда значение не нужно.

    Пример: функция вернула два значения, а нужно только первое:

    Константы

    Константа — значение, которое нельзя изменить.

    Константы удобны для настроек и значений, которые не должны «случайно» поменяться.

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

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

    Пример, который не скомпилируется:

    Нужно преобразовать тип явно:

    Запись int64(a) — это преобразование типа (не «каст» в смысле C/C++), оно создаёт значение другого типа.

    Операторы: что можно делать со значениями

    Арифметические операторы

    Для чисел:

  • + сложение
  • - вычитание
  • * умножение
  • / деление
  • % остаток от деления (только для целых)
  • Пример:

    Для строк + означает конкатенацию (склейку):

    Операторы сравнения

    Возвращают bool:

  • == равно
  • != не равно
  • <, <=, >, >= сравнение
  • Пример:

    Логические операторы

    Работают с bool:

  • && логическое И
  • || логическое ИЛИ
  • ! логическое НЕ
  • Особенность: && и || вычисляются с коротким замыканием — правая часть не вычисляется, если результат уже понятен.

    Операторы присваивания

  • = обычное присваивание
  • +=, -=, *=, /=, %= сокращённые формы
  • Пример:

    Инкремент и декремент

    В Go есть ++ и --, но они являются операторами-выражениями на уровне инструкции, а не значением.

    То есть так можно:

    А так нельзя:

    Побитовые операторы

    Используются для работы с битами целых чисел:

  • & AND
  • | OR
  • ^ XOR
  • &^ AND NOT (очистка битов)
  • <<, >> сдвиги
  • Эти операторы особенно полезны в низкоуровневом коде, протоколах, флагах.

    Полезные приёмы для проверки себя в коде

    Печать типа и значения

    fmt.Printf помогает видеть типы:

    Здесь:

  • %v печатает значение «в общем виде»
  • %T печатает тип
  • Форматирование кода

    Как и в прошлой статье, придерживайтесь gofmt:

    Это форматирует Go-файлы в текущей папке (и подпапках).

    Что дальше

    Теперь вы знаете, как в Go:

  • устроен файл программы (package, import, main)
  • объявляются переменные (var и :=) и как работают нулевые значения
  • устроены базовые типы и чем отличаются byte и rune
  • выполняются основные операции: арифметика, сравнение, логика, присваивание
  • делается явное преобразование типов
  • Следующий логичный шаг после синтаксической базы — перейти к управляющим конструкциям (ветвления и циклы) и функциям, чтобы писать программы с логикой, а не только с объявлениями и выражениями.

    3. Функции, структуры, методы и интерфейсы

    Функции, структуры, методы и интерфейсы

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

    Полезные источники по теме:

  • Спецификация Go
  • Effective Go
  • Функции

    Функция в Go объявляется ключевым словом func, имеет имя, список параметров и (опционально) возвращаемые значения.

    Объявление и вызов

    Особенности, которые важно заметить:

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

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

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

    Пример: деление с проверкой на ноль.

    Возврат ошибок как отдельного значения

    В Go принято возвращать ошибки отдельным значением типа error. error — это встроенный интерфейс (к интерфейсам мы ещё вернёмся), который представляет ошибку как значение.

    Идиоматический порядок результатов такой:

  • сначала полезные значения
  • затем error последним
  • Пример:

    Здесь nil означает, что ошибки нет.

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

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

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

  • именованные результаты полезны, когда имя реально улучшает читаемость
  • не стоит использовать их только ради сокращения return
  • Вариативные параметры

    Функция может принимать переменное число аргументов. Тогда последний параметр объявляется как ...T.

    Если у вас уже есть срез []int, его можно передать как вариативные аргументы через ...:

    Структуры

    Структура (struct) — это пользовательский тип данных, который объединяет несколько полей в одну сущность. Если переменные и базовые типы — это кирпичи, то структуры — способ собрать из них понятные модели предметной области.

    Объявление структуры и создание значений

    Правила, которые стоит запомнить:

  • нулевое значение структуры корректно и безопасно (но не всегда полезно)
  • литерал User{ID: 1, Name: "Ada"} предпочтительнее, чем позиционный User{1, "Ada"}, потому что читаемее и устойчивее к изменениям структуры
  • Доступ к полям и изменение

    Указатель на структуру

    Часто используют указатель на структуру *User, чтобы:

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

    Встраивание (embedding)

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

    Важно понимать:

  • это не наследование в смысле классов
  • это удобный способ переиспользовать поля и методы через композицию
  • Методы

    Метод — это функция, привязанная к типу. В Go методы чаще всего определяют для структур (хотя можно и для других именованных типов).

    Метод отличается от функции наличием получателя (receiver): параметра перед именем метода.

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

    Здесь u User — получатель. Метод можно читать как: функция Greet, которая работает с User.

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

    Если метод должен изменять объект, получатель делают указателем:

    Практические правила выбора получателя:

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

    Интерфейс — это набор методов. Тип удовлетворяет интерфейсу, если у него есть все методы с нужными сигнатурами.

    Ключевая особенность Go:

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

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

  • Пакет io
  • Пакет fmt
  • Объявление интерфейса и реализация

    Пример: интерфейс с одним методом.

    Здесь User удовлетворяет Greeter, потому что у User есть метод Greet() string.

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

    Интерфейсные значения и nil

    Переменная интерфейсного типа хранит:

  • конкретное значение
  • конкретный тип этого значения
  • nil-интерфейс — это когда и тип, и значение отсутствуют.

    Важный нюанс: интерфейс может быть не равен nil, даже если внутри лежит nil-указатель конкретного типа.

    Этот случай важно помнить при проверках if err != nil и при работе с интерфейсами в целом.

    Пустой интерфейс и why it exists

    Интерфейс interface{} не требует методов, значит ему удовлетворяет любой тип. В современном Go часто предпочитают обобщения (generics) для типобезопасного кода, но interface{} всё ещё встречается в:

  • API, которые принимают данные произвольного типа
  • отражении (reflection)
  • Старайтесь использовать конкретные типы и конкретные интерфейсы, когда это возможно: так компилятор лучше проверяет ваш код.

    Приведение из интерфейса: type assertion

    Иногда нужно достать конкретный тип из интерфейса. Для этого используют утверждение типа (type assertion).

    Безопасная форма возвращает два значения:

    Если использовать форму с одним значением s := x.(string) и тип не совпадёт, будет паника во время выполнения.

    Type switch

    Если нужно обработать несколько вариантов типов, используйте switch по типу:

    Связка структур, методов и интерфейсов на практике

    Типичная архитектурная идея в Go:

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

  • fmt.Stringer
  • Что дальше

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

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

    4. Коллекции и работа с данными: массивы, слайсы, мапы

    Коллекции и работа с данными: массивы, слайсы, мапы

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

    Официальные источники по теме:

  • Спецификация Go
  • Раздел про встроенные функции (builtins)
  • Статья в блоге Go про слайсы
  • Массивы

    Массив в Go — это последовательность элементов фиксированной длины. Длина массива является частью типа.

    Объявление и инициализация

    Что важно:

  • тип [3]int и [4]int — это разные типы
  • массивы копируются целиком при присваивании и передаче в функцию
  • Копирование массива и последствия

    Если вам нужно изменять исходный массив внутри функции, передавайте указатель *[N]T.

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

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

    Слайс ([]T) — это динамическое представление последовательности элементов. В отличие от массива, слайс не хранит элементы “внутри себя”. Он указывает на подлежащий массив (внутреннее хранилище) и содержит метаданные.

    Слайс можно понимать как структуру с тремя идеями:

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

    Создание слайсов

    #### Литерал

    #### Через make

    make создаёт слайс заданной длины и (опционально) ёмкости.

    #### Нулевое значение и пустой слайс

    Нулевое значение слайса — nil.

    Практически:

  • len у nil-слайса равен 0
  • append работает и с nil-слайсом
  • иногда различие nil и пустого слайса важно при JSON и API-контрактах
  • Индексация и срезы

    Доступ к элементу:

    Взятие части слайса:

    Правило границ:

  • нижняя граница включается
  • верхняя граница не включается
  • len и cap

  • len(s) — сколько элементов сейчас в слайсе
  • cap(s) — сколько элементов можно вместить, пока не потребуется расширение
  • append и рост слайса

    append добавляет элементы в конец и возвращает новый слайс.

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

  • иногда append переиспользует существующее хранилище
  • иногда выделяет новое (обычно когда len превышает cap)
  • поэтому всегда присваивайте результат обратно: s = append(s, ...)
  • Общая память у слайсов и подводные камни

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

    Вывод:

  • изменение элемента через один слайс может изменить “видимое” содержимое других слайсов
  • если вам нужно отделить данные, используйте копирование
  • copy: копирование элементов

    copy(dst, src) копирует элементы из src в dst и возвращает число скопированных элементов.

    Частый шаблон для “отвязки” от общего хранилища:

    Здесь создаётся новый слайс и в него копируются элементы.

    Итерация по слайсам

    Чаще всего используется for range.

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

    Мапы

    Мапа (map[K]V) — это ассоциативный массив: структура данных “ключ → значение”. Используется, когда нужен быстрый доступ по ключу.

    Создание мапы

    Через make:

    Через литерал:

    Нулевое значение мапы — nil. В nil-мапу нельзя записывать.

    Чтение, нулевые значения и идиома comma ok

    Если ключа нет, чтение возвращает нулевое значение типа V.

    Чтобы отличить “ключа нет” от “значение равно нулю”, используйте второе значение при чтении.

    Удаление

    Удаление делается встроенной функцией delete.

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

    Итерация по мапам

    Важная особенность:

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

  • собирают ключи в слайс
  • сортируют
  • обходят по отсортированным ключам
  • Для сортировки полезен пакет:

  • Пакет sort
  • Ограничения мап

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

  • ключ типа K должен быть сравнимым (например, string, числа, указатели, структуры из сравнимых полей)
  • slice, map и func нельзя использовать как ключи
  • мапы нельзя сравнивать друг с другом через == (можно только сравнивать с nil)
  • нельзя брать адрес элемента мапы (например, &m["x"] запрещено)
  • Как выбирать: массив, слайс или мапа

  • Массив используйте, когда размер фиксирован и это улучшает модель данных (например, [16]byte как идентификатор или блок данных).
  • Слайс используйте почти всегда, когда нужно “список элементов” переменной длины.
  • Мапу используйте, когда нужен доступ по ключу и быстрое наличие проверки.
  • Типичные комбинации в реальных программах:

  • []User или []struct{...} для списков сущностей
  • map[string]User для доступа по ID или имени
  • map[string][]User для группировок
  • Связь с предыдущими темами курса

    Коллекции постоянно используются вместе с функциями, структурами и интерфейсами:

  • функции часто принимают []T и возвращают []T как результат обработки данных
  • структуры обычно содержат поля-слайсы и поля-мапы
  • интерфейсы из стандартной библиотеки (например, при работе с вводом-выводом) часто приводят к обработке данных в виде слайсов байт []byte
  • Дальше эти знания лягут в основу тем про обработку данных, ошибки в реальных сценариях и организацию кода по пакетам.

    5. Ошибки, пакеты, модули и управление зависимостями

    Ошибки, пакеты, модули и управление зависимостями

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

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

  • Effective Go
  • Go Blog: Errors are values
  • Документация: Go Modules
  • Справочник: go.mod file
  • Пакет errors
  • Ошибки в Go

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

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

    error — это интерфейс:

    Это означает:

  • ошибка — обычное значение, которое можно хранить в переменной, передавать в функцию, возвращать
  • отсутствие ошибки обозначают nil
  • Типичный стиль функций:

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

    Самые частые способы:

  • errors.New("сообщение")
  • fmt.Errorf("шаблон", args...)
  • Пример:

    Ранний выход и читаемость

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

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

    Часто ошибка из нижнего уровня (например, чтение файла) недостаточно информативна без контекста что именно делала ваша функция. Для добавления контекста используют fmt.Errorf с %w.

    Здесь важно:

  • %w сохраняет исходную ошибку внутри новой
  • сверху вы видите понятное сообщение (контекст), а код всё ещё может распознать исходную причину через errors.Is
  • Проверка ошибок: errors.Is и errors.As

    Есть два основных сценария:

  • проверить, что ошибка относится к известной причине (например, файл не найден)
  • достать конкретный тип ошибки, чтобы получить дополнительные поля (например, путь)
  • Для первого используют errors.Is(err, target).

    Для второго — errors.As(err, &targetType).

    Пример errors.As с ошибкой чтения файла:

    Сентинел-ошибки и сравнение

    Иногда в пакете объявляют переменную-ошибку как маркер конкретного случая:

    Это называют сентинел-ошибкой. Если ошибки могут оборачиваться, то сравнивать через == ненадёжно. Правильнее:

  • использовать errors.Is(err, ErrNotFound)
  • Когда использовать panic

    panic — это аварийное завершение текущего потока выполнения с раскруткой стека. В прикладном коде panic используют редко.

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

  • возвращайте error, если это ожидаемая ситуация (неверный ввод, нет файла, сеть недоступна)
  • используйте panic, если это невозможное состояние или ошибка программиста (нарушен инвариант, критически неправильная конфигурация, которую программа не умеет обрабатывать)
  • > “Don’t panic.” — Rob Pike, один из авторов Go. Выступление «Go Proverbs»

    Пакеты: как организован код

    Что такое пакет

    Пакет — единица организации кода в Go. Любой .go файл начинается с:

    Все файлы в одной папке обычно относятся к одному пакету.

    Пакеты позволяют:

  • разделять ответственность (например, storage, httpapi, domain)
  • скрывать внутренние детали за неэкспортируемыми именами
  • переиспользовать код через импорт
  • Экспорт и видимость

    Вы уже встречали правило:

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

    Из другого пакета будет доступно только calc.Add.

    Импорт пакетов

    Импорт задаёт путь к пакету:

    Важно различать:

  • имя пакета в коде (calc)
  • путь импорта (example.com/myapp/calc)
  • Обычно имя пакета — это последний сегмент пути, но технически оно определяется строкой package ... внутри исходников.

    Инициализация: init

    В Go можно объявить функцию init(), она выполнится автоматически при инициализации пакета.

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

  • избегайте сложной логики в init
  • предпочитайте явную инициализацию из main, чтобы код было проще тестировать
  • Типичная структура проекта

    Есть разные стили, но для учебных и небольших проектов удобно начать так:

    Здесь:

  • cmd/myapp/main.go — точка входа (исполняемый файл)
  • internal/... — пакеты, которые нельзя импортировать извне модуля (это правило Go)
  • !Схема показывает, как main импортирует внутренние пакеты и где обычно располагаются части проекта

    Модули: единица версионирования и зависимостей

    Что такое модуль

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

    Создание модуля вы уже делали:

    Файл go.mod

    go.mod хранит:

  • модульный путь (идентификатор модуля)
  • версию Go, на которой рассчитан модуль
  • зависимости (какие модули и какие версии нужны)
  • Пример:

    Файл go.sum

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

  • фиксировать, какой именно код зависимостей был скачан
  • защищать сборку от незаметной подмены содержимого версий
  • Обычно:

  • go.mod и go.sum коммитят в репозиторий вместе
  • Управление зависимостями: основные команды

    go get

    В современных версиях Go go get используют в первую очередь для добавления или обновления зависимостей:

    go mod tidy

    Приводит зависимости в порядок:

  • добавляет в go.mod то, что реально используется (по импортам)
  • удаляет лишнее
  • обновляет go.sum
  • Обычно go mod tidy запускают:

  • после добавления новых импортов
  • после удаления кода или пакетов
  • перед коммитом, чтобы репозиторий был в чистом состоянии
  • go list -m

    Позволяет посмотреть зависимости модуля. Например:

    go mod vendor

    Создаёт папку vendor/ с исходниками зависимостей. Это нужно не всегда, но бывает полезно когда:

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

    Практический рабочий цикл

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

  • Добавили импорт нового внешнего пакета в код.
  • Выполнили go mod tidy.
  • Запустили go test ./....
  • Закоммитили изменения go.mod и go.sum.
  • Мини-проект: связываем ошибки, пакеты и модуль

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

    Структура:

    internal/calc/calc.go:

    cmd/myapp/main.go:

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

  • calc.ErrDivisionByZero — это общий контракт пакета calc для конкретной ошибки
  • errors.Is позволяет корректно проверять ошибки даже если позже вы начнёте их оборачивать
  • internal/ защищает пакет от импорта внешними модулями
  • Что дальше

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

  • ошибки обрабатываются явно и возвращаются как error
  • код делится на пакеты, а внешний API пакета задаётся экспортируемыми именами
  • зависимости фиксируются через go.mod и go.sum, а управляются командами go mod и go get
  • Следующий логичный шаг после этого блока — перейти к более прикладным темам: тестирование (go test), работа с вводом-выводом, HTTP, конкурентность, а также более строгая организация проекта и интерфейсы на границах модулей.

    6. Конкурентность: горутины, каналы, контекст

    Конкурентность: горутины, каналы, контекст

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

    В Go конкурентность строится вокруг трёх ключевых идей:

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

  • Effective Go: Concurrency
  • Пакет sync
  • Пакет context
  • Go Blog: Context
  • Go Blog: Pipelines and cancellation
  • !Диаграмма показывает, как горутины обмениваются данными через каналы и как context отменяет работу

    Что такое конкурентность в Go

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

    Практические примеры задач, где конкурентность полезна:

  • обработка нескольких сетевых запросов одновременно
  • параллельное чтение файлов и обработка данных
  • конвейеры обработки событий
  • Горутины

    Запуск горутины

    Горутина запускается ключевым словом go перед вызовом функции.

    Что важно:

  • main не ждёт горутины автоматически
  • если main завершилась, процесс завершается, даже если горутины не закончились
  • Ожидание завершения: sync.WaitGroup

    Самый распространённый способ дождаться набора горутин — sync.WaitGroup.

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

  • wg.Add(1) делайте до запуска горутины
  • wg.Done() удобнее всего ставить через defer
  • Осторожно с переменными цикла

    Одна из типичных ошибок новичков — захват переменной цикла в замыкание.

    Неправильно:

    Правильно — передать значение как параметр:

    Гонки данных и sync.Mutex

    Если две горутины читают и пишут одну и ту же переменную без синхронизации, возникает гонка данных (data race). Это может приводить к редким и трудно воспроизводимым багам.

    Пример проблемы:

    Решение через sync.Mutex:

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

  • в Go часто стараются проектировать обмен через каналы, но Mutex остаётся важным инструментом, когда нужен общий изменяемый ресурс
  • Чтобы находить гонки, используйте встроенный инструмент:

    Каналы

    Канал — это типизированный способ передавать значения между горутинами.

    Отправка и получение

  • отправка: ch <- value
  • получение: v := <-ch
  • Пример: одна горутина отправляет, другая получает.

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

    Канал бывает:

  • небуферизованный: make(chan T)
  • буферизованный: make(chan T, n)
  • Небуферизованный канал синхронизирует горутины: отправитель блокируется, пока получатель не заберёт значение.

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

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

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

    Канал можно закрыть: close(ch). Это сигнал получателям, что новых значений не будет.

    Важно:

  • закрывать канал должен отправитель (тот, кто гарантированно больше не будет писать)
  • запись в закрытый канал приводит к панике
  • чтение из закрытого канала возможно: будут вычитаны оставшиеся значения, затем чтение начнёт возвращать нулевые значения
  • Частый шаблон: отправитель закрывает канал, получатель читает через range.

    Идиома comma ok

    Если нужно читать по одному и понимать, закрыт ли канал:

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

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

    Простой пример

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

    Замечание:

  • time.After создаёт канал, который получит значение через указанное время
  • default: неблокирующий select

    Если нужен вариант не ждать:

    Контекст: отмена, дедлайны, метаданные

    context.Context — стандартный интерфейс, который используют почти все сетевые и инфраструктурные библиотеки Go. Он решает три задачи:

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

    Контекст обычно создаётся в верхнем уровне (например, в обработчике HTTP-запроса) и передаётся вниз по параметрам функций.

    Ключевые правила:

  • контекст передают первым аргументом: func Do(ctx context.Context, ...)
  • контекст не хранят в структурах как поле для долгоживущих объектов
  • вместо nil всегда передают реальный контекст (если нет своего, используйте context.Background())
  • Отмена через context.WithCancel

    Что происходит:

  • ctx.Done() — канал, который закрывается при отмене или истечении времени
  • ctx.Err() — причина завершения (например, context.Canceled)
  • Таймаут через context.WithTimeout

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

  • всегда вызывайте cancel() (обычно defer cancel()), чтобы освобождать связанные ресурсы
  • Контекст и каналы вместе

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

    Здесь есть сразу несколько важных практик:

  • jobs <-chan int означает канал только для чтения (воркер не может в него писать)
  • отправитель закрывает jobs
  • воркер выходит либо по ctx.Done(), либо по закрытию jobs
  • Типовые паттерны конкурентности

    Worker pool

    Worker pool ограничивает количество одновременных обработчиков, чтобы не создавать слишком много горутин и не перегружать ресурсы.

    Схема обычно такая:

  • один канал заданий
  • фиксированное число воркеров
  • опционально канал результатов
  • Fan-out и fan-in

  • fan-out: вы “раздаёте” задания нескольким воркерам
  • fan-in: вы “собираете” результаты от нескольких горутин в один поток
  • Эти идеи удобно реализуются каналами и select.

    Практические советы и частые ошибки

  • Всегда думайте, кто закрывает канал: обычно закрывает тот, кто пишет.
  • Не используйте закрытие канала как сигнал “остановись немедленно” для множества отправителей: это часто приводит к паникам из-за записи в закрытый канал.
  • Не забывайте про завершение горутин: утечки горутин (goroutine leaks) появляются, когда горутина навсегда блокируется на чтении/записи.
  • Используйте context для отмены в сетевых и долгих операциях: это стандарт экосистемы.
  • Проверяйте гонки через go test -race.
  • Связь с предыдущими темами курса

    Эта тема напрямую продолжает предыдущие статьи:

  • из блока про функции вы уже знаете про возврат error: в конкурентном коде это особенно важно, потому что ошибки возникают в разных местах и их нужно корректно собирать
  • из блока про пакеты и модули важно уметь выделять конкурентные части в отдельные пакеты (например, internal/worker)
  • из блока про коллекции каналы часто передают значения структур и слайсов (chan Job, chan []byte)
  • Дальше обычно переходят к прикладным темам, где конкурентность встречается постоянно: HTTP-серверы и клиенты, работа с вводом-выводом, тестирование конкурентного кода и проектирование границ через интерфейсы.

    7. Тестирование и создание сетевых приложений (HTTP, JSON)

    Тестирование и создание сетевых приложений (HTTP, JSON)

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

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

  • тестирование через go test и пакет testing
  • создание HTTP-сервисов и работу с JSON на стандартной библиотеке
  • Ключевые пакеты, которые будем использовать:

  • net/http
  • encoding/json
  • testing
  • net/http/httptest
  • context
  • Как устроено тестирование в Go

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

    Как запускаются тесты

    Основная команда:

    Что означает ./...:

  • ./ это текущая папка
  • ... означает рекурсивно все подпакеты
  • Часто используемые варианты:

  • go test -v ./... выводит подробный лог
  • go test -race ./... включает детектор гонок данных
  • go test -cover ./... считает покрытие
  • Как Go находит тесты

    Go считает тестом функцию вида:

    и файл с именем, заканчивающимся на _test.go.

    Пример мини-теста:

    Стиль: table-driven tests

    Самый распространённый стиль в Go для проверки множества сценариев это табличные тесты.

    Здесь t.Run делает каждый кейс отдельным подтестом.

    Полезная статья про подтесты: Subtests and Sub-benchmarks.

    Что делать с ошибками в тестах

    Есть два типичных сценария:

  • тестируете, что ошибки нет
  • тестируете, что ошибка конкретного вида
  • Проверка отсутствия ошибки:

    Проверка конкретной причины ошибки, если вы используете оборачивание через %w:

    Это напрямую связано с предыдущей темой про errors.Is и errors.As.

    HTTP в Go: базовая архитектура

    В стандартной библиотеке HTTP построен вокруг идеи обработчика запросов.

    Интерфейс http.Handler

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

  • http.ResponseWriter это то, куда вы пишете ответ
  • *http.Request содержит входящие данные: метод, URL, заголовки, тело
  • Практический вариант, который используют чаще всего: http.HandlerFunc.

    Запуск сервера

    Минимальный HTTP-сервер:

    Как связать HTTP, контекст и вашу бизнес-логику

    У каждого запроса есть контекст: r.Context(). Внутрь бизнес-логики его нужно передавать явно, продолжая стиль из темы про context.

    Пример: обработчик вызывает сервис.

    Так границы становятся тестируемыми, потому что UserService легко подменить в тестах.

    !Поток обработки HTTP-запроса и передача контекста

    JSON в Go: кодирование и декодирование

    Основной пакет: encoding/json.

    Marshal и Unmarshal

    Если вы работаете с []byte, можно использовать:

  • json.Marshal(v) для кодирования в JSON
  • json.Unmarshal(data, &v) для декодирования
  • Но в HTTP чаще используют потоковый вариант: json.Encoder и json.Decoder.

    JSON-теги в структурах

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

    Частый тег omitempty означает: не включать поле в JSON, если оно имеет нулевое значение.

    Декодирование входящего JSON безопаснее делать через Decoder

    Паттерн для HTTP:

    DisallowUnknownFields полезен для API-контрактов: если клиент прислал лишнее поле, вы это заметите.

    Кодирование ответа

    Encode добавляет перевод строки в конце. Для API это обычно нормально.

    Мини-пример: HTTP API для создания пользователя

    Ниже пример, где разделены ответственности:

  • HTTP-слой: валидация ввода, коды ответов, JSON
  • сервис: бизнес-логика
  • Типы запросов и ответов

    Контракт сервиса

    Handler

    Тестирование HTTP-обработчиков

    Для HTTP в Go есть отличный пакет net/http/httptest, который позволяет тестировать обработчики без реального запуска порта.

    Тест: проверяем код ответа и JSON

    Сделаем фейковый сервис, который всегда создаёт пользователя.

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

  • httptest.NewRequest создаёт запрос
  • httptest.NewRecorder записывает ответ
  • мы вызываем mux.ServeHTTP(rec, req) напрямую
  • Табличные тесты для ошибок валидации

    Тестирование HTTP-клиента

    Если ваш код делает HTTP-запросы наружу, не стоит тестировать его, обращаясь к реальным сервисам.

    Два основных подхода:

  • httptest.NewServer поднимает тестовый HTTP-сервер в процессе теста
  • подмена http.Client через кастомный Transport (сложнее, но полезно для более низкоуровневых тестов)
  • Пример с httptest.NewServer

    Этот подход максимально близок к реальному взаимодействию клиента и сервера, но работает локально и детерминированно.

    Практические рекомендации для сетевого кода

  • Всегда выставляйте Content-Type в JSON-ответах как application/json.
  • В HTTP-обработчиках проверяйте r.Method, если endpoint поддерживает не все методы.
  • Декодируйте JSON через json.Decoder и включайте DisallowUnknownFields, если вы хотите строгий контракт.
  • Передавайте r.Context() вниз в сервисы и репозитории.
  • Используйте интерфейсы на границах, чтобы тесты могли подменять зависимости.
  • Запускайте go test -race ./..., если есть конкурентность или обработка нескольких запросов.
  • Как эта тема связывается с предыдущими

  • Из темы про ошибки вы берёте привычку возвращать error из бизнес-логики и превращать её в корректные HTTP-ответы.
  • Из темы про пакеты и модули вы берёте организацию кода: HTTP-слой отдельным пакетом, бизнес-логика отдельным пакетом.
  • Из темы про конкурентность и контекст вы берёте r.Context() и понимаете, как корректно останавливать работу по отмене или таймауту.
  • Дальше логично углубляться в прикладную архитектуру: middleware, логирование, конфигурация, работу с базами данных и интеграционные тесты.