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

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

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

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

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

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

Почему Go?

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

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

Установка окружения

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

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

  • Перейдите на официальный сайт go.dev.
  • Выберите версию для вашей операционной системы (Windows, macOS или Linux).
  • Скачайте установочный файл и следуйте стандартным инструкциям инсталлятора.
  • Шаг 2: Проверка установки

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

    Если вы видите сообщение, похожее на go version go1.21.0 darwin/arm64 (версия может отличаться), значит, установка прошла успешно.

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

    Традиционно изучение любого языка начинается с программы, которая выводит приветствие на экран. Давайте разберем структуру программы на Go.

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

    !Структура базовой программы на Go: пакет, импорты и главная функция

    Разбор кода по строкам

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

    Переменные и типы данных

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

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

    Существует два основных способа создания переменных.

    1. Использование ключевого слова var

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

    2. Краткое объявление (Short Declaration)

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

    > Важно: Оператор := можно использовать только внутри функций. На уровне пакета (вне функций) всегда нужно использовать var.

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

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

    | Тип | Описание | Пример | | :--- | :--- | :--- | | int | Целое число (размер зависит от архитектуры процессора) | 42, -5 | | float64 | Число с плавающей точкой (двойная точность) | 3.14, 0.001 | | string | Строка (набор символов) | "Go is great" | | bool | Логический тип | true, false |

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

    Рассмотрим простую арифметическую операцию:

    где — сумма, — первое слагаемое, — второе слагаемое. В Go это будет выглядеть так:

    Константы

    Константы объявляются с помощью ключевого слова const. Их значение должно быть известно на этапе компиляции и не может быть изменено в процессе работы программы.

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

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

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

    Синтаксис if в Go похож на другие C-подобные языки, но имеет важное отличие: условие не нужно заключать в круглые скобки (), но фигурные скобки {} обязательны.

    Также if позволяет выполнить короткую инструкцию перед проверкой условия:

    Цикл for

    В Go есть только один цикл — for. Он универсален и заменяет собой циклы while и do-while из других языков.

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

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

    Цикл while (аналог):

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

    !Логика выполнения цикла for в Go

    Оператор switch

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

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

    Заключение

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

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

    Попробуйте выполнить домашние задания ниже, чтобы закрепить материал!

    2. Работа с данными: массивы, слайсы, карты (maps) и структуры

    Работа с данными: массивы, слайсы, карты (maps) и структуры

    Приветствую вас во второй части курса «Основы программирования на языке Go». В предыдущей статье мы научились настраивать окружение, писать «Hello, World!» и работать с простыми типами данных, такими как числа и строки. Однако в реальной разработке данных обычно много, и они имеют сложную структуру. Нам нужно хранить списки пользователей, каталоги товаров, настройки конфигурации и многое другое.

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

    Массивы (Arrays)

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

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

    Главная особенность массива в Go: его размер является частью его типа. Это означает, что [5]int и [10]int — это два совершенно разных типа данных, и вы не можете просто так присвоить один другому.

    Особенности массивов

  • Фиксированный размер. Вы не можете изменить размер массива после его создания. Если вам нужно больше места, придется создать новый массив и скопировать туда данные.
  • Значение, а не ссылка. В Go массивы передаются по значению. Если вы передадите массив в функцию, она получит его полную копию. Изменения внутри функции не затронут оригинальный массив.
  • Из-за своей жесткости массивы в чистом виде используются в Go довольно редко. Чаще всего они служат основой для более гибкой структуры — слайсов.

    Слайсы (Slices)

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

    !Структура слайса: указатель на массив, длина и вместимость

    Структура слайса

    Технически слайс — это маленькая структура, содержащая три поля:

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

    Слайс можно создать несколькими способами:

    При взятии среза используется математическая логика полуинтервалов. Если мы пишем arr[low:high], то длина нового слайса вычисляется по формуле:

    где — итоговая длина слайса (количество элементов), — верхняя граница индекса (не включается в выборку), а — нижняя граница индекса (включается в выборку).

    Динамическое расширение: append

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

    Как это работает под капотом? Когда вы добавляете элемент, Go проверяет, хватает ли вместимости (capacity) базового массива. Если места достаточно, просто увеличивается длина. Если места нет, Go создает новый, более крупный массив (обычно в 2 раза больше), копирует туда все старые данные и добавляет новый элемент. Именно поэтому важно присваивать результат append обратно в переменную слайса.

    Карты (Maps)

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

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

    Тип карты записывается как map[KeyType]ValueType.

    Особенности работы с картами

  • Порядок не гарантирован. При переборе карты (range) порядок элементов будет случайным. Не полагайтесь на то, что элементы будут выводиться в том же порядке, в котором вы их добавили.
  • Нулевое значение. Если вы запросите ключ, которого нет в карте, Go вернет «нулевое значение» для типа данных значения (0 для int, "" для string, false для bool).
  • Чтобы проверить, существует ли ключ на самом деле, используется идиома «comma ok»:

    Здесь переменная ok будет иметь тип bool и значение true, если ключ существует, и false, если его нет.

    Удаление элементов

    Для удаления используется встроенная функция delete:

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

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

    Определение структуры

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

    Теперь мы можем создавать переменные этого типа:

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

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

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

    Структуры могут содержать другие структуры. Это позволяет моделировать сложные объекты реального мира. Кроме того, структуры в Go часто используются для описания формата JSON-ответов от веб-серверов.

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

    Итерация по коллекциям: range

    Для перебора всех рассмотренных типов (массивы, слайсы, карты) в Go используется ключевое слово range в цикле for.

    Для слайсов и массивов:

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

    Для карт:

    Заключение

    Мы рассмотрели четыре кита работы с данными в Go:

    * Массивы — жесткие контейнеры фиксированного размера. * Слайсы — гибкие и динамические «окна» в массивы, основной инструмент Go-разработчика. * Карты — быстрые хранилища пар «ключ-значение». * Структуры — способ создания собственных типов данных, описывающих сложные объекты.

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

    А пока — проверьте свои знания в небольшом тесте ниже!

    3. Функции, указатели и методы: основы организации кода

    Функции, указатели и методы: основы организации кода

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

    В этой статье мы разберем три фундаментальных концепции, которые превращают набор инструкций в структурированное приложение:

  • Функции — строительные блоки логики.
  • Указатели — инструменты для эффективной работы с памятью.
  • Методы — способ наделить данные поведением.
  • Понимание этих тем критически важно, так как именно здесь Go проявляет свою философию простоты и эффективности.

    Функции в Go

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

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

    Базовый синтаксис функции выглядит так:

    Рассмотрим простую функцию сложения:

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

    Множественный возврат значений

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

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

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

    Иногда мы не знаем заранее, сколько аргументов будет передано в функцию. Для этого используются вариативные функции (variadic functions). Примером такой функции является fmt.Println.

    Чтобы объявить такой параметр, перед типом ставятся три точки ...:

    Внутри функции переменная nums воспринимается как слайс типа []int.

    Указатели (Pointers)

    Указатели часто пугают новичков, но в Go они реализованы гораздо безопаснее и проще, чем в C или C++. Указатель — это переменная, которая хранит не само значение (например, число 42), а адрес ячейки памяти, где это значение лежит.

    Зачем нужны указатели?

  • Эффективность. Передача больших структур данных в функции по значению требует их полного копирования. Передача указателя копирует только адрес (обычно 8 байт на 64-битных системах).
  • Изменяемость. Если вы хотите, чтобы функция изменила оригинальную переменную, ей нужно знать, где эта переменная находится в памяти.
  • !Визуализация того, как указатель ссылается на ячейку памяти с данными

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

    * Оператор & (амперсанд) получает адрес переменной. Оператор (звездочка) обозначает тип указателя или «разыменовывает» его (получает значение по адресу).

    Передача по значению и по ссылке

    Рассмотрим математическую модель эффективности использования указателей. Допустим, у нас есть структура данных размером байт. Мы передаем её в функцию раз.

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

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

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

    где — размер указателя (обычно 8 байт на 64-битной архитектуре). Если велико (например, 1 мегабайт), то будет значительно больше , что делает использование указателей предпочтительным для производительности.

    Методы

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

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

    Метод объявляется так же, как функция, но перед именем метода добавляется специальный параметр — получатель (receiver).

    Получатель значения vs Получатель указателя

    Это один из самых важных моментов в Go. При объявлении метода вы должны решить, как передавать получателя: как значение или как указатель.

    1. Value Receiver (Получатель значения)

    В этом случае метод работает с копией структуры. Если вы измените поля r внутри метода, оригинальная структура не изменится. Используйте этот вариант, если метод только читает данные или структура очень маленькая.

    2. Pointer Receiver (Получатель указателя)

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

    Пример использования:

    Конструкторы

    В Go нет специальных методов-конструкторов, как __init__ в Python или constructor в Java. Вместо этого принято создавать обычные функции, имя которых начинается с New.

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

    Организация кода и лучшие практики

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

  • Именуйте функции понятно. Имя должно быть глаголом или описывать действие (например, CalculateTotal, FetchData).
  • Возвращайте ошибки. Если функция может завершиться неудачей, она должна возвращать error последним значением.
  • Не злоупотребляйте указателями. Если тип данных маленький (например, int, bool или небольшая структура), передавайте его по значению. Это безопаснее и проще для сборщика мусора.
  • Используйте методы для поведения. Если функция логически связана с какой-то структурой, сделайте её методом этой структуры.
  • Заключение

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

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

    А пока закрепите материал, выполнив задания ниже!

    4. Интерфейсы и полиморфизм: гибкая архитектура приложений

    Интерфейсы и полиморфизм: гибкая архитектура приложений

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

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

    Что такое интерфейс?

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

    Интерфейс в Go — это именованный набор сигнатур методов. Интерфейс говорит не о том, чем является объект (структурой, числом или строкой), а о том, что этот объект умеет делать.

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

    Давайте представим, что мы разрабатываем игру, где есть разные персонажи и объекты, способные издавать звуки. Нам не важно, кто это — собака, человек или инопланетянин. Нам важно лишь то, что они умеют «говорить».

    Определим интерфейс Speaker:

    Этот код означает: «Любой тип данных, у которого есть метод Speak, возвращающий строку, автоматически считается Speaker».

    Неявная реализация (Duck Typing)

    Это ключевая особенность Go. В таких языках, как Java или C#, вам нужно явно указывать: class Dog implements Speaker. В Go этого делать не нужно.

    Если тип имеет все методы, перечисленные в интерфейсе, он реализует этот интерфейс автоматически. Это часто называют «утиной типизацией» (Duck Typing): «Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка».

    Рассмотрим пример:

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

    !Визуализация принципа полиморфизма: разные структуры соответствуют одному интерфейсу

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

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

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

    Функция MakeItSpeak принимает любой тип, удовлетворяющий интерфейсу Speaker. Это делает архитектуру приложения невероятно гибкой. Если завтра вы добавите структуру Cat, вам не придется переписывать функцию MakeItSpeak.

    Математическая логика реализации

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

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

    Где — набор методов интерфейса, — набор методов типа, а символ означает, что множество методов интерфейса является подмножеством методов типа. То есть тип должен содержать все методы интерфейса (и, возможно, иметь свои собственные дополнительные методы).

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

    В Go существует интерфейс, который не содержит ни одного метода:

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

    Пустой интерфейс используется, когда мы не знаем заранее, какой тип данных нам придет. Начиная с версии Go 1.18, для него существует удобный алиас (псевдоним) — any.

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

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

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

    Синтаксис выглядит так: x.(T), где x — переменная интерфейса, а T — конкретный тип, который мы ожидаем там увидеть.

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

    Type Switch (Переключатель типов)

    Если переменная интерфейса может содержать множество разных типов, и для каждого нужна своя логика, удобно использовать type switch:

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

    Go поощряет создание маленьких интерфейсов. Хорошей практикой считается интерфейс с одним-двумя методами. Если вам нужен более сложный интерфейс, вы можете собрать его из нескольких простых. Это называется встраиванием (embedding).

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

    Теперь любой тип, который имеет методы Read и Write, автоматически реализует интерфейс ReadWriter.

    Лучшие практики

  • Принимайте интерфейсы, возвращайте структуры. Это «золотое правило» Go. Ваши функции должны быть гибкими к входящим данным (принимать интерфейс), но возвращать конкретные типы, чтобы вызывающий код мог сам решать, как их использовать.
  • Не создавайте интерфейсы заранее. В Java часто сначала пишут интерфейс, а потом класс. В Go интерфейсы создают тогда, когда появляется реальная необходимость в абстракции (например, когда у вас появляется второй тип, который должен вести себя так же, как первый).
  • Чем меньше, тем лучше. Интерфейс с одним методом (например, Reader) реализовать гораздо проще, чем интерфейс с десятью методами. Это повышает вероятность повторного использования кода.
  • Заключение

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

    Мы разобрали: * Как объявлять и реализовывать интерфейсы. * Что такое неявная реализация. * Как использовать полиморфизм. * Как работать с пустым интерфейсом any и извлекать типы обратно.

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

    А пока закрепите знания об интерфейсах, выполнив задания ниже!

    5. Конкурентность в Go: горутины, каналы и синхронизация

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

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

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

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

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

    > «Конкурентность — это композиция независимо выполняемых вычислений. Параллелизм — это одновременное выполнение вычислений».

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

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

    Для оценки теоретического ускорения программы при распараллеливании часто используют закон Амдала:

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

    Горутины (Goroutines)

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

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

    Преимущества горутин:

  • Легкость. Занимают всего пару килобайт памяти на старте.
  • Масштабируемость. Можно запустить сотни тысяч горутин на обычном ноутбуке.
  • Умное планирование. Планировщик Go (Go Scheduler) автоматически распределяет тысячи горутин по нескольким реальным потокам процессора.
  • !Визуализация работы M:N планировщика Go: множество горутин мультиплексируются на меньшее количество потоков ОС

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

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

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

    Ловушка главной функции

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

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

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

    У WaitGroup есть три основных метода: * Add(delta int): увеличивает счетчик активных задач. * Done(): уменьшает счетчик на 1 (обычно вызывается через defer). * Wait(): блокирует выполнение текущей горутины, пока счетчик не станет равен нулю.

    Каналы (Channels)

    Горутины позволяют запускать код одновременно. Но как им обмениваться данными? Использовать общие переменные опасно (об этом ниже). Философия Go гласит:

    > «Не общайтесь, используя общую память; используйте общую память, общаясь».

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

    !Метафора канала: передача данных между горутинами подобно передаче предметов по трубе

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

    Каналы создаются с помощью make:

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

    Блокировка

    По умолчанию каналы небуферизированные. Это означает: * Отправка ch <- v блокирует горутину, пока кто-то не прочитает данные. * Чтение <-ch блокирует горутину, пока кто-то не отправит данные.

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

    Пример:

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

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

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

    Отправитель может закрыть канал, чтобы сообщить, что данных больше не будет. Получатель может проверить это или использовать цикл for range.

    Оператор select

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

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

    Состояние гонки и Мьютексы

    Иногда нам все же приходится работать с общей памятью (например, счетчиками или кэшами). Если две горутины пытаются одновременно изменить одну переменную, возникает состояние гонки (Race Condition).

    Представьте простой инкремент переменной: count++. На машинном уровне это три операции:

  • Прочитать значение count из памяти.
  • Прибавить 1.
  • Записать новое значение в память.
  • Если две горутины сделают это одновременно, они могут прочитать одно и то же старое значение, и в итоге счетчик увеличится только на 1, а не на 2. Это называется проблемой потерянного обновления.

    sync.Mutex

    Чтобы защитить участок кода от одновременного доступа, используется Мьютекс (Mutex — Mutual Exclusion, взаимное исключение).

    Использование Lock() и Unlock() гарантирует, что код между ними выполняется атомарно (неделимо) по отношению к другим горутинам.

    Заключение

    Конкурентность в Go — это мощный инструмент, который делает язык уникальным. Мы разобрали:

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

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

    А пока — закрепите материал, выполнив задания ниже!