Go (Golang) с нуля: основы, практика и мини-проекты

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

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

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

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

Что вы получите после урока

  • Установленный Go и проверенную установку через терминал
  • Понимание, какие инструменты нужны для работы каждый день
  • Первый проект на Go с корректной структурой
  • Первую программу Hello, World! и навыки запуска и сборки
  • Установка Go

    Где скачать

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

  • Downloads — The Go Programming Language
  • Install the Go programming language
  • Установка на Windows

  • Скачайте установщик .msi с официальной страницы загрузок.
  • Запустите установку и оставьте настройки по умолчанию.
  • Откройте PowerShell и проверьте:
  • Если команда не найдена, обычно проблема в переменной окружения PATH. В таком случае проще всего переустановить Go с официального установщика и убедиться, что галочка добавления в PATH включена (в большинстве версий установщик делает это сам).

    Установка на macOS

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

    На странице установки есть актуальные шаги под ваш дистрибутив, но общий подход такой:

  • Скачайте архив tar.gz с официальной страницы.
  • Распакуйте в стандартную директорию.
  • Добавьте .../go/bin в PATH.
  • Проверьте:
  • > Если вы делаете первые шаги, лучше строго следовать официальной инструкции установки для вашей ОС: Install the Go programming language

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

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

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

  • go version — показывает версию Go.
  • go env — показывает настройки окружения (куда ставятся пакеты, где кеш, какая ОС/архитектура).
  • go mod init — создаёт новый проект (модуль) с управлением зависимостями.
  • go run — запускает программу без отдельного шага сборки.
  • go build — собирает исполняемый файл.
  • go test — запускает тесты.
  • go fmt — форматирует код по стандарту Go.
  • Что такое модуль (Go module)

    Модуль — это ваш проект на Go с файлом go.mod в корне. В go.mod хранится:

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

  • Go Modules Reference
  • !Схема, показывающая типичный цикл разработки: писать код, форматировать, тестировать, запускать и собирать

    Редактор и расширения

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

  • Visual Studio Code
  • Go extension for Visual Studio Code
  • После установки расширения оно обычно предложит поставить дополнительные инструменты (для подсказок, форматирования, навигации по коду). Соглашайтесь — это стандартная практика.

    Создаём первый проект

    Выбираем папку

    Создайте отдельную папку для проектов, например:

  • C:\projects\go (Windows)
  • ~/projects/go (macOS/Linux)
  • Далее создайте папку проекта, например hello-go, и перейдите в неё.

    Инициализируем модуль

    Выполните:

    Появится файл go.mod. Имя example.com/hello-go — это идентификатор модуля. Для учебных проектов можно использовать такой формат, а для реальных часто используют домен/путь репозитория.

    Первая программа на Go

    Создайте файл main.go в папке проекта со следующим кодом:

    Разбор кода простыми словами

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

  • pkg.go.dev — Go Packages
  • Запуск и сборка

    Быстрый запуск

    Запустите программу командой:

    Точка означает текущую папку (текущий модуль/пакет). В результате вы увидите:

  • Hello, World!
  • Сборка исполняемого файла

    Соберите программу:

    После этого рядом появится исполняемый файл:

  • Windows: hello-go.exe (или имя папки/модуля, зависит от окружения)
  • macOS/Linux: hello-go
  • Запустите его:

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

    В Go принято форматировать код единообразно. Для этого есть стандартный форматтер.

    Выполните:

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

  • go: command not found или «go не является внутренней или внешней командой»
  • - Go не установлен или не добавлен в PATH. Проверьте установку по официальной инструкции: Install the Go programming language
  • Проект запускается не из той папки
  • - Убедитесь, что вы находитесь в директории, где лежит go.mod.
  • Не создаётся go.mod
  • - Проверьте права на папку и что вы запускаете go mod init внутри папки проекта.

    Что дальше по курсу

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

    Для самостоятельного закрепления позже будет полезно пройти официальный мини-туториал:

  • Get started with Go
  • 2. Синтаксис и базовые типы: переменные, функции, управление потоком

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

    В прошлом уроке вы установили Go, создали модуль через go mod init, написали main.go и запустили Hello, World! командой go run .. Теперь разберём основы языка: как объявлять переменные и константы, какие базовые типы есть в Go, как писать функции и управлять выполнением программы через if, switch, for.

    Полезные официальные источники (на будущее, как справочник):

  • Спецификация языка Go
  • Effective Go
  • Минимальный каркас программы

    Типичный исполняемый файл на Go выглядит так:

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

  • package main — пакет, который собирается в исполняемый файл.
  • import "fmt" — подключение пакета стандартной библиотеки.
  • func main() — точка входа.
  • > В Go папка обычно соответствует пакету: все .go файлы в одной папке (с одинаковым package ...) компилируются вместе.

    Переменные

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

    var позволяет объявить переменную на уровне функции или на уровне пакета (вне функций).

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

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

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

    Правила :=:

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

    Исправление:

    Значения по умолчанию (zero values)

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

  • int и другие числа — 0
  • boolfalse
  • string — пустая строка ""
  • Область видимости

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

    !Наглядно показывает, где переменные доступны, а где выходят из области видимости

    Константы

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

    iota для последовательностей

    iota помогает создавать последовательные значения в блоке const.

    Здесь будут значения 0, 1, 2.

    Базовые типы

    Ниже — основные типы, которые чаще всего встречаются на старте.

    | Тип | Что хранит | Пример значения | Комментарий | |---|---|---|---| | bool | логика | true | часто в условиях | | string | строки (байты в UTF-8) | "Привет" | строка неизменяема | | int | целое число | 42 | размер зависит от платформы (32/64) | | int64 | целое 64-бит | int64(42) | удобно для больших чисел | | uint | беззнаковое целое | uint(10) | для счётчиков и размеров | | byte | байт (uint8) | byte(255) | часто для данных/файлов | | rune | Unicode-код (int32) | rune('Ж') | для символов | | float64 | число с плавающей точкой | 3.14 | основной тип для вещественных | | complex128 | комплексное число | 1 + 2i | используется редко |

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

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

    Это сделано, чтобы уменьшить количество неожиданных ошибок.

    Форматированный вывод

    Пакет fmt поддерживает форматирование:

    Полезно помнить:

  • %s — строка
  • %d — целое число
  • %v — значение “как есть”
  • %T — тип
  • Справочник по fmt:

  • Документация пакета fmt
  • Функции

    Объявление и параметры

    Функция объявляется через func:

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

    Возврат нескольких значений

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

    Идея простая:

  • если всё хорошо, возвращаем результат и nil вместо ошибки
  • если ошибка, возвращаем “пустой” результат и объект ошибки
  • Вариативные параметры

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

    nums внутри функции — это набор переданных значений, по которым можно пройти циклом.

    Управление потоком

    if и “короткое” выражение перед условием

    В Go можно объявить переменную прямо в if — она будет доступна только внутри if и else.

    Здесь:

  • n := 10 выполняется до проверки условия
  • n существует только внутри этого if/else
  • switch

    switch удобен для выбора ветки по значению. В Go:

  • break обычно не нужен
  • переход к следующему кейсу не происходит автоматически
  • Можно писать switch без выражения — тогда кейсы становятся условиями:

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

    В Go нет while и do...while: вместо них используют for.

    Классический цикл со счётчиком:

    Цикл “как while”:

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

    Цикл по диапазону (range) часто используют со строками, массивами, срезами и картами (к ним вы ещё вернётесь дальше по курсу):

    break и continue

  • break — выйти из цикла
  • continue — перейти к следующей итерации
  • defer — отложенное выполнение

    defer запускает вызов в конце текущей функции (даже если функция завершилась через return). Это полезно для освобождения ресурсов.

    Порядок вывода будет:

  • start
  • work
  • end
  • Итоги

    Теперь у вас есть базовый “набор выживания” в Go:

  • переменные: var и :=, значения по умолчанию, область видимости
  • константы: const, последовательности через iota
  • базовые типы и явные преобразования
  • функции: параметры, возврат значений, несколько результатов, вариативные аргументы
  • управление потоком: if, switch, for, break/continue, defer
  • В следующем уроке логично перейти к составным типам и структурам данных (массивы/срезы/карты), чтобы писать более полезные программы и обрабатывать наборы значений.

    3. Составные типы: массивы, срезы, карты, строки и работа с файлами

    Составные типы: массивы, срезы, карты, строки и работа с файлами

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

    Официальные справочники (как источник истины):

  • A Tour of Go
  • Go by Example
  • Документация пакета os
  • Документация пакета bufio
  • Документация пакета strings
  • Массивы

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

    Ключевые свойства массивов:

  • Длина фиксирована и задаётся при объявлении: [3]int и [4]int — разные типы.
  • Значения по умолчанию (zero values) применяются к каждому элементу.
  • Массив — значимый тип: при присваивании или передаче в функцию он копируется целиком.
  • Пример копирования массива:

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

    Срезы

    Срез — это «окно» на массив переменной длины. Тип среза записывается как []T.

    У среза есть:

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

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

    Когда нужно создать срез заданной длины или ёмкости, используют make:

    Индексация и slicing

    Срез можно брать из массива или другого среза:

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

  • x[a:b] включает a, но не включает b.
  • x[:b] означает «с начала до b».
  • x[a:] означает «с a до конца».
  • append и возможное перераспределение памяти

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

    Важно понимать поведение:

  • append может вернуть срез, который ссылается на тот же массив, если cap хватает.
  • или может создать новый массив и скопировать элементы, если ёмкости не хватает.
  • Из-за этого правило практики такое:

  • всегда делайте s = append(s, ...) и используйте результат.
  • Срезы и общая память

    Два среза могут смотреть на один массив. Тогда изменение элемента через один срез видно через другой:

    Цикл по срезу через range

    Вы уже видели for. Для срезов обычно используют range:

    Если индекс не нужен:

    Карты (map)

    Карта (map) — это структура «ключ → значение». Полезна для быстрых поисков по ключу.

    Создание map через make

    Если вы создаёте карту пустой и будете наполнять позже:

    Важно:

  • неинициализированная карта равна nil
  • запись в nil-карту вызовет панику
  • Проверка существования ключа: "comma ok"

    У карты чтение отсутствующего ключа возвращает zero value типа значения. Чтобы отличить «ключ отсутствует» от «ключ есть, но значение 0», используют форму с ok:

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

    Перебор map

    Порядок обхода карты не гарантируется и может меняться между запусками.

    Строки

    Строка в Go — это неизменяемая последовательность байт (обычно UTF-8 текст).

    Длина строки и байты против символов

    len(s) возвращает количество байт, а не «количество букв»:

    Чтобы безопасно итерироваться по Unicode-символам, используйте range по строке — он выдаёт руны (rune):

    Здесь:

  • i — позиция в байтах
  • rrune (Unicode-код символа)
  • Частые операции со строками

    Для работы со строками часто используют пакет strings:

    Когда вы склеиваете много строк в цикле, вместо result += part обычно используют strings.Builder (он экономит лишние выделения памяти):

    Работа с файлами

    Файлы читают и пишут через пакет os. Часто добавляют bufio для построчного чтения.

    Быстро прочитать весь файл

    Если файл небольшой, удобно прочитать его целиком:

    os.ReadFile возвращает []byte, поэтому для печати текста делаем string(data).

    Быстро записать весь файл

    Число 0644 — это права доступа на Unix-подобных системах. На Windows они интерпретируются иначе, но код остаётся переносимым.

    Открытие файла и обязательный defer Close

    Когда вы открываете файл через os.Open или os.Create, его нужно закрыть. В Go это обычно делается через defer сразу после успешного открытия.

    Здесь defer f.Close() гарантирует закрытие файла при выходе из main (в том числе при раннем return).

    Построчное чтение через bufio.Scanner

    Удобно для логов, конфигов, текстовых данных:

    Мини-пример: подсчёт строк в файле

    Этот пример связывает всё вместе: файл, defer, цикл for, переменные и простую логику.

    Итоги

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

  • массивы: фиксированная длина, копирование как значения
  • срезы: len/cap, make, append, общий базовый массив
  • карты: make, чтение/запись, delete, проверка наличия ключа через value, ok := m[key]
  • строки: неизменяемость, len в байтах, перебор через range как по рунам
  • файлы: os.ReadFile, os.WriteFile, работа через os.Open и обязательный defer Close, построчное чтение bufio.Scanner
  • Следующий логичный шаг после этого материала — научиться собирать всё в небольшие утилиты и мини-проекты: парсить ввод, хранить данные в структурах и писать более устойчивый код с обработкой ошибок.

    4. Структуры, методы, интерфейсы и модули проекта

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

    В прошлых уроках вы научились работать с базовым синтаксисом Go, срезами, картами, строками и файлами. Следующий шаг — научиться собирать данные и поведение вместе и организовывать код проекта так, чтобы он масштабировался. Для этого в Go используют структуры, методы, интерфейсы и пакеты/модули.

    Полезные официальные материалы:

  • A Tour of Go
  • Effective Go
  • Go Packages (pkg.go.dev)
  • Go Modules
  • Структуры

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

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

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

  • var u User создаёт значение структуры с полями по умолчанию.
  • Литерал User{...} удобен и читаем.
  • Именованные поля (Name: "Bob") уменьшают риск ошибок при добавлении новых полей.
  • Указатели на структуры и доступ к полям

    Часто структуру передают по указателю, чтобы:

  • не копировать большие структуры
  • изменять поля внутри функций/методов
  • Особенность Go: если у вас есть указатель c Counter, то обращаться к полям можно как c.Value (без явного (c).Value) — компилятор сделает разыменование автоматически.

    Вложенные структуры и композиция

    Структуры удобно вкладывать друг в друга:

    Такой подход помогает строить понятные модели данных и хорошо сочетается с работой с файлами и форматами (JSON, CSV) в будущих уроках.

    Методы

    Метод — это функция, привязанная к типу (чаще всего к структуре). Метод задаётся через приёмник (receiver).

    Value receiver и pointer receiver

    Приёмник может быть значением (u User) или указателем (u *User). Выбор влияет на копирование и возможность менять состояние.

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

  • Если метод должен изменять структуру — используйте *T.
  • Если структура большая — *T помогает избежать копирования.
  • Если тип должен вести себя как “объект с состоянием” — обычно все его методы делают на *T для единообразия.
  • Конструкторы в стиле Go

    В Go нет ключевого слова constructor, но принято делать функцию NewType:

    Вы уже умеете возвращать value, error из прошлых уроков — здесь это применяется к созданию корректных объектов.

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

    Go поощряет композицию. Встраивание — это когда одна структура содержит другую без имени поля.

    Это не наследование в классическом смысле, но помогает “подмешивать” поведение.

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

    Интерфейсы

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

    Объявление интерфейса

    Автоматическая реализация

    Ключевые идеи:

  • Нигде не пишется implements.
  • Интерфейсы удобно принимать в аргументах функций: это снижает связность и облегчает тестирование.
  • Пустой интерфейс и any

    any — это псевдоним для пустого интерфейса interface{}. Такой тип может хранить значение любого типа.

    Используйте any осторожно: он полезен для универсальных API, но часто скрывает ошибки типов. В современном Go для многих задач лучше подходят обобщения (generics), но это отдельная тема.

    Интерфейс error

    Ошибки в Go — это тоже интерфейс:

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

    Частая практика: маленькие интерфейсы

    Обычно в Go любят небольшие интерфейсы на 1–2 метода. Это делает код гибче.

    Примеры стандартной библиотеки (как ориентир):

  • io.Reader читает байты
  • io.Writer пишет байты
  • Смотреть удобно здесь:

  • Документация пакета io
  • Пакеты и модули проекта

    До этого момента вы писали всё в одном main.go. В реальных проектах код делят на пакеты, а зависимости управляются через модули.

    Пакет (package)

    Пакет — это набор .go файлов в одной папке с одинаковым package ....

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

  • Имя, начинающееся с заглавной буквы, — экспортируется из пакета (User, NewUser).
  • Имя со строчной буквы — внутреннее (validateName).
  • Модуль (module) и go.mod

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

    Вы уже делали:

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

  • go mod tidy — добавляет недостающие зависимости и удаляет неиспользуемые
  • go list -m all — показывает зависимости модуля
  • Документация:

  • Go Modules
  • Пример структуры небольшого проекта

    Один из удобных шаблонов для учебных и небольших утилит:

  • cmd/app/main.go — точка входа приложения
  • internal/... — код, который нельзя импортировать извне модуля
  • pkg/... — библиотечный код, который допускается импортировать извне (опционально)
  • Пример:

    internal/greet/greet.go:

    cmd/hello-go/main.go:

    Запуск из корня модуля:

    Почему это полезно:

  • main остаётся тонким (склеивает части приложения)
  • логика живёт в отдельных пакетах и легче тестируется
  • папка internal защищает от случайного “публичного API”
  • Дополнительный ориентир по структуре (это не официальный стандарт, но распространённая конвенция):

  • Standard Go Project Layout
  • !Пример структуры проекта и импортов между пакетами

    Как всё связывается с предыдущими темами

    Теперь вы можете собрать мини-утилиту, которая:

  • читает данные из файла (вы уже делали это через os и bufio)
  • парсит строки в структуры (например, User или Record)
  • хранит данные в срезах и картах ([]User, map[string]User)
  • выносит логику в методы ((u *User) Normalize())
  • абстрагирует зависимости через интерфейсы (например, Storage с методами Save и Load)
  • организует код по пакетам внутри модуля
  • Итоги

    В этом уроке вы освоили основу “архитектурного” мышления в Go:

  • структуры как способ описывать сущности и данные
  • методы как поведение, привязанное к типам, и выбор value или pointer receiver
  • интерфейсы как инструмент абстракции, который реализуется автоматически
  • пакеты и модули как основа структуры проекта, повторного использования и управления зависимостями
  • С этим набором вы готовы переходить к практике: писать небольшие утилиты и мини-проекты, где данные, ввод/вывод и логика аккуратно разделены по пакетам.

    5. Ошибки, тестирование и отладка: best practices

    Ошибки, тестирование и отладка: best practices

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

    В этой статье разберём три практики, которые постоянно используются в реальной разработке:

  • ошибки: как возвращать, обогащать контекстом и правильно проверять
  • тестирование: как писать тесты на стандартном testing и запускать go test
  • отладка: как быстрее находить причину проблем с помощью инструментов и приёмов
  • Полезные официальные ссылки (как справочник):

  • Документация пакета errors
  • Документация пакета fmt
  • Документация пакета testing
  • Команда go test (документация cmd/go)
  • Delve (отладчик Go)
  • Статья про race detector
  • Ошибки в Go: базовая модель

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

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

    Это означает: любая структура с методом Error() string может быть ошибкой.

    Главное правило

  • Если функция может завершиться неуспешно, она обычно возвращает (результат, error).
  • Если err != nil, результат обычно не используют.
  • Пример:

    User{} здесь — нулевое значение структуры, которое возвращается вместе с ошибкой.

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

    Частые варианты:

  • errors.New("...") — простая ошибка строкой
  • fmt.Errorf("...") — форматированная ошибка
  • Обогащение ошибок контекстом и wrapping

    Одна из лучших практик: не терять контекст, где именно всё сломалось. Для этого в Go есть wrapping через %w.

    Почему %w, а не %v:

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

    errors.Is(err, target) отвечает на вопрос: есть ли внутри цепочки обёрток такая причина.

    errors.As(err, &targetType) пытается извлечь ошибку конкретного типа (например, ваш тип ошибки).

    Sentinel errors против типизированных ошибок

    Есть два распространённых подхода к тому, как различать виды ошибок.

  • Sentinel error: заранее объявленная переменная ошибки, которую сравнивают через errors.Is
  • Типизированная ошибка: отдельный тип с полями (удобно, если нужно передать детали)
  • Практическое правило:

  • если нужна только категория ошибки — sentinel error
  • если нужны детали (какое поле, какой id, какие ограничения) — тип ошибки
  • Когда использовать panic

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

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

    > Хороший ориентир: если вы ожидаете, что это может случиться в продакшене из-за внешних факторов (пользователь, сеть, файл, база данных), это не повод для panic.

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

    Частая архитектурная практика:

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

    Логирование: минимум, который нужен сразу

    Для простых утилит достаточно пакета log:

    Практика для начинающих:

  • не логируйте внутри функций-библиотек без необходимости, лучше вернуть ошибку
  • логируйте в main, где вы понимаете контекст запуска
  • Если вы используете Go 1.21+, можно посмотреть на структурированное логирование log/slog:

  • Документация пакета log/slog
  • Тестирование в Go: как это устроено

    В Go встроена инфраструктура тестирования. Обычно достаточно:

  • файла something_test.go
  • функций TestXxx(t *testing.T)
  • запуска go test ./...
  • Первый тест

    Допустим, у вас есть функция:

    Тест:

    t.Error и t.Fatal

  • t.Error, t.Errorf — помечают тест как проваленный, но тест продолжит выполняться
  • t.Fatal, t.Fatalf — проваливают тест и немедленно прекращают выполнение текущего теста
  • Правило практики:

  • если после ошибки тест не имеет смысла продолжать (данные невалидны) — t.Fatal
  • если хотите собрать несколько ошибок за один прогон — t.Error
  • Табличные тесты (table-driven tests)

    Очень популярный стиль в Go: вы описываете набор кейсов и прогоняете их циклом.

    Почему это удобно:

  • легко добавлять новые кейсы
  • удобно читать набор сценариев
  • t.Run показывает отдельные результаты по каждому сценарию
  • Тестирование ошибок

    Если функция возвращает ошибку, не сравнивайте ошибки простым ==, если вы используете wrapping. Вместо этого используйте errors.Is.

    Временные файлы и директории в тестах

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

  • используйте t.TempDir()
  • Полезные команды go test

  • go test ./... — прогнать все пакеты рекурсивно
  • go test -run TestName — запуск тестов по шаблону
  • go test -v — подробный вывод
  • go test -count=1 — отключить кеш результатов (полезно при отладке)
  • go test -cover ./... — покрытие кода тестами
  • Документация по флагам:

  • Команда go test (документация cmd/go)
  • Отладка: как быстрее находить проблему

    Отладка обычно идёт от самого дешёвого к самому сильному инструменту.

    Слой 1: компилятор, go test, gofmt

  • компилятор Go часто даёт очень точные сообщения об ошибках
  • если поведение странное, сначала запустите тесты
  • всегда держите код отформатированным (go fmt ./...), чтобы легче читать диффы и быстрее замечать ошибки
  • Слой 2: печать состояния (fmt/log)

    Для быстрых проверок:

  • %v — значение
  • %+v — структуры с именами полей
  • %#v — Go-синтаксис значения (часто полезно для точного сравнения)
  • %T — тип
  • Слой 3: race detector (поиск гонок)

    Если вы позже начнёте писать код с горутинами, важнейший инструмент — race detector.

    Запуск:

    Материал от авторов Go:

  • Статья про race detector
  • Слой 4: отладчик Delve

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

  • Delve (отладчик Go)
  • Минимальный сценарий:

    Дальше в интерактивной консоли Delve обычно используют команды вида break, continue, next, step, print.

    Практические best practices: короткий чек-лист

  • Всегда проверяйте err и возвращайте его наверх, если не можете обработать на месте
  • Добавляйте контекст через fmt.Errorf("...: %w", err), чтобы ошибки было легче понимать
  • Для проверки причин используйте errors.Is, для извлечения типа ошибки — errors.As
  • Не используйте panic для ожидаемых проблем (файл не найден, плохой ввод, сеть)
  • Пишите табличные тесты для функций с набором сценариев
  • В тестах избегайте зависимостей от окружения: используйте t.TempDir()
  • Перед коммитом прогоняйте go test ./... и форматируйте код через go fmt ./...
  • Итоги

    Теперь у вас есть фундаментальные практики, которые делают Go-проекты надёжными:

  • ошибки как значения: создание, wrapping, проверка errors.Is/As
  • тестирование через testing и go test, табличные тесты, полезные флаги
  • отладка от простого к сильному: печать состояния, -count=1, -race, Delve
  • Эти навыки особенно важны, когда вы начнёте собирать мини-проекты из нескольких пакетов: ошибки будут проходить через слои, а тесты помогут не ломать поведение при изменениях.

    6. Конкурентность в Go: goroutine, channels, контексты

    Конкурентность в Go: goroutine, channels, контексты

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

    В Go конкурентность строится вокруг трёх базовых идей:

  • goroutine — лёгкий поток выполнения
  • channels — безопасный обмен данными между горутинами
  • context — стандартный способ отмены, таймаутов и передачи “контекста запроса” по стеку вызовов
  • Официальные источники (как справочник):

  • A Tour of Go: Concurrency
  • Документация пакета context
  • Документация пакета sync
  • Документация пакета time
  • Что такое конкурентность и чем она отличается от параллелизма

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

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

    Go даёт удобные инструменты для конкурентности, а реальный параллелизм зависит от среды выполнения (ядра CPU, планировщик Go).

    Goroutine

    Goroutine — это функция, запущенная конкурентно с помощью ключевого слова go.

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

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

    !Визуально показывает, что goroutine выполняются конкурентно и main завершает программу

    Типичная проблема: гонки данных

    Если две горутины одновременно читают и пишут в общую переменную без синхронизации, возникает гонка данных.

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

  • либо не делите память (передавайте данные через каналы)
  • либо используйте примитивы синхронизации (sync.Mutex, sync.RWMutex)
  • Проверять гонки помогает race detector, который вы уже видели в теме отладки:

    Channels

    Channel (канал) — типизированная “труба” для передачи значений между горутинами.

    Канал создаётся через make(chan T) и используется операторами:

  • отправка: ch <- value
  • получение: v := <-ch
  • Небуферизованный канал: синхронизация через встречу

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

  • отправителя, пока кто-то не прочитает
  • получателя, пока кто-то не отправит
  • Это делает небуферизованный канал одновременно:

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

    Буферизованный канал создаётся так: make(chan T, capacity).

    Правила блокировки:

  • отправка блокируется, когда буфер заполнен
  • получение блокируется, когда буфер пуст
  • Сравнение каналов

    | Свойство | Небуферизованный make(chan T) | Буферизованный make(chan T, n) | |---|---|---| | Отправка | ждёт получателя | ждёт только если буфер полон | | Получение | ждёт отправителя | ждёт только если буфер пуст | | Типичное применение | синхронизация и “handoff” | очередь задач, ограничение нагрузки |

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

    Канал закрывают отправители, чтобы сообщить получателям: “данных больше не будет”. Закрытие делает close(ch).

    Правила:

  • закрывать должен тот, кто производит значения
  • в закрытый канал отправлять нельзя (будет паника)
  • читать из закрытого канала можно, и когда значения закончатся, чтение вернёт zero value
  • Для проверки “канал ещё открыт” используют форму “comma ok”:

    Range по каналу

    Частый паттерн: читать значения, пока канал не закрыт.

    Цикл завершится автоматически, когда канал закрыт и значения закончились.

    !Помогает понять блокировки и что означает close

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

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

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

    Для простых таймаутов можно использовать time.After, который возвращает канал, в который придёт значение спустя заданное время.

    Sync.WaitGroup: как дождаться горутин

    Когда вы запускаете несколько горутин, часто нужно дождаться их завершения. Для этого есть sync.WaitGroup.

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

  • Add(n) вызывайте до запуска горутин
  • Done() обычно делают через defer, чтобы не забыть
  • Wait() блокирует текущую горутину, пока счётчик не станет нулём
  • Паттерн worker pool: ограничиваем параллельную обработку

    Worker pool — набор воркеров (горутин), которые читают задачи из канала и обрабатывают их. Это помогает:

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

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

  • jobs <-chan int означает “канал только для чтения”
  • results chan<- int означает “канал только для записи”
  • close(jobs) сообщает воркерам, что задач больше не будет
  • отдельная горутина ждёт WaitGroup и закрывает results, чтобы range results завершился
  • Context: отмена, дедлайны и “контекст запроса”

    Пакет context — стандарт Go для сценариев, где нужно:

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

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

  • context.Background() — корневой контекст для main, инициализации, тестов
  • context.WithCancel(parent) — контекст с ручной отменой
  • context.WithTimeout(parent, d) — контекст с таймаутом
  • context.WithDeadline(parent, t) — контекст с дедлайном
  • Главное правило: функции WithCancel/WithTimeout/WithDeadline возвращают cancel (функцию отмены), и её нужно вызывать, обычно через defer cancel().

    Пример: отмена через WithCancel

    Здесь ctx.Done() — канал, который “закрывается” при отмене или по таймауту. Это стандартный механизм остановки работы.

    !Показывает, как отмена останавливает несколько конкурентных задач

    Пример: таймаут через WithTimeout

    ctx.Err() объясняет причину завершения контекста (например, превышен дедлайн или была ручная отмена).

    Context и сетевые запросы

    Один из самых важных практических кейсов: отменять HTTP-запросы по таймауту.

    Context values

    В context можно хранить значения через context.WithValue, но это инструмент для редких случаев. Практическое правило:

  • для бизнес-данных используйте явные параметры функций
  • в context кладите только “сквозные” данные уровня инфраструктуры (например, request id)
  • Документация и рекомендации есть в описании пакета:

  • Документация пакета context
  • Как связать это с предыдущими темами курса

  • Из темы про ошибки: конкурентные функции обычно возвращают ошибки через канал результатов или через отдельный канал ошибок, а в верхнем уровне (main) вы решаете, что логировать и как завершаться.
  • Из темы про структуры и интерфейсы: воркер может принимать интерфейс зависимости (например, io.Reader или ваш Storage), и это облегчает тестирование.
  • Из темы про тестирование: для конкурентного кода особенно полезны табличные тесты и go test -race.
  • Итоги

    Теперь у вас есть рабочий набор для конкурентных программ на Go:

  • goroutine: запуск конкурентной работы через go
  • channels: обмен данными и синхронизация, буферизация, close, range, “comma ok”
  • select: ожидание нескольких каналов, таймауты
  • sync.WaitGroup: корректное ожидание завершения горутин
  • context: отмена и таймауты как стандарт, ctx.Done() и ctx.Err(), интеграция с HTTP
  • 7. Практика: создание CLI и простого HTTP-сервиса (мини-проекты)

    Практика: создание CLI и простого HTTP-сервиса (мини-проекты)

    Вы уже знаете основы синтаксиса Go, умеете работать со срезами/картами/файлами, структурами и интерфейсами, писать тесты и обрабатывать ошибки, а также знакомы с конкурентностью и context. В этом уроке вы соберёте это в два мини-проекта:

  • CLI-утилита: принимает аргументы и флаги, читает файл, печатает результат, корректно сообщает об ошибках
  • HTTP-сервис: отдаёт JSON, принимает параметры, использует context, логирует и корректно завершает работу
  • Цель урока — увидеть, как темы курса соединяются в прикладные программы, которые уже напоминают реальную разработку.

    Полезные ссылки-справочники:

  • Документация пакета flag
  • Документация пакета os
  • Документация пакета bufio
  • Документация пакета net/http
  • Документация пакета encoding/json
  • Документация пакета context
  • Общая структура учебного репозитория

    Сделаем один модуль и две точки входа.

    Пример структуры:

    Создайте модуль:

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

    Мини-проект CLI: утилита для статистики текста

    Сделаем утилиту gtool, которая:

  • принимает путь к файлу как аргумент
  • поддерживает флаг -mode со значениями lines или words
  • печатает число строк или слов
  • возвращает ненулевой код выхода при ошибке
  • Пакет с логикой: internal/textstat

    internal/textstat/textstat.go:

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

  • Используется интерфейс io.Reader, а не *os.File, чтобы логику было легко тестировать и переиспользовать
  • Ошибки возвращаются наверх как error
  • Режимы вынесены в тип Mode и константы, чтобы уменьшить количество магических строк
  • Точка входа: cmd/gtool/main.go

    cmd/gtool/main.go:

    Ключевые практики для CLI:

  • Флаги парсятся через flag.Parse(), а позиционные аргументы читаются через flag.Arg(i) и flag.NArg()
  • Ошибки и подсказки печатаются в os.Stderr, чтобы стандартный вывод оставался чистым для пайпов
  • Код выхода задаётся через os.Exit(code)
  • Запуск CLI

    Из корня модуля:

    Или сборка:

    Как связать CLI с темами курса

  • Из темы про файлы: os.Open и defer f.Close()
  • Из темы про ошибки: возвращаем ошибки из Count, а решение как завершать программу — в main
  • Из темы про интерфейсы: используем io.Reader, что упрощает тестирование
  • Мини-проект HTTP: простой JSON-сервис

    Сделаем сервис apiserver с двумя эндпоинтами:

  • GET /health возвращает {"status":"ok"}
  • GET /count?mode=words считает слова, но берёт текст из тела запроса
  • Почему текст из тела в GET спорный дизайн, но полезен для учебной задачи:

  • не нужно строить HTML-форму или отдельный клиент
  • легко показать чтение r.Body и обработку ошибок
  • В реальных API для такого чаще делают POST.

    Обработчики: internal/api/handlers.go

    internal/api/handlers.go:

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

  • r.Context() даёт context.Context, который автоматически отменяется, если клиент ушёл
  • http.MaxBytesReader ограничивает размер тела запроса
  • Ответы сериализуются через encoding/json
  • Ошибки для клиента оформляются как JSON
  • > Для настоящих API обычно используют http.Server таймауты чтения и middleware, но для мини-проекта достаточно показать базовые идеи.

    Точка входа: cmd/apiserver/main.go

    cmd/apiserver/main.go:

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

  • Сервер запускается в горутине, чтобы main мог ждать сигнал завершения
  • signal.Notify ловит SIGINT и SIGTERM
  • srv.Shutdown(ctx) даёт graceful shutdown: новые соединения не принимаются, активным дают время завершиться
  • Простейшее логирование оформлено как обёртка logRequests
  • !Жизненный цикл сервиса и корректное завершение

    Проверка HTTP-сервиса

    Запуск:

    Проверка /health:

    Проверка /count:

    Ожидаемый ответ похож на:

  • {"mode":"words","count":2}
  • Как довести мини-проекты до более реального уровня

    Ниже идеи, которые хорошо тренируют материал прошлых статей.

  • Добавить тесты для internal/textstat на strings.NewReader и табличные сценарии
  • В HTTP-обработчиках возвращать разные коды ошибок более последовательно
  • Добавить флаг -addr для apiserver через flag
  • Применить go test -race ./... и убедиться, что нет гонок
  • Итоги

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

  • тонкий main в cmd/..., который настраивает зависимости и управляет завершением
  • логика вынесена в пакеты internal/...
  • ошибки возвращаются наверх и оформляются на границе приложения
  • в HTTP-сервисе используются net/http, JSON-ответы, context и graceful shutdown