Основы языка программирования Go (Golang)

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

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

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

Добро пожаловать в мир Go! Если вы читаете эту статью, значит, вы решили освоить один из самых востребованных и эффективных языков программирования современности. Go (или Golang) был разработан в компании Google такими легендами индустрии, как Роб Пайк, Кен Томпсон и Роберт Гризмер. Их цель была амбициозна: создать язык, который сочетал бы в себе производительность C++, простоту чтения кода Python и эффективность разработки.

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

Почему именно Go?

Прежде чем мы начнем установку, давайте кратко разберем, почему Go стал языком облачной инфраструктуры (Docker и Kubernetes написаны на нем).

  • Простота. В спецификации языка всего около 25 ключевых слов. Выучить синтаксис можно за выходные.
  • Производительность. Go — компилируемый язык. Это значит, что ваш код превращается в машинные инструкции, понятные процессору напрямую, без виртуальных машин.
  • Статическая типизация. Компилятор найдет большинство глупых ошибок еще до того, как вы запустите программу.
  • Встроенная конкурентность. Go умеет выполнять тысячи задач одновременно, не «пожирая» всю память компьютера.
  • Установка окружения

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

    Шаг 1: Скачивание и установка

    Перейдите на официальный сайт языка: The Go Programming Language.

    * Windows: Скачайте MSI-инсталлятор и следуйте инструкциям мастера установки. * macOS: Скачайте PKG-файл или используйте Homebrew: brew install go. * Linux: Скачайте архив tar.gz и распакуйте его в /usr/local, добавив путь к бинарным файлам в переменную окружения PATH.

    Шаг 2: Проверка установки

    Откройте терминал (или командную строку) и введите команду:

    Если вы видите что-то вроде go version go1.21.0 darwin/arm64, поздравляю — Go установлен корректно!

    Шаг 3: Выбор редактора кода

    Хотя код можно писать и в блокноте, для комфортной работы рекомендую использовать Visual Studio Code (VS Code). Это бесплатный редактор, для которого существует официальное расширение Go. Оно обеспечит подсветку синтаксиса, автодополнение и автоматическое форматирование кода.

    Ваша первая программа: Hello, World!

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

    Откройте файл и напишите следующий код:

    !Структура базовой программы на Go

    Разбор кода

  • package main: Эта строка сообщает компилятору, что этот файл должен быть скомпилирован как исполняемая программа, а не как общая библиотека.
  • import "fmt": Мы подключаем пакет fmt (format), который содержит функции для форматирования текста, включая вывод на экран.
  • func main() { ... }: Это точка входа. Когда вы запускаете программу, выполнение всегда начинается с функции main.
  • Запуск программы

    В терминале, находясь в папке с файлом, выполните:

    Команда go run компилирует и сразу запускает вашу программу. Вы должны увидеть вывод: Привет, мир!.

    Если вы хотите создать исполняемый файл (например, .exe для Windows), используйте команду go build main.go.

    Переменные: хранение данных

    Программы работают с данными. Чтобы сохранить данные в памяти, мы используем переменные. В Go переменные — это именованные области памяти определенного типа.

    !Переменные как типизированные контейнеры для данных

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

    В Go есть два основных способа создать переменную.

    #### 1. Полная форма (с ключевым словом var)

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

    #### 2. Краткая форма (только внутри функций)

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

    Обратите внимание на оператор :=. Он означает «создать новую переменную и присвоить ей значение». Если переменная уже создана, использовать := нельзя, нужно использовать обычное =.

    Базовые типы данных

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

    Целые числа (Integers)

    Для хранения целых чисел используются типы int, int8, int16, int32, int64. Самый часто используемый — просто int. Его размер зависит от разрядности вашего процессора (32 или 64 бита).

    Диапазон значений для знакового целого числа определяется формулой:

    Где — допустимое значение переменной, а — количество бит, выделенных под этот тип данных (например, 8, 16, 32 или 64).

    Например, для типа int8 () диапазон будет от до .

    Числа с плавающей точкой (Floats)

    Для дробных чисел используются float32 и float64. В современном программировании стандартом де-факто является float64 из-за высокой точности.

    Булев тип (Boolean)

    Тип bool может принимать только два значения: true (истина) или false (ложь). Он используется в условиях и циклах.

    Строки (Strings)

    Строки в Go — это последовательность байтов. Они заключаются в двойные кавычки ". Go имеет полную поддержку Unicode (UTF-8) «из коробки», поэтому вы можете смело использовать эмодзи или кириллицу.

    Нулевые значения (Zero Values)

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

    | Тип данных | Нулевое значение | | :--- | :--- | | int, float | 0 | | bool | false | | string | "" (пустая строка) |

    Пример:

    Константы

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

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

    Комментарии

    Хороший код должен быть понятен, но иногда нужны пояснения. Комментарии игнорируются компилятором и служат только для людей.

    * // — однострочный комментарий (используется чаще всего). / ... */ — многострочный комментарий.

    Заключение

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

    В следующей статье мы научимся управлять потоком выполнения программы: использовать условные операторы if и циклы for, чтобы наши программы стали по-настоящему умными.

    2. Управляющие конструкции, функции и идиоматичная обработка ошибок

    Управляющие конструкции, функции и идиоматичная обработка ошибок

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

    Сегодня мы превратим наш код из простого списка инструкций в интеллектуальный алгоритм. Мы разберем, как Go управляет потоком выполнения, почему в языке нет цикла while, как функции могут возвращать несколько значений и почему отсутствие исключений (exceptions) — это не баг, а фича.

    Условный оператор if

    Логика любой программы строится на ветвлениях: «Если условие истинно, сделай это, иначе — то». В Go для этого используется конструкция if.

    Синтаксис очень похож на C или Java, но с важным отличием: круглые скобки вокруг условия не нужны.

    If с инициализацией

    Go поддерживает уникальную возможность: вы можете объявить переменную прямо внутри условия if. Эта переменная будет видна только внутри блока if и соответствующего else. Это позволяет не засорять область видимости временными переменными.

    Switch: элегантная замена множеству if

    Когда условий становится слишком много, цепочка if-else превращается в нечитаемую «лестницу». Здесь на помощь приходит switch.

    В отличие от C++ или Java, в Go не нужно писать break в конце каждого кейса. Go автоматически выходит из switch после выполнения первого подходящего условия. Это предотвращает частые ошибки случайного «проваливания» в следующий блок.

    Switch без условия

    Вы можете использовать switch вообще без выражения после ключевого слова. В таком случае это просто более чистый способ записи длинной цепочки if-else.

    Цикл for: один цикл, чтобы править всеми

    В Go есть только одно ключевое слово для циклов — for. Разработчики языка решили, что while и do-while избыточны, так как for может покрыть все сценарии.

    !Логическая структура цикла for

    1. Классический цикл (как в C)

    2. Цикл в стиле while

    Если пропустить инициализацию и пост-действие, for превращается в while.

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

    Если убрать вообще все условия, цикл будет выполняться вечно (пока вы не прервете его командой break или не выключите программу).

    Функции

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

    Объявление функции начинается с ключевого слова func, затем идет имя, список параметров с типами и, при необходимости, возвращаемые типы.

    Простая математическая функция

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

    Где — длина гипотенузы, а и — длины катетов.

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

    Это одна из «киллер-фич» Go. Функция может возвращать не одно, а сколько угодно значений. Это кардинально меняет подход к написанию кода, особенно при обработке ошибок.

    Идиоматичная обработка ошибок

    В Go нет исключений (try-catch-finally). Создатели языка посчитали, что исключения делают поток управления неявным и сложным для отслеживания. Вместо этого ошибки в Go — это значения.

    Типичная функция в Go возвращает два значения: результат работы и ошибку. Ошибка имеет тип error.

    * Если все прошло хорошо, ошибка равна nil (пустое значение). * Если что-то сломалось, ошибка содержит информацию о проблеме.

    Пример безопасного деления

    > «Ошибки — это не что-то, что нужно скрывать или обрабатывать в отдельном блоке кода. Это часть нормального потока выполнения программы». — Effective Go

    Этот паттерн if err != nil вы будете встречать в Go-коде постоянно. Он заставляет программиста явно решать, что делать с ошибкой в месте её возникновения.

    Отложенный вызов: defer

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

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

    Если в функции несколько defer, они выполняются в порядке LIFO (Last In, First Out — последним пришел, первым ушел), как стопка тарелок.

    Заключение

    Мы рассмотрели фундамент логики в Go. Теперь вы знаете, что:

  • if может инициализировать переменные.
  • switch не требует break.
  • for — единственный цикл на все случаи жизни.
  • Функции могут возвращать несколько значений.
  • Ошибки обрабатываются явно через проверку if err != nil.
  • Этих инструментов уже достаточно, чтобы писать сложные алгоритмы. В следующей статье мы перейдем к структурам данных, которые делают Go невероятно мощным: массивам, слайсам и мапам (картам).

    3. Составные типы данных: слайсы, карты (maps), структуры и указатели

    Составные типы данных: слайсы, карты (maps), структуры и указатели

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

    Сегодня мы переходим на следующий уровень. Мы разберем инструменты, которые делают Go мощным языком для бэкенд-разработки: указатели, массивы, слайсы, карты (maps) и структуры. Эти типы данных позволят вам моделировать сложные системы и эффективно управлять памятью.

    Указатели: карта сокровищ в памяти

    Многие новички боятся указателей, особенно если пришли из языков вроде Python или JavaScript, где они скрыты «под капотом». В Go указатели открыты, но безопасны. Давайте разберем их без лишней сложности.

    Что такое указатель?

    Представьте, что переменная — это дом, в котором живут данные. У этого дома есть адрес (например, «Улица Пушкина, д. 10»). Указатель — это записка, на которой написан этот адрес. Имея записку, вы можете найти дом и изменить то, что находится внутри.

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

    !Визуализация того, как указатель ссылается на ячейку памяти с данными

    Операторы & и *

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

  • & (амперсанд) — взятие адреса. Позволяет узнать, где в памяти лежит переменная.
  • * (звездочка) — разыменование. Позволяет получить доступ к значению, лежащему по этому адресу.
  • Зачем это нужно?

    Главная причина использования указателей — эффективность. Когда вы передаете большую структуру данных в функцию, Go по умолчанию создает её полную копию. Если структура занимает 100 МБ, программа потратит время и память на копирование. Передав указатель, вы передаете лишь крошечный адрес (обычно 8 байт на 64-битных системах).

    Массивы и Слайсы

    В Go есть два типа для хранения последовательностей данных: массивы (Arrays) и слайсы (Slices). Важно понимать разницу между ними.

    Массивы (Arrays)

    Массив — это последовательность элементов фиксированной длины. Размер массива является частью его типа. Это значит, что [5]int и [10]int — это совершенно разные типы данных.

    Из-за фиксированного размера массивы в Go используются редко. Они служат фундаментом для более гибкой структуры — слайсов.

    Слайсы (Slices)

    Слайс (или «срез») — это динамическое «окно» в массив. Он может менять свой размер и является основным инструментом работы со списками в Go.

    Слайс состоит из трех компонентов:

  • Pointer — указатель на первый элемент внутри базового массива.
  • Length (длина) — количество элементов, доступных в слайсе сейчас.
  • Capacity (емкость) — сколько всего элементов помещается в базовом массиве, начиная с указателя.
  • !Внутреннее устройство слайса: указатель на массив, длина и емкость

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

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

    #### Добавление элементов: append

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

    Важно: Функция append возвращает новый слайс. Если в базовом массиве закончилось место (Length достигла Capacity), Go автоматически создаст новый, больший массив, скопирует туда данные и вернет слайс, указывающий на новое место.

    #### Срезы слайсов

    Вы можете «отрезать» кусок от слайса или массива, используя синтаксис [low:high]. Граница low включается, а high — нет.

    Карты (Maps)

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

    Эффективность поиска в карте описывается формулой временной сложности:

    Где — время поиска элемента, а обозначает константное время. Это значит, что поиск элемента в карте из 10 записей и в карте из 10 миллионов записей займет примерно одинаковое (мгновенное) время.

    Работа с картами

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

    Проверка существования ключа

    Что будет, если запросить ключ, которого нет? Go вернет нулевое значение для типа данных значения (например, 0 для int). Но как отличить «пользователю 0 лет» от «пользователя не существует»?

    Используйте идиому «comma ok»:

    Важно: Порядок обхода карты в цикле for в Go намеренно сделан случайным. Никогда не полагайтесь на порядок элементов в map.

    Структуры (Structs)

    Если массивы и карты хранят однотипные данные, то структуры позволяют объединять данные разных типов в единый логический объект. Это основа объектно-ориентированного программирования в Go.

    Объявление и создание

    Мы создаем новый тип данных, описывающий сущность (например, Пользователя).

    Анонимные структуры

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

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

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

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

    Заключение

    Сегодня мы значительно расширили наш арсенал. Теперь вы знаете:

    * Как работать с памятью напрямую через указатели. * В чем разница между жесткими массивами и гибкими слайсами. * Как мгновенно находить данные с помощью карт (maps). * Как моделировать реальные объекты с помощью структур.

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

    4. Интерфейсы и методы: реализация полиморфизма и композиции

    Интерфейсы и методы: реализация полиморфизма и композиции

    В предыдущей статье мы научились создавать собственные типы данных с помощью структур. Мы создали «скелет» наших объектов: описали, из каких полей они состоят. Но в реальном мире объекты не просто хранят данные, они действуют. Собака не просто имеет поле «порода», она умеет лаять. Файл не просто имеет имя, его можно прочитать или закрыть.

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

    Методы: поведение объектов

    В классических объектно-ориентированных языках (как Java или C#) методы находятся внутри классов. В Go классов нет, но есть методы. Метод — это обычная функция, у которой есть особый аргумент — получатель (receiver).

    Получатель указывается в круглых скобках между ключевым словом func и именем функции.

    Объявление метода

    Давайте создадим структуру Rectangle (прямоугольник) и научим её вычислять свою площадь.

    В этом примере r внутри функции Area работает как копия переменной rect. Мы можем читать из неё данные.

    Pointer Receiver vs Value Receiver

    Это один из самых важных моментов в Go. Получатель может быть двух видов:

  • Value Receiver (по значению): func (r Rectangle) ...
  • Pointer Receiver (по указателю): func (r *Rectangle) ...
  • В чем разница?

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

    Когда вы используете Pointer Receiver, метод получает ссылку на оригинал. Изменения внутри метода повлияют на исходную переменную.

    Пример изменения состояния:

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

    Интерфейсы: контракты поведения

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

    В Go интерфейсы реализованы неявно (implicit). Это отличает Go от Java или PHP, где нужно писать implements. В Go, если тип имеет все методы, описанные в интерфейсе, он автоматически реализует этот интерфейс.

    !Визуализация того, как разные структуры подходят под один интерфейс благодаря наличию нужного метода

    Определение интерфейса

    Представим, что у нас есть не только прямоугольники, но и круги. Мы хотим вычислять площадь для любой фигуры.

    Математическая формула площади круга:

    Где — искомая площадь, — математическая константа (примерно 3.14159), а — радиус круга.

    Опишем интерфейс Shape (Фигура):

    Теперь реализуем этот интерфейс для круга:

    Полиморфизм в действии

    Полиморфизм (от греч. «много форм») позволяет нам писать код, который работает с разными типами данных единообразно.

    Поскольку и Rectangle, и Circle реализуют интерфейс Shape, мы можем создать функцию, которая принимает Shape. Ей все равно, что именно ей передадут, главное, чтобы у этого «чего-то» был метод Area().

    Это делает код невероятно гибким. Если завтра вы добавите структуру Triangle, вам не нужно будет переписывать функцию printArea. Достаточно просто добавить метод Area к треугольнику.

    Пустой интерфейс (Empty Interface)

    В Go есть специальный интерфейс, у которого нет методов: interface{}. Начиная с версии Go 1.18, для него есть алиас any.

    Поскольку у него нет требований к методам, любой тип в Go реализует пустой интерфейс. Это аналог Object в Java или void* в C.

    Хотя это кажется удобным, злоупотреблять any не стоит. Использование пустого интерфейса отключает проверку типов на этапе компиляции, что лишает вас главного преимущества Go — типобезопасности.

    Утверждение типа (Type Assertion)

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

    Композиция интерфейсов

    Go не поддерживает наследование классов («Квадрат наследуется от Прямоугольника»). Вместо этого Go использует композицию. Мы можем создавать сложные интерфейсы, объединяя простые.

    Классический пример из стандартной библиотеки io:

    Если ваша структура имеет методы Read и Write, она автоматически становится ReadWriter. Это позволяет строить сложные системы из маленьких, независимых блоков.

    > «Чем больше интерфейс, тем слабее абстракция». — Роб Пайк, Go Proverbs

    Это означает, что лучше создавать маленькие интерфейсы (1-2 метода), чем огромные «божественные» интерфейсы, которые сложно реализовать.

    Практический пример: Логгер

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

  • Определяем интерфейс:
  • Реализуем консольный логгер:
  • Реализуем файловый логгер (имитация):
  • Используем в бизнес-логике:
  • Благодаря интерфейсам, наш Service ничего не знает о файлах или консоли. Он просто знает, что у logger есть метод Log. Это делает код тестируемым и расширяемым.

    Заключение

    Сегодня мы разобрали ключевые концепции, которые превращают набор инструкций в архитектуру:

    * Методы позволяют привязывать функции к данным. * Интерфейсы позволяют абстрагироваться от конкретных типов. * Полиморфизм дает возможность работать с разными объектами через единый интерфейс. * Композиция заменяет сложное наследование простым объединением поведений.

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

    5. Основы конкурентности: эффективная работа с горутинами и каналами

    Основы конкурентности: эффективная работа с горутинами и каналами

    Добро пожаловать на одну из самых захватывающих лекций курса! Мы добрались до «киллер-фичи» языка Go — конкурентности. Именно благодаря ей Go стал языком номер один для написания высоконагруженных серверов, микросервисов и облачных платформ.

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

    Сегодня мы научим наши программы делать несколько дел сразу. Мы разберем, что такое горутины, как они общаются через каналы и почему девиз Go звучит так: «Не общайтесь, используя общую память; используйте общую память, общаясь».

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

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

    Конкурентность (Concurrency) — это структура программы. Это способность разбивать программу на независимые части, которые могут* выполняться в произвольном порядке. Параллелизм (Parallelism) — это физическое* выполнение нескольких задач одновременно на разных ядрах процессора.

    Представьте себе кофейню.

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

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

    Где — ускорение работы программы, — доля кода, которую можно распараллелить (от 0 до 1), а — количество доступных процессоров (ядер). Эта формула показывает, что даже с бесконечным числом ядер ускорение ограничено той частью программы, которая должна выполняться последовательно.

    Горутины (Goroutines)

    В других языках (Java, C++, Python) для одновременного выполнения задач используются потоки операционной системы (OS Threads). Они тяжелые: каждый поток потребляет 1-2 МБ памяти, и их создание занимает время.

    В Go используются горутины. Это «легковесные потоки», управляемые не операционной системой, а самим Go (его планировщиком). Горутина занимает всего пару килобайт памяти при старте. Вы можете запустить миллионы горутин на обычном ноутбуке, и он не зависнет.

    !Сравнение тяжеловесных потоков и легких горутин

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

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

    Если вы запустите этот код, то, скорее всего, увидите только Привет из main!. Почему?

    Дело в том, что программа на Go завершается, как только завершается функция main. Наша горутина sayHello была запущена, но main завершилась быстрее, чем планировщик успел выделить время для sayHello.

    Чтобы увидеть результат, нам нужно «подождать». Самый плохой способ — использовать time.Sleep. Правильный способ — использовать sync.WaitGroup.

    Синхронизация с WaitGroup

    Пакет sync содержит примитивы для синхронизации. WaitGroup — это счетчик. Мы увеличиваем его, когда запускаем горутину, и уменьшаем, когда она завершается. Функция main будет ждать, пока счетчик не станет равен нулю.

    Обратите внимание: мы передаем указатель &wg, так как все горутины должны работать с одним и тем же объектом WaitGroup.

    Каналы (Channels)

    Горутины позволяют запускать задачи одновременно. Но как передать результат вычислений из одной горутины в другую? Использовать глобальные переменные опасно — это приводит к «гонкам данных» (race conditions).

    Go предлагает элегантное решение — каналы. Каналы — это типизированные трубы, по которым горутины могут отправлять и принимать данные.

    !Гоферы обмениваются данными через канал

    Создание и использование

    Канал создается функцией make с ключевым словом chan.

    Оператор стрелки <- указывает направление данных:

    * ch <- v : Отправить значение v в канал ch. * v := <-ch : Прочитать значение из канала ch и сохранить в v.

    Пример обмена данными

    Блокировка и Deadlock

    По умолчанию каналы небуферизированные. Это значит, что:

  • Отправка ch <- v блокирует горутину, пока кто-то не прочитает из канала.
  • Чтение <-ch блокирует горутину, пока кто-то не запишет в канал.
  • Это позволяет использовать каналы для синхронизации без явных блокировок.

    Однако, если вы попытаетесь читать из канала, в который никто никогда не напишет (или писать, когда никто не читает) в рамках одной горутины, вы получите Deadlock (взаимную блокировку).

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

    Каналу можно дать емкость (буфер). В такой канал можно писать без блокировки, пока буфер не заполнится.

    Закрытие каналов и Range

    Отправитель может закрыть канал, чтобы сообщить: «Данных больше не будет». Для этого используется функция close(ch).

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

    Но чаще всего для чтения из канала используют цикл for range. Он автоматически читает данные, пока канал открыт, и завершается, когда канал закрывают.

    Select: управление множеством каналов

    Что делать, если нам нужно ждать ответа от нескольких каналов одновременно? Например, мы запрашиваем данные из двух серверов и хотим обработать тот ответ, который придет первым.

    Для этого существует конструкция select. Она похожа на switch, но для каналов.

    В этом примере select выберет тот case, который будет готов первым. Если готовы несколько, Go выберет один из них случайным образом.

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

    Generator

    Функция, которая запускает горутину и возвращает канал, из которого можно читать результаты.

    Fan-in (Мультиплексор)

    Функция, которая объединяет данные из нескольких каналов в один.

    Worker Pool

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

    Заключение

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

  • Горутины — это дешевый способ делать дела одновременно.
  • WaitGroup помогает дождаться завершения всех горутин.
  • Каналы позволяют безопасно передавать данные.
  • Select управляет ожиданием множества событий.
  • Конкурентность — мощный инструмент, но он требует дисциплины. Главное правило: всегда знайте, как и когда ваша горутина завершится. Утечка горутин (когда они висят в памяти вечно) — частая проблема новичков.

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