Linux и Go. Эффективное низкоуровневое программирование. [Олег Иванович Цилюрик] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

Linux: эффективная многопроцессорность
Используем Go
Проект книги
Автор: Олег Цилюрик

Редакция 4.120
07.02.2024г.

© 2024
1

Оглавление

Предисловие.................................................................................................................................7
Предназначение и целевая аудитория....................................................................................8
Код примеров и замеченные опечатки................................................................................10
Чего нет в этой книге............................................................................................................12
Соглашения и выделения, принятые в тексте....................................................................13
Напоминание.........................................................................................................................14
Источники информации........................................................................................................15
Часть 1. Инструментарий языка Go.........................................................................................16
Предыстория Go....................................................................................................................17
Разворачиваем экосистему Go.............................................................................................26
Неформально о синтаксисе Go............................................................................................66
Новости от последних версий............................................................................................126
Часть 2. Конкурентность и многопроцессорность...............................................................132
Процессоры в Linux............................................................................................................133
Параллелизм и многопроцессорность...............................................................................147
Масштабирование...............................................................................................................174
Часть 3. Некоторые примеры и сравнения............................................................................196
Осваиваемся в синтаксисе Go............................................................................................197
Структуры данных, типы и их методы..............................................................................216
Строки, руны и UNICODE.................................................................................................223
Элементы функционального программирования.............................................................233
Скоростные и другие сравнения языков...........................................................................244
Многопроцессорные параллельные вычисления.............................................................258
Заключение...............................................................................................................................280
Об авторе...................................................................................................................................281

2

Содержание

Предисловие.................................................................................................................................7
Предназначение и целевая аудитория....................................................................................8
Код примеров и замеченные опечатки................................................................................10
Замечание о версиях.........................................................................................................11
Чего нет в этой книге............................................................................................................12
Соглашения и выделения, принятые в тексте....................................................................13
Напоминание.........................................................................................................................14
Источники информации........................................................................................................15
Часть 1. Инструментарий языка Go.........................................................................................16
Предыстория Go....................................................................................................................17
«Отцы-основатели» о целях и мотивации…..................................................................17
Применимость: беглый взгляд.........................................................................................17
Go, C, C++, Java, Python, Rust ….....................................................................................18
Управление памятью и истоки ненадёжности..........................................................24
Источники информации...................................................................................................24
Разворачиваем экосистему Go.............................................................................................26
Создание среды.................................................................................................................26
Стандартная инсталляция...........................................................................................26
Версии среды................................................................................................................28
Альтернативы...............................................................................................................28
«Самая последняя» версия..........................................................................................29
Смена версий................................................................................................................33
Проверяем: простейшая программа................................................................................34
Простейшее приложение.............................................................................................36
Библиотеки статические и динамические.................................................................37
Компиляция или интерпретация.................................................................................38
Выбор: GoLang или GCC ?.........................................................................................38
Инфраструктура GoLang..................................................................................................39
Команды go...................................................................................................................39
Переменные окружение..............................................................................................41
Переменная окружения GOPATH..........................................................................45
Переменная окружения GOTOOLDIR..................................................................46
Переменная окружения GOARCH и GOOS..........................................................47
Платформы, переносимость и кросс-компиляция...............................................47
Стиль кодирования (автоформатирование — fmt)....................................................49
Сборка приложений (build).........................................................................................51
Сценарии на языке Go (run)........................................................................................51
Загрузка проектов из сети (get)...................................................................................51
Репозиторные системы...........................................................................................52
Установка проектов.................................................................................................54
Утилиты GoLang (tool)................................................................................................55
Утилиты компиляции..............................................................................................57
Связь с кодом C (Cgo)......................................................................................................59
Сторонний и дополнительный инструментарий...........................................................61
Интерактивный отладчик Delve.................................................................................63
Источники информации...................................................................................................65
Неформально о синтаксисе Go............................................................................................66
Типы данных.....................................................................................................................69
Переменные.......................................................................................................................71
Повторные декларации и переприсвоения................................................................73
Константы..........................................................................................................................74
3

Агрегаты данных..............................................................................................................75
Массивы и срезы..........................................................................................................76
Двухмерные массивы и срезы....................................................................................79
Структуры.........................................................................................................................80
Таблицы (хэши).................................................................................................................82
Динамическое создание переменных.............................................................................83
Конструкторы и составные литералы........................................................................84
Операции...........................................................................................................................85
Функции............................................................................................................................87
Вариативные функции.................................................................................................91
Стек процедур завершения.........................................................................................92
Обобщённые функции.................................................................................................93
Функции высших порядков........................................................................................94
Встроенные функции...................................................................................................96
Объектно ориентированное программирование............................................................97
Методы..........................................................................................................................98
Множество методов................................................................................................99
Встраивание и агрегирование..............................................................................100
Функции как объекты...........................................................................................101
Интерфейсы................................................................................................................102
Именование интерфейсов.....................................................................................105
Контроль интерфейса............................................................................................105
Обработка ошибочных ситуаций..................................................................................106
Структура пакетов (библиотек) Go...............................................................................108
Функция init................................................................................................................112
Импорт для использования побочных эффектов....................................................112
Некоторые полезные и интересные стандартные пакеты...........................................113
Пакет runtime..............................................................................................................113
Форматированный ввод-вывод.................................................................................114
Строки и пакет strings................................................................................................115
Строчные литералы...............................................................................................122
Большие числа............................................................................................................123
Автоматизированное тестирование..........................................................................124
Источники информации.................................................................................................124
Новости от последних версий............................................................................................126
Модули.............................................................................................................................126
Дженерики.......................................................................................................................127
Источники информации.................................................................................................131
Часть 2. Конкурентность и многопроцессорность...............................................................132
Процессоры в Linux............................................................................................................133
Процессоры, ядра и гипертриэдинг..............................................................................135
Загадочная нумерация процессоров.............................................................................136
Управление процессорами Linux..................................................................................139
Аффинити маска........................................................................................................139
Как происходит диспетчирование в Linux..............................................................141
Приоритеты nice....................................................................................................143
Приоритеты реального времени..........................................................................144
Источники информации.................................................................................................146
Параллелизм и многопроцессорность...............................................................................147
Эволюция модели параллелизма...................................................................................147
Параллельные процессы и fork................................................................................147
Потоки ядра и pthread_t POSIX................................................................................149
Потоки C++............................................................................................................151
Сопрограммы — модель Go..........................................................................................153
4

Параллелизм в Go......................................................................................................154
Сопрограммы — как это выглядит...............................................................................155
Возврат значений функцией.....................................................................................157
Ретроспектива: сопрограммы в C++........................................................................158
Каналы.............................................................................................................................158
Функциональные замыкания в сопрограммах.............................................................162
Примитивы синхронизации...........................................................................................164
Конкурентность и параллельность...............................................................................169
Источники информации.................................................................................................172
Масштабирование...............................................................................................................174
Планирование активности сопрограмм...................................................................174
Испытательный стенд....................................................................................................175
Микрокомпьютеры (Single-Board Computers).........................................................175
Рабочие десктопы......................................................................................................178
Серверы промышленного класса.............................................................................179
Масштабирование в реальном мире.............................................................................180
1-я попытка …............................................................................................................181
2-й подход к снаряду…..............................................................................................186
О числе потоков исполнения.........................................................................................192
Источники информации.................................................................................................195
Часть 3. Некоторые примеры и сравнения............................................................................196
Осваиваемся в синтаксисе Go............................................................................................197
Утилита echo...................................................................................................................197
Итерационное вычисление вещественного корня.......................................................198
Вычисление числа π.......................................................................................................200
Вычисления неограниченной точности........................................................................202
Случайная последовательность и её моменты............................................................203
Обсчёт параметров 2D выпуклых многоугольников...................................................205
TCP клиент-сервер..........................................................................................................209
Тривиальный WEB сервер.............................................................................................213
Порядок итераций для map: сюрприз...........................................................................214
Источники информации.................................................................................................215
Структуры данных, типы и их методы..............................................................................216
Массивы и срезы............................................................................................................216
Многомерные срезы и массивы....................................................................................220
Функции с множественным возвратом.........................................................................220
Строки, руны и UNICODE.................................................................................................223
Символы, байты и руны.................................................................................................223
Изменение содержимого строк.....................................................................................224
Палиндромы....................................................................................................................225
Регулярные выражения..................................................................................................229
Источники информации.................................................................................................232
Элементы функционального программирования.............................................................233
Функциональные замыкания.........................................................................................233
Карринг............................................................................................................................238
Рекурсия...........................................................................................................................239
Рекурсия с кэшированием.........................................................................................240
Чистые функции.............................................................................................................242
Источники информации.................................................................................................243
Скоростные и другие сравнения языков...........................................................................244
Алгоритмические задачи для сравнения......................................................................244
Некоторые известные алгоритмы..................................................................................244
Числа Фибоначчи.......................................................................................................245
Пузырьковая сортировка...........................................................................................248
5

Ханойская башня.......................................................................................................252
Решето Эратосфена....................................................................................................255
Многопроцессорные параллельные вычисления.............................................................258
Скорость активации параллельных ветвей..................................................................258
Гонки................................................................................................................................262
Защита критических данных....................................................................................265
Многопроцессорный брутфорс.....................................................................................268
Каналы в сопрограммах.................................................................................................273
Таймеры...........................................................................................................................274
Тикеры.........................................................................................................................275
Когда не нужно злоупотреблять многопроцессорностью...........................................276
Источники информации.................................................................................................279
Заключение...............................................................................................................................280
Об авторе...................................................................................................................................281

6

Предисловие
Хорошая книга не дарит тебе откровение,
хорошая книга укрепляет тебя в твоих
самостоятельных догадках.
Андрей Рубанов, «Хлорофилия»
Этот текст не будет изложением того, как писать программы — это во множестве описано в
литературе. И это не будет учебным руководством по языку Go, об этом тоже во множестве
написано и издано в последние годы (то что понравилось — я привожу ниже в источниках
информации)... хотя мы и затронем частично синтаксические основы нового языка. Здесь будет
только изложение того, как добиваться максимальной производительности проекта, используя для
этой цели предоставленные возможности аппаратурой («железом»). Для этого придётся
углубиться (в 1-й части) в некоторые особенности использования языка Go, начиная с
разворачивания среды программирования, и, далее, в те синтаксические особенности, которые
понадобятся в последующем рассмотрении.

7

Предназначение и целевая аудитория
Авангардная музыка, авангардная живопись ...
идеальный способ быть музыкантом
и художником, не умея ни играть, ни рисовать.
А.Гаррос и А.Евдокимов «Новая жизнь»
Первоначально этот текст начал создаваться в 2012-2013 г.г. на заказ руководства крупной
международной софтверной компании GlobalLogic, как учебный курс, ориентированный на
программных разработчиков компании, планировавших переориентацию некоторых проектов на
Go. Позже планы учебного курса потеряли актуальность, а текст был опубликован под лицензией
общественного достояния для свободного доступа.
Но за прошедшее время инструментарий Go активно развивался и изменялся, выложенный текст
заметно устарел, кроме того он не был ориентирован на эффективное использование
многопроцессорных архитектур, по которым накопилось достаточно много нового материала.
Поэтому текст послужил только основой для радикальной его переделки 2022 года.
Этот текст создавался в расчёте на более-мене опытного разработчика программного
обеспечения, имеющих за плечами один или несколько завершённых проектов, предпочтительно
на C или C++1. … или на студента, только вникающего в профессию, которому «одинаково на чём
писать» и который подбирает свой любимый инструментарий для будущих побед.
Этот текст никак не может быть использован как систематический учебник или справочник по
языку Go — для этого есть формализованные описания, часть из которых перечислены в
указанных источниках информации. Точно так же — это не учебник программирования и того,
как решать задачи на языке Go. При написании ставилась скромная цель: дать разработчику, в
достаточной мере владеющему языками и C и C++ в Linux краткое руководство по адаптации
этих своих знаний применительно к языку Go. Поэтому будет постоянно, везде где это возможно,
приводиться сравнения кодов и конструкций языков Go с C и C++: этот путь сравнения — самый
быстрый быстрый путь освоения Go. Некоторые примеры кода (в обсуждении и в архиве) будут
даваться в параллельных вариантах: на C/C++ и Go. Этот метод сравнения с C и C++, будет
первым принципом, на котором будет строится всё изложение. Тем более (в смысле простоты
адаптации), что Go является прямым продолжением языковой линии языка C и, в некоторой
степени (скорее «от противного»), C++, а у истоков его разработки непосредственно стояли люди
из числа первоначальных разработчиков C и операционной системы UNIX: Роб Пайк и Кен
Томпсон.
А второй принцип: дать максимально много примеров работающих законченных приложений на
Go, использующих наиболее широкий спектр возможностей языка (и дополняющей его
экосистемы). На этом основан весь текст: дать максимально много примеров использования
конструкций Go в разнообразных ситуациях, даже в ущерб точности и соответствию формальной
документации языка. По программному коду самих этих примеров будут, даже временами без
каких-либо комментариев, дополнительно рассыпаны всякие мелкие «вкусности» из языка, на
которые внимательный практик сразу же обратит внимание. И отметит их себе в копилку...
Второй, и может быть главной, целью этой работы было во всех деталях изучение естественного
параллелизма, вводимого синтаксисом Go, а также то, каким образом эти параллельные
механизмы проявляются в многопроцессорной архитектуре. Этому, фактически, посвящена
вторая половина материала.
… ну и, наконец, что может быть и не очевидно, этот текст, в теперешнем его виде, не про Go

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

Наконец, последняя часть текста вообще несколько выпадает из общего развития повествования,
и посвящена примерам реализации некоторых характерных и известных задач на Go.
Предлагаются сравнительные решения их на Go и на других языках программирование (C и C++
главным образом) — такие сравнения позволяют отчётливо проследить различия в
1

8

В самом первоначальном варианте этот конспект, или проект книги, так и назывался: «Go для программистов C и
C++» … он где-то так и гуляет по Интернет с 2014-го года под таким названием.

идеологических подходах в разрешении аналогичных задач.

9

Код примеров и замеченные опечатки
Всегда пишите код так, будто сопровождать
его будет склонный к насилию психопат,
который знает, где вы живете.
Martin Golding
Все примеры кодов, обсуждаемые в тексте, содержатся в архиве, прилагаемом к тексту. Все
примеры были испробованы и проверены, и могут быть воспроизведены из архива. Все коды (как
и весь текст в целом) ориентированы на операционную систему Linux. Язык Go имеет
высочайшую степень переносимости и совместимости, но под операционными системами
семейства Windows (или других) может потребоваться внести изменения в примеры из архива,
как правило незначительные.
Все примеры кода отрабатывались и выверялись на компьютерах 3-х групп производительности и
целевого применения. Это показатель потенциала всей инфраструктуры GoLang в смысле
масштабировании. Эти 3 группы, на которых отработаны все примеры:
1. от самых малых, «игрушечных» однокристальных образцов SoC (System-on-a-Chip Система на кристалле)…
2. несколько (4-5-6 экземпляров, в разное время) традиционных десктопов архитектуры
x86_64, с различающимися характеристиками по числу процессорных ядер,
дистрибутивом Linux, версиями этих дистрибутивов…
3. сервер промышленного класса DELL PowerEdge R420, с 2-мя установленными
физическими процессорами Intel Xeon® E5-2470 v2, по 20 ядер каждый, в итоге 40
процессоров.
Рассмотрение переносимости создаваемого программного обеспечения между различными
архитектурами настолько актуально и интересно, что мы отдельно вернёмся к нему позже, к
концу нашего разбирательства, в отдельной главе посвящённой масштабированию систем.
Примеры программного кода сгруппированы по разделам текста в каталоги, поэтому всегда будет
указываться имя каталога в архиве (например, xxx) и имя файла примера кода в этом каталоге
(например, zzz.go). Некоторые каталоги могут содержать подкаталоги, тогда указывается и
подкаталог для текущего примера (например, xxx/yyy). Большинство каталогов (вида xxx)
содержат одноимённые файлы (вспомогательные) вида xxx.hist — в них содержится
скопированные с терминала результаты выполнения примера (журнал, протокол работы) в
хронологической последовательности развития этого примера, показывающие как этот пример
должен выполняться, а в более сложных случаях здесь же могут содержаться команды,
показывающие порядок компиляции и сборки примеров архива.
Все примеры неоднократно проверялись и перепроверялись компиляцией и выполнением. В
отношении стилистики написания кода примеров у кого-то могут возникнуть некоторые
замечания. Но! … Код писался на протяжении нескольких лет (как минимум 7-8), на протяжении
которых шла работа с текстом, когда активно, когда не очень… За это время менялись даже
стандарты языков, появлялись конструкции которых не было раньше, иногда примеры
переписывались, иногда нет. Во-вторых, примеры написаны на разных языках: C, C++, Python,
Go… Когда, в своей профессиональной работе, в периоды когда пишешь на C++ — то теряешь
беглость Python, когда в другой период пишешь на POSIX API Linux — то теряешь
выразительность C++, а о беглости в Go приходится забыть… Но весь код перед вами, и вы
можете самостоятельно украсить стилистику на свой вкус.
Примеры листингов кода, по мнению автора, — это самая ценная часть проделанной работы.
Потому что это инструмент для подтверждения сказанного и отправная точка для дальнейших
самостоятельных экспериментов. Весь остальной объем книги — это просто пространный
комментарий к этим примерам и листингам кода. Архив примеров листингов кода выложен для
свободного скачивания и использования по ссылке https://zip.bhv.ru/9785977517416.zip.
Конечно, и при самой тщательной выверке и вычитке, не исключены недосмотры и опечатки в

10

объёмном тексте, могут проскочить мало внятные стилистические обороты и подобное. Да и в
процессе вёрстки текста может быть привнесено много любопытного... О замеченных таких
дефектах я прошу сообщать по электронной почте olej.tsil@gmail.com, и я был бы признателен за
любые указанные недостатки рукописи, замеченные ошибки, или высказанные пожелания по её
доработке.
Примечание относительно версий программных продуктов: Поскольку текст книги накапливался и обновлялся на
протяжении, как минимум, 8 лет, то версии устанавливаемых и тестируемых программных продуктов, ясное дело, не
один раз сменились. Я показываю во многих местах инсталляцию программных инструментов (пакетов), которые мы
используем. После первичной инсталляции пакета Linux а). его нельзя заново проинсталлировать через некоторое
время для демонстрации и б). версия уже установленных программных пакетов будет обновляться вместе с
обновлением программного обеспечения всего дистрибутива Linux. Поэтому было принято решение не пытаться
обновлять листинги инсталляций по тексту — цифры могут отличаться, и у вас быть совсем другие версии, но
инсталляции и версии мы показываем только для того, чтобы у вас был путеводитель по созданию своего рабочего
окружения.

Замечание о версиях
Если на клетке слона прочтешь надпись: буйвол,
— не верь глазам своим.
«Плоды раздумья» Козьма Прутков
Текст книги, начиная от первоначального конспекта для узкого круга изучающих, расширялся,
накапливался и корректировался на протяжении достаточно многих лет. За это время сменились
не одна версии любого из упоминаемых инструментов. Первоначально казалось хорошей идеей, в
примерах выполняемых действий и команд, отслеживать изменения версий инструментария. Но
позже практика показала что это дурная идея: невозможно по каждому «чиху» версии показать
команды инсталляции инструмента (просто потому что он уже инсталлирован в предыдущей
версии). Более того, попытка «угнаться» за версиями в таком обстоятельном объёме текста только
приводит к путанице и ошибкам, но ничего не добавляет содержательно.
Поэтому, если вы видите в тексте цифры версий, которые, пусть даже значительно, отличаются от
того что вы видите на экране своего монитора — это не должно смущать, логика поведения
остаётся неизменной в зависимости от конкретики цифр.
Но там же, где поведение GoLang поменялось по логике своего поведения, вследствие
естественного развития и модификации инструмента, по возможности, сделаны попытки везде
исправить текст в соответствии с актуальным положением вещей.

11

Чего нет в этой книге
Никто не обнимет необъятного!
Козьма Прутков
Вместе с языком Go бурно развивается номенклатура пакетов Go от разнообразных сторонних
разработчиков — окружение языка Go. Если для для языка C библиотечная инфраструктура
(стандарт POSIX) стабилизировалась десятилетиями, как и стандарт C++ (от Б.Страуструпа), то
для Go номенклатура доступных целевых пакетов меняется, расширяется от месяца к месяцу. Она
настолько широка и разнообразна (каждому разработчику интересно что-то своё из областей
деятельности), что пытаться охватить это разнообразие бессмысленно … если мы углубимся в
область использования A, то почему мы обойдём вниманием область B? Поэтому информация о
целевых пакетах Go и сторонних инструментах полностью и сознательно исключена из
рассмотрения2. Текущее состояние пакетов вы можете найти на https://pkg.go.dev/ (Discover
Pakages).
По мнению автора, в языке Go есть 2 предмета, выделяющих его, в положительную сторону, на
фоне многих других языков. А именно:
1. Конкурентное программирование и возможности параллельного многопроцессорного
выполнения;
2. Расширенные возможности
символьных строк.

обработки

мультиязычной

текстовой

информации,

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

2

12

Некоторые из самых употребимых (и самых старых, устоявшихся) пакетов мы будем использовать в своих
примерах кода на Go без всяких дополнительных объяснений. Вы можете также использовать их: as is.

Соглашения и выделения, принятые в тексте
Для большей ясности при чтении текста, он размечен шрифтами по функциональной
принадлежности выделяемого фрагмента. Применена широко используемая, устоявшаяся в
других публикациях и интуитивно ясная разметка:
– Отдельные ключевые понятия и термины в тексте, на которые нужно обратить особое
внимание, будут выделены курсивом.
– Тексты программных листингов, вывод в ответ на консольные команды пользователя размечен
моноширинным шрифтом.
– Так же моноширинным шрифтом (прямо в тексте) могут быть выделены: имена команд,
программ, файлов ... т.е. всех терминов, которые должны оставаться неизменяемыми, например:
/proc, clang, ./myprog, ...


Ввод пользователя в консольных командах (сами команды, или ответы пользователя в диалоге
… то что мы набираем на клавиатуре), кроме того что это листинг, выделены жирным
моноширинным шрифтом, чтобы отличать от ответного вывода программ (или системы) в
диалогах (который набран просто моноширинным шрифтом).
– В показанных многочисленных листингах ввода-вывода терминала (как это обычно и принято
в публикациях по UNIX/Linux) команды от имени ординарного пользователя и от имени
администратора root будут различаться по предшествующему значку приглашения: $ — это
ординарный пользователь, # — это root (это достаточно традиционное соглашение, но не
следует забывать, что оно настраиваемое — в вашей конкретной системе виды приглашений могут
быть другими).
– Интернет-ссылки (URL) по тексту, в том числе и адреса электронной почты, выделяются
жирным написанием (в приводимых библиографиях такое выделение не делается).

13

Напоминание
Язык Go — относительно новый язык со своей экосистемой (GoLang). Он до сих пор очень
динамично развивается: дополняется, уточняется… и изменяется. С регулярностью в несколько
месяцев выходит очередной новый релиз системы. На дату написания этого текста (начало 2024
года) самая последняя стабильная версия (если говорить о состоянии стандартных пакетов:
https://pkg.go.dev/std):
Version: go 1.21.6 Latest Published: Jan 9, 2024, License: BSD-3-Clause
В новых версиях появляется много нового и интересного, чего не хватало в предыдущих. В
частности в версии 1.21 (Релиз Go 1.21 : https://golang-blog.blogspot.com/):
Релиз Go, версия 1.21, выходит через шесть месяцев после Go 1.20. Большинство его изменений
касаются реализации цепочки инструментов, среды выполнения и библиотек. Как всегда, выпуск
поддерживает обещание совместимости Go 1; на самом деле Go 1.21 улучшает это обещание.
Следите за обновлениями системы GoLang!

14

Источники информации
[1] Цукалос М., Golang для профи: работа с сетью, многопоточность, структуры данных и машинное
обучение с Go, изд. «Питер» Спб, 2020 г., 720 стр. — https://rutracker.org/forum/viewtopic.php?t=5929666
[2] Титмус, М. А., Облачный GO : создание надежных сервисов в ненадежных окружениях, изд. «ДМК
ПРЕСС», 2022 г., 417 стр. —
https://mdk-arbat.ru/book/6241942#instock , https://rutracker.org/forum/viewtopic.php?t=6112741
[3] Алан А. А. Донован, Брайан У. Керниган, Язык программирования Go, изд. «Вильямс», 2016 г., 432 стр.
https://rutracker.org/forum/viewtopic.php?t=5222397
[4] Олег Цилюрик, Конспект: язык Go в Linux, 2014 г. — http://flibusta.is/b/510170
[5] Блог о языке программирования Go — https://golang-blog.blogspot.com/
[6] Уроки для изучения Golang — https://golangify.com/
[7] The Go Programming Language Specification, Version of December 15, 2022 — https://go.dev/ref/spec
[8] Standard library, Version: go1.21 — https://pkg.go.dev/std
[9] Подборка проектов для разработки GUI на Go, 6 апреля 2022 —
https://dzen.ru/media/golang/podborka-proektov-dlia-razrabotki-gui-na-go-624d38f4828df173e1426ead

15

Часть 1. Инструментарий языка Go

16

Предыстория Go
Существует великое множество языков
программирования, которые не уступают или
даже превосходят Си по красоте и удобству.
Тем не менее ими никто не пользуется.
Денис Ритчи

«Отцы-основатели» о целях и мотивации…
Эх, знали бы бесстрашные молодые оторвы,
ужасающие своими подвигами сеть, что
активизм во все эпохи разный, а вот старость,
иконы и коты – одинаковые во все времена…
Виктор Пелевин «iPhuck 10»
Go — новый компилируемый язык программирования с естественными параллелизмами,
разработанный компанией Google. Первоначальная разработка Go началась в сентябре 2007 года,
и его непосредственным проектированием занимались Роберт Гризмер, Роб Пайк и Кен Томпсон,
то есть лица непосредственно стоявшие 40 лет назад у истоков языка C и операционной системы
UNIX. Официально язык был представлен в ноябре 2009 года.
Некоторыми из заявленных (и достигнутых, как увидим далее) целей разработки были:
- Создать современную альтернативу языку C (которому более 40 лет) и одновременно избежать
громоздкости и тяжеловесности языка C++;
- Естественным образом отобразить в языке возможность параллельных вычислений в
многопроцессорных (SMP, многоядерных) системах;
- Обеспечить высокую переносимость между операционными системами. На данный момент
поддержка Go осуществляется для операционных систем Linux, FreeBSD, OpenBSD, Mac OS X,
Windows. Как мы увидим вскорости, Go позволяет создавать в Linux вообще автономные
приложения, вообще не использующие интерфейс системных вызовов через стандартную
библиотеку C (libc.so);
Примечание: Хотя Go и реализован практически во всех операционных системах, автор ничего не может сказать о
состоянии дел и особенностях Go в Mac OS X или Windows. Всё наше дальнейшее рассмотрение будет проводиться
только в операционной системе Linux.

- Обеспечить высокую переносимость между между аппаратной архитектурой самых различных
процессорных платформ: i386, amd64, ARM, MIPS, PPC, …
Помимо этих основных целей, явно формулировался целый ряд дополнительных, попутных,
достижение которых мы будем обсуждать по ходу дальнейшего обсуждения…
Ещё одна особенность проекта, отличающего его от многих (если не всех) других языков
программирования — прекрасная документированность проекта: с Go можно садится работать не
располагая никакими иными сторонними источниками справочной информации, кроме
оригинальных страниц документации, размещённой на самом сайте проекта GoLang (ссылки
документации указаны в конце этого раздела).

Применимость: беглый взгляд
Динамику развития Go и огромный задел доступных библиотечных пакетов Go можно оценить,
например, посмотрев в репозитарий, например, (только для одной текущей процессорной
архитектуры X86_64) дистрибутива Fedora (далеко не самый быстро наполняемый новинками
дистрибутив):
$ dnf list golang*
...

17

Имеющиеся пакеты
golang.x86_64
golang-antlr4-runtime-devel.noa
golang-ariga-atlas.x86_64
golang-ariga-atlas-devel.noarch
golang-bazil-fuse-devel.noarch
golang-bin.x86_64
...

1.16.13-1.fc35
4.9.3-1.fc35
0.3.3-1.fc35
0.3.3-1.fc35
0-0.17.20200722gitfb710f7.fc35
1.16.13-1.fc35

updates
updates
updates
updates
fedora
updates

По количеству:
$ dnf list golang* | wc -l
2190

Это демонстрация, кроме прочего, и возможность в поддерживаемых разных аппаратных
архитектурах и простота коросс-компиляции под них, что мы детально рассмотрим вскоре. Но
список впечатляющий!
Несмотря на относительную молодость Go, его уже избрали в качестве инструментария авторы
многих открытых публичных проектов. Здесь собран для ознакомления указатель на несколько
сот реализаций на Go, начиная с простеньких утилит и до комплексных развиваемых и
долгосрочно поддерживаемых проектов: https://code.google.com/p/go-wiki/wiki/Projects. Там же
показаны автономные инструментальные проекты Go-инфраструктуры (от независимых
разработчиков), которые мы даже не сможем упомянуть в связи с ограничениями объёмов.
На Go реализован такой уже широчайше известный и популярный проект как Docker. Как пример
последнего времени: анонсирован крупнейший проект Syncthing — открытое кроссплатформенное приложение (Linux, Mac OS X, Windows, FreeBSD и Solaris, Android), строящееся
по модели клиент-сервер и предназначенное для синхронизации файлов между двумя
участниками (point to point). Проект реализуется на языке Go.
В 2009 Go был признан языком года по версии организации TIOBE.

Go, C, C++, Java, Python, Rust …
Ведь традиция, как ты понимаешь, это не
сохранение пепла, а поддержание огня.
Павел Крусанов «Мёртвый язык»
Вопрос вот в чём: при таком множестве языков программирования (больше сотни, несколько сот),
созданных за 50-60 лет, зачем нужен ещё один язык Go. Попробуем, очень поверхностно, не
акцентируясь и не теряя особенно на то своё время, взглянуть на состояние дел...
Go, так же, как С и С++ является чисто компилирующим языком: в результате компиляции и
связывания (из некоторого числа отдельных объектных файлов — фрагментов будущего
приложения) создаётся единый бинарный исполнимый файл (в Linux это исполнимый ELFформат), пригодный в дальнейшем для многократного выполнения только аппаратными
вычислительными средствами компьютера, без поддержки какой-либо интерпретирующей среды.
Это очень важно отметить, потому как, следуя тенденциям в развитии последних 20-30 лет,
подавляющее большинство из многих десятков языковых сред требуют ту или иную
интерпретацию периода выполнения — это может быть либо виртуальная языковая машина (Java,
Python, Kotlin), либо просто текстуальная интерпретация программного кода (Perl, PHP, Ruby,
Lua, Tcl, ...)3. Таким образом Go заполняет некоторую недостаточно заполненную нишу
инструментариев.
Язык Go, непосредственно по разнообразным высказываниям его отцов-основателей, является
прямым продолжением линии C, то как они же, эти же авторы, сами и спроектировали бы C — но
… «40 лет спустя», с учётом опыта прошедших десятилетий эксплуатации C. (Это как у А. Дюма:
«Три мушкетёра», но только «20 лет спустя».)
В отношении C++ всё заметно сложнее (и об этом будет детальнее ниже), из высказываний
3

18

Это особенно бросается в глаза если сравнить ситуацию с ранним периодом становления и развития IT технологий
(60-е, 70-е, ...), когда практически все языки разработки были чисто компилирующими: Algol, FORTRAN,
COBOL, Pascal.

Р.Пайка:
Корни Go основаны на C и, в более широком смысле, на семействе Algol. Кен Томпсон в шутку
сказал, что Роб Пайк, Роберт Грейнджер и он сам собрались вместе и решили, что они
ненавидят C++. Будь то шутка или нет, Go сильно отличается от C++.

В отношении скоростных показателей (помимо многих других требований, о которых уже
сказано выше и о мы ещё поговорим позже) проектировщики ставили 2 скоростных требования:
1. Скорость компиляции аналогичного кода должен быть выше чем у C.
2. Скорость выполнения скомпилированного кода должна если и уступать скорости своего
эквивалента на C, то незначительно.
В отношении скорости компиляции, синтаксис Go спроектирован так (на уровне лексического
анализа), что позволяет осуществлять очень быструю компиляцию, и, как следствие, даже
непосредственно запускать исходный код Go на выполнение, на манер скриптов — того, как это
происходит в интерпретирующих языках, командой: go run xxx.go. Но это никак не
интерпретация, а неявная скрытая быстрая компиляция с последующим выполнением. К оценкам
скорости компиляции Go и что это даёт в итоге — мы вернёмся позже...
Эксперименты относительно скорости выполнения программ ... дело, в общем, неблагодарное,
потому что в зависимости от типа задачи (кто и на что «заточен») на одних задачах вы будете
получать соотношение «больше чем...», а на других, для ровно тех же языков, «меньше чем...».
Можно только очень грубо говорить о разнице в порядках скорости (в 10 раз, в 100 раз, …). Мы
вернёмся к подобным сравнениям детально позже, ближе к концу книги, когда будет понятно
«что к чему», но пока сделаем хотя бы поверхностные намётки. И для сравнения скорости
эквивалентных приложений для Go соберём простейшее приложение (каталог compare/fibo
архива) на нескольких языках программирования, реализующее хорошо известный рекурсивный
алгоритм вычисления чисел Фибоначчи4, выбрав его как алгоритм с чрезвычайно высокой
степенью роста трудоёмкости от порядка задачи, O(N):
fibo_c.c :
#include
#include
unsigned long fib(int n) {
return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}
int main(int argc, char **argv) {
unsigned num = atoi(argv[1]);
printf("%ld\n", fib(num));
return 0;
}

fibo_cc.cc :
#include
#include
using namespace std;
unsigned long fib(int n) {
return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}
int main(int argc, char **argv) {
unsigned num = atoi(argv[1]);
cout dir
Volume in drive X is Boot
Volume Serial Number is D60A-0DC2
Directory of X:\Users\Default\Downloads
06/20/2018
06/20/2018
01/20/2022
01/20/2022
01/20/2022

02:00 AM

.
02:00 AM

..
03:25 PM
1,054,653 drive-download-20220120T132509Z-001.zip
03:30 PM
1,901,056 hello.32.exe
03:30 PM
2,138,112 hello.64.exe
3 File(s)
5,093,821 bytes
2 Dir(s)
368,934,912 bytes free

X:\Users\Default\Downloads>hello.32.exe

47

ты кто будешь?
> bastard
какое длинное имя ... целых 9 байт
привет, bastard
X:\Users\Default\Downloads>hello.64.exe
ты кто будешь?
> windown
какое длинное имя ... целых 9 байт
привет, windown

А теперь проанализируем то что произошло:
- бинарные файлы подготовлены полностью работой Linux в исполнимом Windows-формате PE и
готовы к запуску в Windows…
- символьная кодировка (кирилица) исходного файла UTF-8 согласована с консольной кодировкой
Windows: CP-866, CP-1251, или что оно там у них … даже при том, что для представления
UNICODE в Windowsиспользуется кодировка UTF-16, а не UTF-8 …
- любопытно, что длина вводимой строки (7 байт) дополнена (9-байт) 2-мя байтами (LF + CR), в
то время, как в Linux строки завершаются только 1-м символом (LF).
Ну и, наконец, сборка под совершенно чужеродную операционную систему и другую
архитектуру (32-бит вместо 64-бит) заняла у меня не более 10 секунд…
Это совершенно невиданная в более ранних языковых инструментах степень переносимости и
мультиплатформенности! Точно с той же лёгкостью мы сможет собирать приложения под
архитектуры ARM, MIPS, PPC и любую другую аппаратную платформу…
Ещё интереснее — используем аппаратные платформы одноплатных микро-компьютеров на
архитектуре ARM. Но компиляцию приложения сделаем в привычном десктопном окружении
Linux на Intel x86_64:
$ GOARCH=arm go build -o hello.32.arm hello.go
$ ls -l *.arm
-rwxrwxr-x 1 olej olej 1880350 мар 23 21:59 hello.32.arm
$ file hello.32.arm
hello.32.arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, Go
BuildID=ZfMUX4fIpDYH_dqrGeIZ/q3f4F1Z0legZmrfeS1Qu/Ozb37t0uutQShZb8-IJl/QT5B9gi5cCsDLo2SLM5f,
not stripped

Теперь копированием бинарного исполнимого ELF-файла переносим его (по сети) в среду
одноплатного Rapsberry Pi :
$ inxi -Cxxx
CPU:
Info: Quad Core model: ARMv7 v7l variant: cortex-a7 bits: 32
type: MCP arch: v7l rev: 5
features: Use -f option to see features bogomips: 256
Speed: 1000 MHz min/max: 600/1000 MHz Core speeds (MHz):
1: 1000 2: 1000 3: 1000 4: 1000
$ lsb_release -a
No LSB modules are available.
Distributor ID:
Raspbian
Description:
Raspbian GNU/Linux 11 (bullseye)
Release: 11
Codename: bullseye
$ uname -a
Linux raspberrypi 5.10.103-v7+ #1530 SMP Tue Mar 8 13:02:44 GMT 2022 armv7l GNU/Linux
$ ./hello.32.arm
ты кто будешь?

48

> вася
какое длинное имя ... целых 9 байт
привет, вася

И точно также, этот же исполнимый файл копируем по сети в совершенно другую архитектуру
Orange Pi One (несовместимый аппаратно с Rapsberry Pi ... для полноты ощущений) :
$ inxi -Cxxx
CPU:
Topology: Quad Core model: ARMv7 v7l variant: cortex-a7 bits: 32
type: MCP arch: v7l rev: 5
features: Use -f option to see features bogomips: 0
Speed: 1008 MHz min/max: 480/1008 MHz Core speeds (MHz):
1: 1008 2: 1008 3: 1008 4: 1008
$ lsb_release -a
No LSB modules are available.
Distributor ID:
Ubuntu
Description:
Ubuntu 20.04.4 LTS
Release: 20.04
Codename: focal
$ uname -a
Linux orangepione 5.15.25-sunxi #22.02.1 SMP Sun Feb 27 09:23:25 UTC 2022 armv7l armv7l armv7l
GNU/Linux
$ ./hello.32.arm
ты кто будешь?
> вася
какое длинное имя ... целых 9 байт
привет, вася
$ hostname
orangepione

Примечание: В ARM-архитектуре из-за отсутствия динамической идентификации устройств PCI стандарта
совместимость чипсетов определяется явно выписанным в сборке системы деревом устройств (DeviceTree,
определенное в DeviceTree Specification), что порождает великое множество вариантов аппаратных семейств,
несовместимых с точки зрения сборок образов операционной системы. Это выходит за рамки наших целей
рассмотрения, но требует, при необходимости, выверять собранные приложения в каждой отдельной конфигурации.

Стиль кодирования (автоформатирование — fmt)
В принципе, синтаксис Go достаточно свободный (но не полностью свободный), и код может
быть записан достаточно произвольным образом (в смысле переносов строк и отступов), хотя это
и не совсем свободное кодирование, как, например, в C/C++ (строку нельзя разорвать и перенести
в произвольном месте)9. В конечном счёте, стиль записи программного кода (code style) является
делом предпочтений, или, ещё чаще, предписывается корпоративными правилами, принятыми в
той или иной компании по разработке программного обеспечения.
Одна из команд менеджера go — команда форматирования (fmt), которая представляет исходные
коды на Go в едином стиле, как его понимают сами разработчики Go. Для демонстрации
используем показанный выше файл исходного кода hello.go (то что написали вручную),
скопировав его предварительно в экземпляр с именем hello.0.go:
$ ls -l hello.0.go
-rw-r--r--. 1 Olej Olej 601 сен 24 21:49 hello.0.go

Трансформируем произвольно записанной форме (как нам самим нравится) файл кода Go
командой форматирования:
9

49

Из кода любой программы на C и C++ можно вообще полностью убрать все переносы строки, при этом код
останется синтаксически верным и будет компилироваться без ошибок. Это и называется: полностью свободный
синтаксис языка программирования. Другая крайность — язык Python, где уровень синтаксической вложенности
определяется величиной отступа оператора в строке. Язык Go занимает некоторое промежуточное положение в
этом ряду. Является ли абсолютно свободный синтаксис языка некоторым преимуществом?

$ go fmt hello.0.go
hello.0.go
$ ls -l hello.0.go
-rw-r--r--. 1 Olej Olej 557 сен 24 21:50 hello.0.go

Как легко видеть, файл программы на Go изменился (смотрим по размеру). Теперь он выглядит
несколько по-иному (сравните с показанным ранее исходным вариантом):
hello.0.go :
package main
/* первая программа
демонстрирующая
синтаксис языка Go */
import (
"fmt"
"os"
)
func main() {
fmt.Println("ты кто будешь?")
fmt.Printf("> ")
буфер := make([]byte, 120)
длина, _ := os.Stdin.Read(буфер) // возвращается 2 значения
Ω := длина
ответ := string(буфер[:Ω-1]) // убрали '\n'
fmt.Printf("какое длинное имя ... целых %d байт\n", Ω)
fmt.Printf("привет, %s\n", ответ)
}

Команда fmt при групповом форматировании фалов кода Go (каким-то странным образом —
скорее всего по содержимому) отбирает и форматирует только те файлы, которые ещё не
подвергались форматированию, то что не соответствует GoLang code style:
$ go fmt *.go
select.go
close.go
buffer.go
bazel0.go
bazel.go
unblock.go

Это при том, что в показанном каталоге гораздо больше файлов исходного кода Go че те, которые
подверглись переформатированию:
$ ls *.go
bazel0.go
bazel.go

buffer.go
close.go

closure1.go
closure2.go

closure3.go
multy.go

once.go
parm.go

result.go
select.go

sleep.go
smp1.go

smp2.go
unblock.go

Примечание: Строго говоря, команда go fmt ничего про звездочку в примере выше не узнает — ее раскрывает
командный интерпретатор bash. Но нас здесь интересует то, что команда форматирования затрагивает (отбирает)
только те файлы кода *.go, которые ранее не были затронуты форматированием (в каком-то смысле это отдаленно
напоминает то, что делает утилита make).

Всё предусмотрено в системе Go! Даже эстетика представления кода Go в едином стиле…
И в завершение обсуждения этой команды, в порядке совета или пожелания: каждый день,
завершая работу с кодом Go, и покидая каталог в котором вели эту работу — выполните команду:
$ go fmt *.go
...

50

Сборка приложений (build)
Большинство команд сборки приложений (build) мы уже неоднократно видели ранее по ходу
текста, и они, в общем, интуитивно понятны. При соблюдении правил инфраструктуры GoLang, в
любой терминальный подкаталог в src можно опуститься, и просто выполнить для сборки
размещённого там проекта (без указания входных-выходных файлов):
$ go build

Симметрично, для удаления результатов сборки выполняем:
$ go clean

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

Сценарии на языке Go (run)
Одной из заявленных целей разработчиков Go была наивысшая скорость компиляции ( то есть
синтаксис языка, не обременяющий компилятор избыточными затратами производительности).
Важной аргументацией этой цели было не только сокращение времени компиляции больших
проектов (в конце концов, многократная компиляция больших проектов делается на
инструментальных компьютерах самой высокой производительности, и с использованием систем
инкрементальной сборки, по типу make, ninja и других). Ещё одним важным дополнительным
аргументом была возможность использования кодовых файлов *.go (особенно небольшого
размера) непосредственно в качестве сценариев в операционной системе Linux (в дополнение к
широко применяемых в этой системе bash/dash/zsh, Perl, Python и др.). Идея такого
сценарного использования кода состоит в том, чтобы а). быстро откомпилировать код без
медленной записи результата в дисковую файловую систему и б). затем однократно выполнить
откомпилированный результат.10
Вот как выглядит это на примере простейшего приложения (каталог архива tools — ещё одна
проверка UTF-8 кодировки — китайский язык) выполнение Go кода без предварительной
компиляции:
tiny.go :
package main
import "fmt"
func main() { fmt.Println("Hello, 世界") }
$ time go run tiny.go
Hello, 世界
real
0m0,257s
user
0m0,298s
sys
0m0,114s

Обратите внимание: мы, якобы, не делаем явно никакой компиляции, мы просто запускаем файл
кода на выполнение, как это было бы, например, в Python, Ruby, или в консольном JavaScript … да
и просто Shell (bash/dash), в конце концов.

Загрузка проектов из сети (get)
Особый интерес, кроме собственно сборки, представляет команда загрузки и установки
программных пакетов из сети — get (распределённый менеджмент программных проектов).
Авторы GoLang расширяют окружение разработки из среды локального компьютера на всю сеть
Интернет, когда любой проект лежащий в сети — его скачивание единообразными средствами и
продолжение развития локально. В этом Go продолжает и развивает идеи: пакетных систем
инсталляции Linux, систем инсталлирования Python ( pip и другие), распределённых
10 Первоначально, в ранних версиях, для этого развивался и предлагался сторонний проект gorun, но на сегодня для
этих целей достаточно команды GoLang run.

51

репозиториев GIT ...
Для установки программных проектов Go (пакетов и дополнительных инструментальных
пакетов) из сети используется команда get. Она использует, на выбор, несколько репозиторных
систем (созданных, популярных и известных задолго до появления Go).

Репозиторные системы
Команда get имеет возможность использовать альтернативно одну из 4-х общеизвестных систем
инсталляций и контроля версий, в зависимости от ресурса, откуда скачивается конкретный
проект:


svn — Subversion: http://subversion.apache.org/packages.html



hg — Mercurial: http://mercurial.selenic.com/wiki/Download



git — Git: http://git-scm.com/downloads



bzr — Bazaar: http://wiki.bazaar.canonical.com/Download

Именно поэтому, при начальной инсталляции (пакета) GoLang он предлагает сразу же
инсталлировать и репозиторные системы, о чём было сказано ранее.
Естественно, что для использования репозиторной системы соответствующее приложение
менеджера репозитарной системы должно быть предварительно установлено в вашей системе 11,
иначе вы получите сообщения подобные следующим:
$ go get code.google.com/p/go.tools/cmd/godoc
go: missing Mercurial command. See http://golang.org/s/gogetcmd
package code.google.com/p/go.tools/cmd/godoc: exec: "hg": executable file not found in $PATH

Примечание: Сам проект GoLang и его ответвления используют преимущественно систему Mercurial, а проекты,
ведущиеся под эгидой Ubuntu — систему Bazaar. Поэтому из 4-х репозиторных систем лучше иметь установленными
все.

Приложения менеджеров репозитариев управлениями версий вполне можно установить из
показанных выше адресов (URL их проектов)... Но проще и целесообразнее сделать это просто
проверив присутствие и, если их ещё нет в системе, просто установив их из пакетной системы
своего собственного дистрибутива Linux:
$ aptitude search subversion
p
hgsubversion
p
python-subversion
v
python2.7-subversion
p
subversion
p
subversion-tools

-

клиент Subversion как расширение Mercurial
Python bindings for Apache Subversion
Advanced version control system
различные инструменты Apache Subversion

$ sudo apt install subversion
Чтение списков пакетов… Готово
Построение дерева зависимостей
Чтение информации о состоянии… Готово
Будут установлены следующие дополнительные пакеты:
libapr1 libaprutil1 libserf-1-1 libsvn1 libutf8proc2
Предлагаемые пакеты:
db5.3-util libapache2-mod-svn subversion-tools
Следующие НОВЫЕ пакеты будут установлены:
libapr1 libaprutil1 libserf-1-1 libsvn1 libutf8proc2 subversion
Обновлено 0 пакетов, установлено 6 новых пакетов, для удаления отмечено 0 пакетов, и 25 пакетов
не обновлено.
Необходимо скачать 2.354 kB архивов.
После данной операции объём занятого дискового пространства возрастёт на 10,3 MB.
Хотите продолжить? [Д/н] y
...

11 Сами репозиторные системы и их менеджеры никоим образом не относятся к проекту Go, и созданы задолго до
него . Они относятся к нему как совершенно внешние проекты от сторонних разработчиков. Но GoLang изнутри,
сам по себе, ориентирован на активное использование репозиториев (вместо ручного управления кодом), поэтому
предлагает их установить, даже если вы их и не используете для других целей.

52

$ which svn
/usr/bin/svn
$ svn --version
svn, version 1.13.0 (r1867053)
compiled Mar 24 2020, 12:33:36 on x86_64-pc-linux-gnu
...
$ apt list mercurial
Вывод списка… Готово
mercurial/focal 5.3.1-1ubuntu1 amd64
$ sudo apt install mercurial
Чтение списков пакетов… Готово
Построение дерева зависимостей
Чтение информации о состоянии… Готово
Будут установлены следующие дополнительные пакеты:
libpython2-stdlib mercurial-common python2 python2-minimal
Предлагаемые пакеты:
kdiff3 | kdiff3-qt | kompare | meld | tkcvs | mgdiff qct python-mysqldb python-openssl pythonpygments wish python2-doc python-tk
Следующие НОВЫЕ пакеты будут установлены:
libpython2-stdlib mercurial mercurial-common python2 python2-minimal
Обновлено 0 пакетов, установлено 5 новых пакетов, для удаления отмечено 0 пакетов, и 25 пакетов
не обновлено.
Необходимо скачать 3.034 kB архивов.
После данной операции объём занятого дискового пространства возрастёт на 15,5 MB.
...
$ which hg
/usr/bin/hg
$ hg --help
Распределенная система контроля версий Mercurial
...
$ hg --version
Распределенная SCM Mercurial (версия 5.3.1)
...
$ sudo apt install git
Чтение списков пакетов… Готово
Построение дерева зависимостей
Чтение информации о состоянии… Готово
Будут установлены следующие дополнительные пакеты:
git-man liberror-perl
Предлагаемые пакеты:
git-daemon-run | git-daemon-sysvinit git-doc git-el git-email git-gui gitk gitweb git-cvs gitmediawiki git-svn
Следующие НОВЫЕ пакеты будут установлены:
git git-man liberror-perl
Обновлено 0 пакетов, установлено 3 новых пакетов, для удаления отмечено 0 пакетов, и 25 пакетов
не обновлено.
Необходимо скачать 5.465 kB архивов.
После данной операции объём занятого дискового пространства возрастёт на 38,4 MB.
...
$ which git
/usr/bin/git
$ git --version
git version 2.25.1

53

$ apt list bzr
Вывод списка… Готово
bzr/focal,focal 2.7.0+bzr6622+brz all
$ sudo apt install bzr
Чтение списков пакетов… Готово
Построение дерева зависимостей
Чтение информации о состоянии… Готово
Будут установлены следующие дополнительные пакеты:
brz python3-breezy python3-deprecated python3-dulwich python3-fastimport python3-github
python3-gitlab python3-gpg python3-wrapt
Предлагаемые пакеты:
brz-doc python3-breezy.tests python3-breezy-dbg python3-kerberos python3-paramiko git-core
python-gitlab-doc
Следующие НОВЫЕ пакеты будут установлены:
brz bzr python3-breezy python3-deprecated python3-dulwich python3-fastimport python3-github
python3-gitlab python3-gpg python3-wrapt
Обновлено 0 пакетов, установлено 10 новых пакетов, для удаления отмечено 0 пакетов, и 25 пакетов
не обновлено.
Необходимо скачать 2.281 kB архивов.
После данной операции объём занятого дискового пространства возрастёт на 14,0 MB.
...
$ which bzr
/usr/bin/bzr
$ bzr --version
Breezy (brz) 3.0.2
...

Установка проектов
Команды установки12 get и install будут работать только если у вас переменная окружения
GOPATH установлена на некоторый реально существующий каталог (как было описано раньше),
иначе:
$ go get github.com/astaxie/beego
package github.com/astaxie/beego: cannot download, $GOPATH not set. For more details see: go
help gopath

Если указанный в GOPATH каталог не содержит требуемых подкаталогов src, pkg или bin, то те
из них, который нужны для установки, будут созданы при выполнении команды get.
Несколько примеров того как в экосистеме GoLang работают команды загрузки проектов Go из
сети:
$ go env GOPATH
/home/olej/go
$ go get github.com/astaxie/beego
$ ls `go env GOPATH`/src/github.com/astaxie/
beego
$ go get launchpad.net/gorun
$ ls -l `go env GOPATH`/src/launchpad.net/gorun
итого 44
-rw-rw-r-- 1 olej olej 35147 янв 20 03:02 COPYING
-rw-rw-r-- 1 olej olej 7597 янв 20 03:02 gorun.go
$ go get github.com/golang/lint/golint
package github.com/golang/lint/golint: code in directory
/home/olej/go/src/github.com/golang/lint/golint expects import "golang.org/x/lint/golint"

12 Команда get претерпела существенные изменения с введением модулей в версии GoLang после 1.1
и теперь дополнена родственной ей новой командой install.

54

$ ls -l `go env GOPATH`/src/github.com/golang/lint/golint
итого 20
-rw-rw-r-- 1 olej olej 3807 фев 22 10:12 golint.go
-rw-rw-r-- 1 olej olej 449 фев 22 10:12 importcomment.go
-rw-rw-r-- 1 olej olej 8459 фев 22 10:12 import.go

Конкретный размер и состав импортируемых проектов определяется только самим проектом, и
может представлять собой от одного файла и до целого дерева каталогов-файлов.
Более подробную информацию о команде get вы можете получить выполнив:
$ go help get
...

Утилиты GoLang (tool)
Ещё одна команда go, заслуживающая комментариев — это команда tool, без параметров она
выводит список всех доступных (установленных) внешних утилит экосистемы GoLang:
$ go tool
addr2line
api
asm
buildid
cgo
compile
cover
dist
doc
fix
link
nm
objdump
pack
pprof
test2json
trace
vet

Как легко видеть — это список исполнимых утилит, находящихся в каталоге GOTOOLDIR (эта
переменная окружения устанавливается командой go на основании GOROOT):
$ go env GOTOOLDIR
/home/olej/goroot/pkg/tool/linux_amd64
$ ls -l `go env GOTOOLDIR`
итого 85908
-rwxr-xr-x 1 root root 3119176
-rwxr-xr-x 1 root root 4413016
-rwxr-xr-x 1 root root 3594344
-rwxr-xr-x 1 root root 1995672
-rwxr-xr-x 1 root root 3541560
-rwxr-xr-x 1 root root 18432728
-rwxr-xr-x 1 root root 3853624
-rwxr-xr-x 1 root root 2668248
-rwxr-xr-x 1 root root 3410456
-rwxr-xr-x 1 root root 2438744
-rwxr-xr-x 1 root root 4546712
-rwxr-xr-x 1 root root 3077160
-rwxr-xr-x 1 root root 3383112
-rwxr-xr-x 1 root root 1603800
-rwxr-xr-x 1 root root 11022216
-rwxr-xr-x 1 root root 1995736

55

фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев
фев

15
15
15
15
15
15
15
15
15
15
15
15
15
15
15
15

2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020
2020

addr2line
api
asm
buildid
cgo
compile
cover
dist
doc
fix
link
nm
objdump
pack
pprof
test2json

-rwxr-xr-x 1 root root
-rwxr-xr-x 1 root root

8699448 фев 15
6138072 фев 15

2020 trace
2020 vet

Чем наблюдаемые здесь внешние утилиты Go отличаются от команд Go? Судя по всему, в число
утилиты вынесены достаточно редко требуемые команды, выполняющие инструментальные
действия, более нужные разработчикам GoLang, чем использователям Go.
Общий формат команд запуска утилит:
$ go help tool
usage: go tool [-n] command [args...]
Tool runs the go tool command identified by the arguments.
With no arguments it prints the list of known tools.
The -n flag causes tool to print the command that would be
executed but not execute it.
For more about each tool command, see 'go doc cmd/'.

Как следует из этой краткой общей справки, по каждой из этих утилит может быть получена
отдельная справка по её конкретному использованию, например так:
$ go tool nm -h
usage: go tool nm [options] file...
-n
an alias for -sort address (numeric),
for compatibility with other nm commands
-size
print symbol size in decimal between address and type
-sort {address,name,none,size}
sort output in the given order (default name)
size orders from largest to smallest
-type
print symbol type after name
$ go doc nm
Nm lists the symbols defined or used by an object file, archive, or
executable.
Usage:
go tool nm [options] file...
The default output prints one line per symbol, with three space-separated
fields giving the address (in hexadecimal), type (a character), and name of
the symbol. The types are:
T
t
R
r
D
d
B
b
C
U

text (code) segment symbol
static text segment symbol
read-only data segment symbol
static read-only data segment symbol
data segment symbol
static data segment symbol
bss segment symbol
static bss segment symbol
constant address
referenced but undefined symbol

Following established convention, the address is omitted for undefined
symbols (type U).
The options control the printed output:

56

-n
an alias for -sort address (numeric),
for compatibility with other nm commands
-size
print symbol size in decimal between address and type
-sort {address,name,none,size}
sort output in the given order (default name)
size orders from largest to smallest
-type
print symbol type after name
$ go tool fix -h
usage: go tool fix [-diff] [-r
-diff
display diffs instead of
-force string
force these fixes to run
-r string
restrict the rewrites to

fixname,...] [-force fixname,...] [path ...]
rewriting files
even if the code looks updated
this comma-separated list

Available rewrites are:
cftype
Fixes initializers and casts of C.*Ref and JNI types
context
Change imports of golang.org/x/net/context to context
egl
Fixes initializers of EGLDisplay
gotypes
Change imports of golang.org/x/tools/go/{exact,types} to go/{constant,types}
jni
Fixes initializers of JNI's jobject and subtypes
netipv6zone
Adapt element key to IPAddr, UDPAddr or TCPAddr composite literals.
https://codereview.appspot.com/6849045/
printerconfig
Add element keys to Config composite literals.

Утилиты компиляции
Отдельно следует обратить внимание на группу утилит, выполняющих поэтапную компиляцию и
сборку приложений: compile, link, objdump ... В чём особый интерес? Во-первых, как понятно,
для более детального анализа того что происходит при компиляции. А во-вторых, потому что
Интернет переполнен вопросами: почему возникают упорные ошибки при любой компиляции
вида:
$ go tool compile tiny.go
tiny.go:2:8: could not import fmt (file not found)

Мы здесь используем в качестве эталонного приложения минимальный образец кода, который
неоднократно используем по тексту (каталог tools):
tiny.go :
package main
import "fmt"
func main() { fmt.Println("Hello, 世界") }

Мы сможем «научить» компилятор правильно подгружать информацию импорта … но прежде
посмотрим на каталог, где хранятся импорты:
$ du -hs `go env GOROOT`/pkg
421M
/home/olej/goroot/pkg

57

И теперь проделаем (команду подсказали в обсуждениях разработчики GoLang):
$ time GODEBUG=installgoroot=all go install std

После чего (однократно выполненной команды) все компиляции пойдут успешно:
$ go tool compile tiny.go
$ ls -l tiny.o
-rw-rw-r-- 1 olej olej 8048 мар 14 13:44 tiny.o

Теперь ещё раз посмотрим на каталог импортов:
$ du -hs `go env GOROOT`/pkg
531M
/home/olej/goroot/pkg

Нами выполненная команда подгрузила в каталог дополнительных 110Mb из сети, что сделало
возможным отделённые компиляции с отладочными возможностями (что конкретно подгрузила
команда я не выяснял).
Теперь вернёмся к результату компиляции:
$ file tiny.o
tiny.o: current ar archive
$ ar -t tiny.o
__.PKGDEF
_go_.o

С удивлением отметим, что результатом компиляции не является объектный файл в
общепринятом в Linux смысле и формате, а это AR архив Linux. Что мы можем получить
дополнительно из такой компиляции? Во-первых, мы можем следующим шагом собрать
приложение, так как мы делаем это и командой build, воспользовавшись утилитой link: (как и
всегда и везде, выходной файл мы можем сразу определить опцией -o …, а если нет — то это
a.out):
$ go tool link tiny.o
$ ls -l a.out
-rwxrwxr-x 1 olej olej 1762931 мар 14 14:05 a.out
$ ./a.out
Hello, 世界

Во-вторых, в смысле дополнительных возможностей, предоставляется ещё одна утилита этой
группы objdump, для анализа итогов компиляции, вплоть до ассемблерного кода:
$ go tool objdump tiny.o
TEXT main.main(SB) gofile../home/olej/2023/own.BOOKs/BHV.Go.2/examples/tools/tiny.go
tiny.go:4
0x130c
493b6610
CMPQ 0x10(R14), SP
[2:2]R_USEIFACE:type:string [2:2]R_USEIFACE:type:*os.File
tiny.go:4
0x1310
764c
JBE 0x135e
tiny.go:4
0x1312
55
PUSHQ BP
tiny.go:4
0x1313
4889e5
MOVQ SP, BP
tiny.go:4
0x1316
4883ec38
SUBQ $0x38, SP
tiny.go:4
0x131a
440f117c2428
MOVUPS X15, 0x28(SP)
tiny.go:4
0x1320
488d1500000000
LEAQ 0(IP), DX
[3:7]R_PCREL:type:string
tiny.go:4
0x1327
4889542428
MOVQ DX, 0x28(SP)
tiny.go:4
0x132c
488d1500000000
LEAQ 0(IP), DX
[3:7]R_PCREL:main..stmp_0
tiny.go:4
0x1333
4889542430
MOVQ DX, 0x30(SP)
print.go:314
0x1338
488b1d00000000
MOVQ 0(IP), BX
[3:7]R_PCREL:os.Stdout
print.go:314
0x133f
488d0500000000
LEAQ 0(IP), AX
[3:7]R_PCREL:go:itab.*os.File,io.Writer
print.go:314
0x1346
488d4c2428
LEAQ 0x28(SP), CX
print.go:314
0x134b
bf01000000
MOVL $0x1, DI
print.go:314
0x1350
4889fe
MOVQ DI, SI
print.go:314
0x1353
e800000000
CALL 0x1358

58

[1:5]R_CALL:fmt.Fprintln
tiny.go:4
0x1358
tiny.go:4
0x135c
tiny.go:4
0x135d
tiny.go:4
0x135e
[1:5]R_CALL:runtime.morestack_noctxt
tiny.go:4
0x1363

4883c438
5d
c3
e800000000

ADDQ $0x38, SP
POPQ BP
RET
CALL 0x1363

eba7

JMP main.main(SB)

Во-третьих, и это самое главное из возможностей, проделывая компиляцию с опцией -m, мы
сразу можем увидеть какие переменные компилятор разместит в хип, а какие в локальном стеке.
А для системы со сборкой мусора это очень важно. Делаем это так:
$ go tool compile -m tiny.go
/home/olej/2023/own.BOOKs/BHV.Go.2/examples/tools/tiny.go:4:6: can inline main
/home/olej/2023/own.BOOKs/BHV.Go.2/examples/tools/tiny.go:4:26: inlining call to fmt.Println
/home/olej/2023/own.BOOKs/BHV.Go.2/examples/tools/tiny.go:4:26: ... argument does not escape
/home/olej/2023/own.BOOKs/BHV.Go.2/examples/tools/tiny.go:4:27: "Hello, 世界" escapes to heap

В данном случае, это взято слишком простое приложение (чтобы не перегружать текст), но для
серьёзного кода это может быть весьма информативно!

Связь с кодом C (Cgo)
Программный код Go может непосредственно использовать код, написанный на языке C. Для
этого используется такой инструмент, как Cgo (https://pkg.go.dev/cmd/cgo), мы его только-что
видели выше в составе tool. Для использования Cgo пишется обычный Go код, но который
импортирует псевдо-пакет "C". Go код после этого может ссылаться к типам как C.size_t,
переменным как C.stdout, или к функциям как C.putchar(). Это особо актуально для
операционной системы Linux, где таким образом обеспечивается низкоуровневый интерфейс ко
всем системным вызовам POSIX API и динамическим разделяемым библиотекам *.so в Linux.
Сделаем простейшее тестовой приложение (каталог tools, пользуясь случаем проверяем ещё раз
и «всесильность» UNICODE и UTF-8 для отображения на китайском языке слова «world»):
hello.go :
package main
// #include
// #include
import "C"
import "unsafe"
func main(){
str := "Hello, 世界\n"
cs := C.CString(str)
C.fputs(cs, (*C.FILE)(C.stdout))
C.free(unsafe.Pointer(cs))
}

Всё, что написано выше включения псевдо-пакета (строка «import "C"») и выглядит как
комментарий (в синтаксисе Go) — является кодом на языке C, который может включать любые
директивы и любой (или почти любой) код C или C++. Собираем и испытываем:
$ go build tiny.c.go
$ ls -l tiny.c
-rwxrwxr-x 1 olej olej 1249984 фев 22 12:30 tiny.c
$ ./tiny.c
Hello, 世界

Необходимые временами, и привычные, опции GCC компилятора CFLAGS, CPPFLAGS, CXXFLAGS
и LDFLAGS могут быть тоже определены через псевдо-дерективы #cgo также в виде комментария,

59

чтобы настроить требуемое поведение C или C++ компилятора (см. переменные среды GoLang
обсуждавшиеся выше: CC="gcc", CXX="g++", CGO_CFLAGS="-g -O2", CGO_CPPFLAGS="",
CGO_CXXFLAGS="-g -O2", CGO_LDFLAGS="-g -O2") — значения, определенные в нескольких
последовательных директивах, объединяются вместе, например:
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include
import "C"

Альтернативно, CPPFLAGS или LDFLAGS могут быть определены через средства pkg-config,
используя директиву #cgo pkg-config: со следующим за ней списком конфигурируемых
пакетов:
// #cgo pkg-config: png cairo glu
// #include
import "C"

Вспомните и сравните с системной командой Linux (относительно любой библиотеки):
$ pkg-config --libs glu
-lGLU

Любая функции C (даже возвращающая void функции) могут быть вызваны в контексте
возврата множественных значений: возвращаемое значение (если оно есть) и привычная C
переменная errno как код ошибка (используйте пустую переменную _ чтобы опустить результат,
если функция ничего не возвращает). Например:
n, err := C.sqrt(-1)
_, err := C.voidFunc()

Несколько специальных функций определено для преобразований между Go и C типами, которые
производят копирование данных (из одного формата в другой). В псевдо-Go определениях они
выглядят подобно следующему:
// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char
// C string to Go string
func C.GoString(*C.char) string
// C string, length to Go string
func C.GoStringN(*C.char, C.int) string
// C pointer, length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte

Поскольку строки C при копировании размещаются в хипе ( C.malloc()), после использования
их память должна быть освобождена C.free().
Вызов функций C по указателю на функции в настоящее время не поддерживается, однако вы
можете объявить Go переменные, которые загрузить указателями на C функции и передавать их
взад и вперед между Go и C. Сам C-код может вызвать функцию по указателям, полученным от
Go. Например (каталог tools):
cex.go :
package main
// typedef int (*intFunc) ();
//
// int bridge_int_func(intFunc f) {
//
return f();
// }

60

// int fortytwo() {
//
return 42;
// }
import "C"
import "fmt"
func main() {
f := C.intFunc(C.fortytwo)
fmt.Println(int(C.bridge_int_func(f)))
}
$ go build cex.go
$ ./cex
42

Таким образом, разрабатываемый код Go получает возможность использовать всё богатство
наработанных библиотек C/C++ и произвольных фрагментов исходных кодов на этих языках, но,
самое главное, все API библиотек POSIX и Linux !
Программа cgo может быть вызвана и автономно, по типу:
$ go tool cgo [cgo options] [-- compiler options] file.go

При этом cgo создаёт из входного файла file.go несколько выходных файла в подкаталоге
_obj: 2 файла Go с исходным кодом, C файл для GCC и C файл GoLang. Например (уже
виденный нами выше исходный hello.c.go):
$ go tool cgo hello.c.go
$ ls -l _obj
итого 28
-rw-rw-r-- 1
-rw-rw-r-- 1
-rw-rw-r-- 1
-rw-rw-r-- 1
-rw-rw-r-- 1
-rw-rw-r-- 1
-rw-rw-r-- 1

olej
olej
olej
olej
olej
olej
olej

olej 605 фев 22 13:04 _cgo_export.c
olej 1547 фев 22 13:04 _cgo_export.h
olej
13 фев 22 13:04 _cgo_flags
olej 819 фев 22 13:04 _cgo_gotypes.go
olej 416 фев 22 13:04 _cgo_main.c
olej 641 фев 22 13:04 hello.c.cgo1.go
olej 1884 фев 22 13:04 hello.c.cgo2.c

Код на C, обратным порядком, также может использовать функции Go. Но это, как кажется, более
экзотика чем необходимость, и о использовании таких возможностей можно почитать подробнее
в фирменном описании cgo (https://pkg.go.dev/cmd/cgo).

Сторонний и дополнительный инструментарий
За последние годы на Go создано «от третьих сторон» столько интересного, полезного и
разнообразного инструмента для разработчиков, что становится необозримым со всем ним
ознакомиться, не только описать. На сегодня «на Go не пишет только ленивый». Я назову только
несколько «лёгких» инструментов, которые позволяют по-быстрому поработать с Go.
Хороший и доступный инструмент для быстрого изучения Go, синтаксиса кода — это интерфейс
WEB: https://go.dev/play/, предоставляемый на сайте самого проекта GoLang, позволяющий
оперативно редактировать, изучать и отлаживать код непосредственно в браузере:

61

Рис. 1.4. Онлайн инструмент для изучения Go

Обратите внимание, что этот интерфейс позволяет выбрать версию GoLang, в которой вы хотите
проверять свой код — вспомним, что стандарт Go динамично меняется, и то, чего в нём не было
вчера, появится завтра.
Кроме того, для отработки кода хорошо бы иметь редактор с адекватной цветовой разметкой под
выбранный язык. Из-за относительной новизны Go, многие из традиционных редакторов кода
Linux ещё не имеют разметки под синтаксис Go (или не имели на момент написания этого
текста), и её придётся настраивать под себя самостоятельно. Если вы не хотите терять время на
ручную настройку цветовой разметки, то можете воспользоваться средой Geany, которая
присутствует в репозиториях практически любого дистрибутива Linux. Geany не является в
общепринятом смысле средой разработки (IDE), и в этом её большой плюс, а представляет собой
развитый многооконный графический терминал, позволяющий «в одном флаконе» редактировать
код, выполнять его сборку (make) в отдельном терминале, а также, запустив в этом терминале mc,
осуществлять навигацию по файлам проекта и, возможно, оттуда же консольные тестовые
запуски … как-то так:

62

Рис. 1.5. Среда разработки Geany

Очень большой набор инструментов и утилит для использования Go разработано
непосредственно в рамках основного проекта GoLang, авторским коллективом Go: godoc,
golint, vet, ... — этот набор активно расширяется. Ещё некоторая часть инструментария
нарабатывается в качестве сторонних проектов: beego, revel ... Информация об отдельных
таких средствах будет иногда упоминаться по ходу дальнейшего рассмотрения.

Интерактивный отладчик Delve
Интерактивный отладчик Delve является отдельным от GoLang продукт. К счастью, он входит в
стандартные репозитории основных дистрибутивов Linux для стандартной установки:
$ aptitude search Delve | grep Go
p delve - debugger for the Go programming language

(Я отфильтровал вывод по Go только для того, чтобы не спутать с подобного названия
инструментом JavaScript.)
$ sudo apt install delve
[sudo] пароль для olej:
Чтение списков пакетов… Готово
Построение дерева зависимостей… Готово
Чтение информации о состоянии… Готово
Следующие НОВЫЕ пакеты будут установлены:
delve
Обновлено 0 пакетов, установлено 1 новых пакетов, для удаления отмечено 0 пакетов, и 4 пакетов
не обновлено.
Необходимо скачать 3.865 kB архивов.
После данной операции объём занятого дискового пространства возрастёт на 12,3 MB.
Пол:1 http://mirror.mirohost.net/ubuntu jammy/universe amd64 delve amd64 1.8.1-1 [3.865 kB]

63

Получено 3.865 kB за 2с (1.962 kB/s)
Выбор ранее не выбранного пакета delve.
(Чтение базы данных … на данный момент установлен 536301 файл и каталог.)
Подготовка к распаковке …/delve_1.8.1-1_amd64.deb …
Распаковывается delve (1.8.1-1) …
Настраивается пакет delve (1.8.1-1) …
Обрабатываются триггеры для man-db (2.10.2-1) …

Исполнимый файл, интересующий нас отладчик, имеет имя dlv:
$ which dlv
/usr/bin/dlv

В общих чертах работа с интерактивным отладчиком Delve похожа на работу с хорошо известным
GNU отладчиком GDB: позволяет расставлять точки останова, контролировать и изменять
значение переменных в точках останова… Программа имеет развитую систему подсказок:
$ dlv --help
Delve is a source level debugger for Go programs.
Delve enables you to interact with your program by controlling the execution of the process,
evaluating variables, and providing information of thread / goroutine state, CPU register state
and more.
The goal of this tool is to provide a simple yet powerful interface for debugging Go programs.
Pass flags to the program you are debugging using `--`, for example:
`dlv exec ./hello -- server --config conf/config.toml`
Usage:
dlv [command]
Available Commands:
attach
Attach to running process and begin debugging.
completion Generate the autocompletion script for the specified shell
connect
Connect to a headless debug server with a terminal client.
core
Examine a core dump.
dap
Starts a headless TCP server communicating via Debug Adaptor Protocol (DAP).
debug
Compile and begin debugging main package in current directory, or the package
specified.
exec
Execute a precompiled binary, and begin a debug session.
help
Help about any command
run
Deprecated command. Use 'debug' instead.
test
Compile test binary and begin debugging program.
trace
Compile and begin tracing program.
version
Prints version.
Flags:
--accept-multiclient
Allows a headless server to accept multiple client
connections via JSON-RPC or DAP.
--allow-non-terminal-interactive
Allows interactive sessions of Delve that don't have a
terminal as stdin, stdout and stderr
--api-version int
Selects JSON-RPC API version when headless. New clients
should use v2. Can be reset via RPCServer.SetApiVersion. See
Documentation/api/json-rpc/README.md. (default 1)
--backend string
Backend selection (see 'dlv help backend'). (default
"default")
--build-flags string
Build flags, to be passed to the compiler. For example:
--build-flags="-tags=integration -mod=vendor -cover -v"
--check-go-version
Exits if the version of Go in use is not compatible
(too old or too new) with the version of Delve. (default true)
--disable-aslr
Disables address space randomization
--headless
Run debug server only, in headless mode. Server will
accept both JSON-RPC or DAP client connections.
-h, --help
help for dlv

64

--init string
Init file, executed by the terminal client.
-l, --listen string
Debugging server listen address. (default
"127.0.0.1:0")
--log
Enable debugging server logging.
--log-dest string
Writes logs to the specified file or file descriptor
(see 'dlv help log').
--log-output string
Comma separated list of components that should produce
debug output (see 'dlv help log')
--only-same-user
Only connections from the same user that started this
instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray
Specifies redirect rules for target process (see 'dlv
help redirect')
--wd string
Working directory for running the program.
Additional help topics:
dlv backend
Help about the --backend flag.
dlv log
Help about logging flags.
dlv redirect
Help about file redirection.
Use "dlv [command] --help" for more information about a command.

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

Источники информации
[1] cgo.Documentation — https://pkg.go.dev/cmd/cgo#hdr-Go_references_to_C
[2] Andrew Gerrand, C? Go? Cgo! , 17 March 2011 — https://go.dev/blog/cgo
[3] Юникод — https://docs.microsoft.com/ru-ru/windows/win32/intl/unicode
[4] Кросс-компиляция в Go — https://habr.com/ru/post/249449/
[5] Михалис Цукалос, Golang для профи. Работа с сетью, многопоточность, структуры данных и машинное
обучение с Go. 2020г., 720 страниц, изд. Питер, ISBN: 978-5-4461-1617-1

65

Неформально о синтаксисе Go
Имейте в виду, если вы сделаете быстро и плохо,
то люди забудут, что вы сделали быстро, и
запомнят, что вы сделали плохо. Если вы
сделаете медленно и хорошо, то люди забудут,
что вы сделали медленно, и запомнят, что вы
сделали хорошо!
Сергей Королёв.
Синтаксис Go во многом заимствуется из классического C (у них общие авторы), что сильно
снижает порог начального вхождения в работу с языком для имеющих минимальный опыт
работы с C/C++. Go является прямым развитием языковой линии C/C++, но с заимствованиями
многих «находок» из Oberon, Python, функциональных и скриптовых языков… мы об этом уже
говорили ранее.
Именно поэтому этот раздел озаглавлен «неформально»: во-первых, потому что изложение
опирается на аналогии из C, который предполагается более-менее известным 13, а, во-вторых,
потому что описание ниже акцентируется на тех сторонах Go, которые понадобятся дальше в
рассмотрении параллельных вычислений, а некоторые другие стороны или названы вскользь, или
даже совершенно опущены.
Go в значительной степени упрощает синтаксис C и делает его элегантным. Например: в Go
отсутствуют обязательные ограничители операторов точкой с запятой. Совершенно понятно, что
требование завершающих точки с запятой в C было вызвано только требованием простоты
лексографического разбора кода, разбитие кода на лексемы, и связано это с неразвитостью
инструментария лексографического разбора 40 лет назад. В Go, в большинстве случаев,
достаточно просто перевода строки, который толкуется в смысле заменителя точки с запятой. Но
это делает синтаксис записи Go не свободным в записи: смысл написанного кода может зависеть
от его размещения по строкам. Такой отход от свободного стиля записи кода характерен для
целого ряда современных языков: Python, Haskell. (Стремление к «свободности» синтаксиса,
модное пару-тройку десятилетий назад, сменилось отказом от неё в новых разработках.)
Go трактует конец любой не пустой линии, как неявную точку с запятой. В результате этого в
ряде случаев нельзя произвольно использовать перенос строки. Например, вы не можете
написать:
func g()
{
}

// НЕВЕРНО

Точка с запятой будет поставлена после g(), и это приведет к тому, что данный код будет
являться объявлением функции, а не её определением. Аналогично вы не можете написать:
if x {
}
else {
}

// НЕВЕРНО

Точка с запятой будет поставлена после }, полностью заканчивает оператор if (укороченная
форма), и перед else вызовет синтаксическую ошибку.
Если вы сомневаетесь в том, как Go толкует код, или он создаёт непонятные сообщения о
синтаксических ошибках — расставьте (временно) точки с запятой в конце сомнительных строк,
и всё станет ясно.
Так как точка с запятой явно обозначает конец выражения, вы вполне можете продолжать
использовать такой ограничитель точно так же, как и в C и C++. Тем не менее, это не
рекомендуется так как засоряет текст. Идиоматически Go опускает ненужные точки с запятой, и
на практике использование точки с запятой ограничивается циклом for и случаем, когда вы
хотите разместить на одной строке несколько коротких выражений.
13 Первоначально этот конспект так и назывался: «Язык Go для программистов C», и предполагает предпочтительным
хотя бы поверхностное знание языка C.

66

Вместо того, чтобы беспокоиться о расположении точек с запятой и скобок, форматируйте ваш
код с помощью команды fmt или программы gofmt (это не совсем одно и то же). Они дают
единый стандартный стиль Go и позволяет вам волноваться за содержательную часть своего кода,
а не его форматирование:
$ which gofmt
/usr/bin/gofmt
$ gofmt --help
usage: gofmt [flags] [path ...]
-cpuprofile string
write cpu profile to this file
-d
display diffs instead of rewriting files
-e
report all errors (not just the first 10 on different lines)
-l
list files whose formatting differs from gofmt's
-r string
rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
-s
simplify code
-w
write result to (source) file instead of stdout
$ go help fmt
usage: go fmt [-n] [-x] [packages]
Fmt runs the command 'gofmt -l -w' on the packages named
by the import paths. It prints the names of the files that are modified.
For more about gofmt, see 'go doc cmd/gofmt'.
For more about specifying packages, see 'go help packages'.
The -n flag prints commands that would be executed.
The -x flag prints commands as they are executed.
To run gofmt with specific options, run gofmt itself.
See also: go fix, go vet.

Но Go не только заимствует из C, но и минимизирует набор допустимых конструкций, устраняя
дублирование и избыточность. В Go доступны только управляющие конструкции if, for и
switch. Первое, что бросается в глаза, это отсутствие круглых скобок:
Loop: for i := 0; i < 10; i++ {
switch f(i) {
case 0, 1, 2: break Loop
}
g(i)
}

При этом конструкция goto и метки сохранились (как бы кто на них не ругался), а операции
инкремента и декремента более не являются выражениями, а являются операторами, и их нельзя
подставлять непосредственно в вычисления выражений. А префиксная форма ( ++i) этих
операций вообще отсутствует, используется только постфиксная ( i++).
А в сравнении с C++ язык Go значительно упростил громоздкость и витиеватость последнего, но
сохранил возможность реализации объектно-ориентированной парадигмы, хотя делает это совсем
по-другому… и очень необычно, но об этом подробно позже.
Проще всего при беглом знакомстве с синтаксисом Go отталкиваться от правил C, от которого Go
во многом происходит, и, отчасти, от C++, а в сравнении обращать внимание на самые
принципиальные отличия. Вот какие основные отличия упоминаются в документации проекта Go
и обсуждениях по языку:
— В Go есть указатели, но нет арифметики для них. Вы не сможете использовать переменную
указатель для прохода по массиву, байтам или строке.

67

— В Go используется динамическая сборка мусора. Нет необходимости (и даже нет
возможности!) освобождать память прямым указанием. Память объекта освобождается только
когда число ссылок на объект становится нулевым. Сборка мусора инкрементная и высоко
эффективна на современных процессорах.
— В Go нигде не используется неявное преобразование типов. Операции, которые сочетают
разные типы, требуют явного приведения (называемого преобразованием в Go), даже если это,
например, всего лишь целочисленные представления с разной разрядностью, например: int8 и
int16, или int64 и uint64 — это попарно перечислены разные типы, их смешение не
допустимо.
— Go не поддерживает спецификаторы const или violatile для переменных, но есть const
как описание константных данных без типа.
— В Go используется nil для неинициализированных указателей, в то время, как в C в тех же
случаях используются NULL или даже просто 0, хотя это, конечно, вопрос только наименований.
— В Go нет классов с конструкторами или деструкторами. Вместо методов класса, иерархии
наследования классов и виртуальных функций, в Go имеются методы и интерфейсы. Интерфейсы
также используются там, где в C++ используются шаблоны.
— В Go отсутствует наследование типов (для похожей, но не идентичной, конструкции
используется анонимное вложение типов, агрегация).
— В Go не допускается переопределение методов (перегрузки функций) и нет определяемых
пользователем операций.
— Массивы в Go являются предопределёнными типами языка (а не агрегатами из существующих
типов). Когда массив используется в качестве параметра функции, функция получает копию
массива, а не указатель на него. Тем не менее, на практике функции часто используют срезы
образуемые из массивов, для передачи параметров.
— В языке предусмотрены строки ( string) как предопределённый тип (а не производный из
существующих). Будучи один раз созданными, они не могут изменяться (строчным переменным,
конечно, могут быть присвоены новые строчные данные, но это будут уже совсем другие данные,
размещённые в другой области памяти — это подход Python). Такой код вполне корректен, но в
первом и во втором присвоениях переменной присваиваются разные литеральные константы
(неизменяемые) типа string, размещаемый в разных областях памяти:
func main() {
var x string
x = "first"
fmt.Println(x)
x = "second"
fmt.Println(x)
}

— В языке предусмотрены хеш-таблицы (ассоциативные массивы). Они ещё называются:
таблицами, словарями (map).
— Внутри языка предусмотрены разделенные параллельные ветви исполнения (go-процедуры,
горутины, сопрограммы) и каналы связи (channel) между ветвями (сознательно не называю их
«потоки», чтобы не путать с потоками ядра операционной системы pthread_t — это
совершенно другие вещи).
— Некоторые типы (словари и каналы) передаются по ссылке, а не по значению. Например,
передача словаря в функцию не копирует словарь, и если функция изменяет словарь, то
изменение будет видно там, откуда её вызвали.
— в Go не используются заголовочные файлы. Вместо этого, любой файл с исходным кодом —
всегда составная часть определенного пакета (не может быть кода вне пакета). Когда пакет
определяет объект (тип, константу, переменную, функцию) с именем, начинающимся с буквы в
верхнем регистре (заглавной), этот объект виден для всех других файлов, которые импортирую
(import) этот пакет в котором определён объект. Если имя начинается с буквы в нижнем регистре
(малой, строчной), то этот объект недоступен, не виден за пределами пакета (это некоторый
эквивалент видимости public и private из классов C++).

68

— Go не требует предшествующего описания используемых функций (либо полного описания,
либо прототипа определения). Важно чтобы функция вообще только присутствовала в данном
пакете (файле). Предшествующее описание в C/C++ — это определённо рудимент, возникший
только из требований простоты компиляции кода. В Go вполне допустима такая структура
программы:
func main() {
own_func()
// ...
}
func own_func() {
// ...
}

— Объявление какого-либо объекта (имени переменной, пакета в списке импорта) в коде Go, и
его дальнейшее не использование в коде — трактуется компилятором как грубая ошибка,
прекращающая компиляцию. Вот что авторы пишут по этому поводу:
Ошибкой является импорт пакета или объявление переменной без их использования.
Неиспользование импорта приводит к раздуванию программы и медленной её компиляции, в то
время как переменная, которая инициализируется, но не используется, по крайней мере,
растрачивает ресурсы, потраченное на её вычисление и, возможно, свидетельствует о
серьёзной ошибке. Когда программа находится в стадии активной разработки,
неиспользованные импорт и переменные часто возникают, и может быть раздражающим их
удаление просто для того, чтобы продолжить компиляцию, тем более, что они снова могут
потребоваться позже. Пустой идентификатор предоставляет временное решение.
И тут же предлагается решение: во всех таких местах использовать «пустой идентификатор»
имени, обозначаемый символом одиночного подчёркивания ("_"). Вот пример, предлагаемый в
иллюстрацию:
package main
import ("fmt" /*неиспользуемый*/; "io"; "log"; "os")
var _ = fmt.Printf // For debugging; delete when done.
Var _ = io.Reader // For debugging; delete when done.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: use fd.
_ = fd
}

Язык чрезвычайно изящный: синтаксис во многом повторяющий C (без необходимости лишних
разделителей ';' завершающих каждый оператор), дополненный своеобразным механизмом
классов (типов) и объектов, но без их громоздкости и тяжеловесности из C++.
В документации утверждается, что: Компиляция выполняется очень быстро – намного быстрее,
чем в некоторых других языках, особенно в сравнении с языками C и C++. Это может оказаться
существенным при работе над крупными проектами. Мы ещё вернёмся к этому факту.

Типы данных
В описаниях Go разделяются фундаментальные, предопределённые типы данных (first class type)
и производные типы данных. К фундаментальным данным относятся, например: скалярные
числовые типы, логические значения, символьные строки, массивы, хэш-таблицы, функции,
интерфейсы, ... (достаточно обширный перечень и не всегда очевидный для программиста
привыкшего к C/C++).
C/C++ и Go предоставляют подобные, но не идентичные, предопределённые типы данных:

69

знаковые и беззнаковые целые числа разной (8, 16, 32, 64) разрядности, 32-разрядные и 64разрядные, числа с плавающей точкой (вещественные и комплексные), структуры, указатели и др.
В Go uint8, int64 и подобно именованные целочисленные типы являются частью языка, а не
построены на вершине иерархии целых чисел, размер которых зависит от реализации (например,
long long в C). В языке существует большое количество различных типов простых скалярных
данных. Например, существует пять вариантов целочисленного типа int: int, int8, int16,
int32, int64. Такие же типы данных, но с префиксом u, представляют беззнаковые значения.
Числа с плавающей точкой представлены тремя типами: float, float32 и float64. Имеется
даже два типа данных для комплексных переменных: complex64 и complex128. Операции над
комплексными значениями вводятся пакетом math/cmplx. Существует тип byte для
представления коротких целых, и часто применяющийся для побайтового представления
символов.
В Go допускается использовать имя byte как синоним беззнакового типа uint8 и приветствуется
использование имени rune как синонима типа int32, там где этим значением представляются
отдельные символы UNICODE в кодировке UTF-32 (чтобы отличать их от собственно числовых
значений, предназначенных для арифметических вычислений).
Помимо этого, стандартная библиотека (пакет math/big — о пакетах будет подробно рассказано
далее) добавляет поддержку больших чисел: целых значений типа big.Int и рациональных
значений типа big.Rat, которые имеют вообще неограниченный размер (то есть их размеры
ограничиваются только доступным объемом машинной памяти).
В языке Go нет нигде неявного приведения типов, поэтому смешение даже этих родственных
типов между собой при компиляции вызывает терминальные ошибки.
Из простых скалярных типов Go предоставляет ещё логический тип bool — 1-битовый
целочисленный тип, представляющий значение истинности в логических выражениях. Для
представления логических значений предназначены логические константы в таком написании:
true и false. Логические значения могут объединяться логическими операциями:
&& - операция «и»
|| - операция «или»
! - операция отрицания (инверсии)
Логические значения могут быть выведены на печать как логические константы:
fmt.Println(true &&
fmt.Println(true &&
fmt.Println(true ||
fmt.Println(true ||
fmt.Println(!true)

true)
false)
true)
false)

Будет выведено (C/C++ в подобном контексте вывели бы целочисленное значение переменныз, 0
или 1):
true
false
true
true
false

Из числа агрегатных типов данных Go дополнительно обеспечивает: встроенный тип строки
(string), хэш-таблицы (map), каналы (channel), а также базовые массивы и их срезы.
Символьные данные (string) представляются в UNICODE, а не ASCII. Строки, ввиду их
важности, будут подробно рассмотрены далее.
Go гораздо более строго типизированным, чем даже C++. В частности, нет никакого неявного
приведения типов в Go, только явное преобразование типа ( int16 и int32 будут уже разными
типами, не говоря уже о floaf64). Это обеспечивает дополнительную безопасность и свободу от
целого класса ошибок, но за счет некоторой дополнительной строгости типизации. Также нет
типа union, поскольку это позволило бы создание системы подтипов. Однако Go интерфейс,

70

описанный как interface{} (пустой интерфейс) предоставляет собой типо-безопасную
альтернативу: такой тип совместим со значением любого типа данных. Выражение T(v)
преобразовывает значение v к типу T, пример некоторых численных преобразований:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

Оба, и C++ и Go поддерживают псевдонимы (синонимы, алиасы) типа ( typedef в C++ и type в
Go). Однако, в отличие от C++, Go трактует новые объявленные типы как разные типы (строгая
именная типизация). Следовательно, следующий код вполне допустим в C++:
// C++
typedef double
typedef double
position pos =
velocity vel =
pos += vel;

position;
velocity;
218.0;
-9.8;

Но эквивалент такого кода недопустим в Go без явного приведения типа:
type position float64
type velocity float64
var pos position = 218.0
var vel velocity = -9.8
// pos += vel
// INVALID: mismatched types position and velocity
pos += position(vel)
// Valid

Go не позволяет указателям быть преобразованными в целые чисел (или сконструированы из
них), в отличие от C/C++. Однако, пакет Go unsafe позволяет явным образом обойти этот
механизм безопасности в случае необходимости (например, для использования кода для систем
низкого уровня).
Описание всех предопределённых типов (в документации Go они называются типами 1-го
уровня) производится в пакете builtin. Полное перечисление всех типов с их определениями
можно получить из описания этого пакета: https://pkg.go.dev/builtin.

Переменные
Go в символьных представлениях везде последовательно использует UTF-8 кодировку для
представления UNICODE кодов символов. Поэтому даже и в именах переменных допускаются
символы национальных алфавитов (русские, греческие, китайские, математически символы и
др.). В этом нет ничего удивительного — ведь Роб Пайк, один из первоначальных архитекторов
Go, и был разработчиком системы кодирования UTF-8 для UNICODE представления.
Вот как это выглядит на тестовом примере (каталог hello), здесь демонстрируется
использование символов греческого алфавита как в именах переменных, так и в составе
символьных константных строк для вывода:
circle.go :
package main
import("fmt"; "os"; "strconv")
var π float64 = 3.1415926;
func main() {
bufer := make([]byte, 80)
for {
fmt.Printf("радиус вашего круга? : ")
длина, _ := os.Stdin.Read(bufer)
str := string( bufer[:длина - 1 ])
радиус, err := strconv.ParseFloat(str, 64)
if err != nil {

71

fmt.Println("ошибка ввода!")
continue
}
fmt.Printf("длина окружности 2*π*радиус = %f\n",
2*π*радиус)
}
}

Выполнение такого приложения:
$ ./circle
радиус вашего круга? : 11
длина окружности 2*π*радиус = 69.115037
радиус вашего круга? : .789
длина окружности 2*π*радиус = 4.957433
радиус вашего круга? : asd
ошибка ввода!
радиус вашего круга? : ^C

В сравнении с C или с C++, синтаксис объявления типов переменных «перевернут» (там где он
вообще требуется), в стиле языков Pascal, Modula-2 или Oberon. Ниже показаны примерно
эквивалентные объявления как они приведены в документации:
Go
var
var
var
var
var
var
var
var

v1
v2
v3
v4
v5
v6
v7
v8

int
string
[10]int
[]int
struct { f int }
*int
map[string]int
func(a int) int

C++
// int v1;
// const std::string v2;
// int v3[10];
// int* v4;
// struct { int f; } v5;
// int* v6;
// unordered_map* v7;
// int (*v8)(int a);

(примерно)
(примерно)
(но нет арифметики для указателей)
(примерно)

Объявления переменных можно группировать:
var (
i int
m float64
)

Но явно объявлять тип переменных, при такой строгости типизации, приходится, как ни странно,
достаточно редко — язык Go поддерживает автоматический вывод типов: переменная может
быть инициализирована при объявлении, её тип при этом можно не указывать, типом переменной
становится (выводится) тип присваиваемого ей значения (примерно то, что появилось в C++
только со стандарта C++11/14):
var v = *p

Но если переменная не инициализирована явно, должен быть явно указан её тип. В таком случае
переменной (не инициализированной) будет неявно присвоено нулевое значение,
предусмотренное для этого типа данных (0 для целочисленных переменных, nil для указателей,
свободное состояние для мютекса и так далее — для каждого типа существует своё значение, которое
толкуется как нулевое). В Go вообще не существует и не может быть не инициализированных
переменных.
Внутри функции короткий синтаксис присвоения локальным переменным значения с
автоматическим выводом типов напоминает обычное присваивание в Pascal:
v1 := v2 // аналог var v1 = v2

Вне функции (в глобальной области), каждая конструкция (данных) начинается с ключевого слова
(var, func и т.д.), а конструкция := является недопустимой:
package main
import "fmt"

72

var i, j int = 1, 2
func main() {
k := 3
c, python, java := true, false, "no!"
fmt.Println(i, j, k, c, python, java)
}

Так же, как множественные присвоения или множественные возвраты из функций (см. далее),
допускается и множественная инициализация переменных:
var v1, v2 uint32 = 10, 20

В Go имеется некоторое ограниченное число ключевых зарезервированных слов, которые могут
употребляться только в свойственном им контексте, и не могут быть использованы в качестве
имён переменных (или любых других объектов). Это обычная практика для большинства языков
программирования. Вот ключевые слова (их очень немного):
break
default
func
interface
select

case
defer
go
map
struct

chan
else
goto
package
switch

const
fallthrough
if
range
type

continue
for
import
return
var

В языке Go есть, кроме того, много предопределенных идентификаторов (имена типов,
логические константы, встроенные функции...). В программах допускается создавать
собственные идентификаторы с этими именами, совпадающими с именами предопределенных
идентификаторов, хотя это не всегда может быть целесообразным. Вот предопределённые имена
(их перечень может расширяться с развитием версии):
append
complex
error
int
iota
panic
rune

bool
complex64
false
int8
len
print
string

byte
complex128
float32
int16
make
println

cap
copy
float64
int32
new
real

close
delete
imag
int64
nil
recover

Повторные декларации и переприсвоения
Показанная чуть выше запись v1 := v2 вводит объявление новой переменной v1. В то время,
как запись v1 = v2 присваивает значение ранее существующей переменной v1:
v1 = v2 // присвоить существующей переменной v1 значение переменной v2

Посмотрим пример, написанный по мотивам обсуждений на сайте GoLang:
revar.go :
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("revar")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
print(&err, "\n")
d, err := f.Stat()
if err != nil {

73

println(err)
f.Close()
os.Exit(2)
}
println(&err)
print("файл открыт:\n")
fmt.Println(d)
f.Close()
os.Exit(0)
}
$ ./revar
0xc200004160
0xc200004160
файл открыт:
&{revar 27963 509 {63544826375 172386509 0x7fbbe4d83c00} 0xc200023000}

Посмотрим как работает оператор := — краткая форма декларации переменной. Вызов
os.Open() объявляет две новые переменные f и err. А немногими строками ниже следует
оператор:
d, err := f.Stat()

Он выглядит так, как если бы объявляются новые переменные d и err. Хотя, обратите внимание,
что имя err фигурирует в обеих декларациях. Такое дублирование является законным: err
объявляется первым оператором, но только вновь переприсваивается вторым. Это означает, что
вызов f.Stat() использует существующую переменную err, объявленную ранее, и просто
присваивает ей новое значение. (Это подчёркивает и специально сделанный вывод адреса
размещения переменной err до и после присвоения.)
В := декларации, переменная v может появляться повторно (даже если она уже была объявлена)
в случаях если:
- это предыдущее объявление v сделано в той же программной единице, что и новое объявление
(если v была уже объявлена во внешней области, то новая декларация позволит создать новую
переменную с тем же именем!);
- использовано соответствующее значение в инициализации присваиваем для v (по типу);
- существует по крайней мере ещё одна другая переменная в декларации, которая объявляется
заново.
Это необычное свойство — это чистый прагматизм, который делает легким использование одной
единственной переменной err, например, в длинной цепочке утверждений if-else. В Go мы видим
это часто.

Константы
В Go константы должны не иметь типа. Это применимо даже для констант, объявленных с
помощью const, если в объявлении не указано типа, а инициализирующее выражение
использует только запись выражений без типа. Значение константы без типа становится
типизированным при использовании в контексте, который требует типизированное значение (это
очень напоминает препроцессорные константы C, макросы), например, присвоение константы
переменной. Это позволяет пользоваться константами относительно свободно и не требует явного
преобразования типов:
var a uint
f(a + 1) // Численная константа без типа - "1" становится типа uint

Язык не налагает ограничений по размеру численных констант без типа или константных
выражений. Ограничение применяется только в том месте, где при использовании константы
потребуется для неё тип.
const huge = 1 > 98)

Go не поддерживает перечислений enum. Вместо этого можно использовать специальное
зарезервированное имя iota в одном объявлении const, чтобы получить набор
увеличивающихся значений. Когда в const опущено инициализирующее выражение, повторно
используется предыдущее выражение.
const (
red = iota
blue
green
)

// red
== 0
// blue == 1
// green == 2

Имя iota может использоваться и в любом другом произвольном контексте в определении
констант (но не в каком-то другом) — это специальный счетчик, значение которого увеличивается
при каждом его последующем упоминании в пределах файла кода:
const {
a, b = iota, iota;
}

Константы a и b получат значения 0 и 1 соответственно. Так как iota может быть неявно
повторяемой для одного или нескольких выражений, то легко можно строить сложные наборы
значений. Вот совсем не очевидный пример (types/iota.go в архиве примеров), который
приводят непосредственно авторы Go:
iota.go :
package main
import "fmt"
type ByteSize float64
const (
_
= iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 = cap(arr) { // недостаточно места
nar := make([]int, n*2) // выделение вдвое больше
arr = arr[0:cap(arr)]
for i := range arr {
nar[i] = arr[i]
}
arr = nar
}
arr = arr[0:n]
arr[n-1] = n
for _, a := range arr {
print(a, " ")
}
print(" : ")
}
}
$ ./array1
len=0 cap=0
+/- ? : 1
1 : len=1 cap=2
+/- ? : 3
1 0 0 4 : len=4 cap=8
+/- ? : 4
1 0 0 4 0 0 0 8 : len=8 cap=16
+/- ? : -5
1 0 3 : len=3 cap=16
+/- ? : 8
1 0 3 4 0 0 0 8 0 0 11 : len=11 cap=16
+/- ? : ^C

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

Двухмерные массивы и срезы
Массивы и срезы Go одномерные. Для создания эквивалентов 2D массивов и срезов необходимо
определить массив-массивов или срез-срезов, подобно следующим:
type Transform [3][3]float64
type LinesOfText [][]byte

// a 3x3 array, really an array of arrays.
// a slice of byte slices.

Поскольку срезы имеют переменную длину, то возможно иметь для каждого отдельного среза
различную длину. Это может быть достаточно общая ситуация, как в таком вот варианте, где
каждая строка переменной типа LinesOfText (срез срезов) имеет независимую длину:
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}

79

Иногда бывает необходимо разместить 2D срез, в ситуациях которые могут возникнуть при
обработке сканированных линий пикселей, например. Существует два способа достижения этой
цели. Один — это выделить каждый срез самостоятельно. Другой — это выделить единый массив
и указывать отдельные срезы из него. Что использовать зависит от конкретики приложения. Если
срезы должны увеличиваться или уменьшаться, то они должны быть выделены независимо,
чтобы избежать перезаписи следующей строки. Если нет, то может быть более эффективным
создать объект с единым распределения. Для справки, вот эскизы двух методов.
Во-первых, строка за строкой (срез срезов):
// Allocate the top-level slice.
// One row per unit of y.
picture := make([][]uint8, Ysize)
// Loop over the rows, allocating the slice for each row.
for i := range picture {
picture[i] = make([]uint8, Xsize)
}

А во-вторых, как альтернатива, теперь как единое выделение, нарезанное линиями:
// Allocate the top-level slice, the same as before.
// One row per unit of y.
picture := make([][]uint8, Ysize)
// Allocate one large slice to hold all the pixels.
// Has type []uint8 even though picture is [][]uint8.
pixels := make([]uint8, Xsize * Ysize)
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
picture[i], pixels = pixels[:Xsize], pixels[Xsize:]
}

Структуры
Структуры трактуются как набор полей — это общее место всех языков программирования:
struct1.go :
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() { fmt.Println(Vertex{1, 2}) }
$ gccgo -g struct.go -o struct
$ ./struct 1
{1 2}

Доступ к полям структуры производится через точку, '.'. Go имеет указатели, но не допускает
арифметики указателей (в таком случае уместнее было бы указатели называть ссылками, как
делают в Java, например, но авторы Go используют наименование указатель). При использовании
указателя на структуру также применяется '.', вместо '->' или '*' используемых в C и C++. С
точки зрения синтаксиса, структура и указатель на структуру используются в Go одинаково:
type myStruct struct { i int }
var v9 myStruct
// v9 является структурой
var p9 *myStruct
// p9 указатель на структуру
f(v9.i, p9.i)
// поле структуры, указатель на поле структуры

80

Всё это хорошо видеть на примере:
struct2.go :
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
p := Vertex{}
fmt.Println(p)
q := &p
q.X = 1e9
p.Y = -3
fmt.Println(p)
}
$ ./struct2
{0 0}
{1000000000 -3}

Выражение new(T) размещает новый обнулённый экземпляр типа T (в Go не бывает не
инициализированных значений) и возвращает указатель на него:
var t *T = new(T)
t := new(T)

Как это происходит показано на примере:
struct3.go :
package main
import "fmt"
type Vertex struct {
X, Y int
}
func main() {
v := new(Vertex)
fmt.Println(v)
v.X, v.Y = 11, 9
fmt.Println(v)
}
$ ./struct3
&{0 0}
&{11 9}

Структуры (и типы образуемые из структур) могут иметь не именованные поля. Такие поля
называются встраиваемыми, в отличие от именованных, называемых агрегированными.
Например:
import "color"
...
type ColoredPoint struct {
color.Color // Анонимное (безымянное) поле (встраивание)
x, y int
// Именованные поля (агрегирование)
}

81

Все операции, допустимые для типа встраиваемого поля, автоматически допустимы
непосредственно и для значений типа структуры, в которую встроено поле. Если создать значение
типа ColoredPoint (например, как: point := ColoredPoint{}), то его поля будут
именоваться как point.Color, point.x и point.y.
Это имеет существенное значение для объектно-ориентированной модели Go, из которой
исключено понятие наследования типов, и это будет показано при рассмотрении этой объектной
модели. Это, фактически, альтернативная замена понятия наследования.

Таблицы (хэши)
Хэш-таблицы хорошо известны, например, из языка Python или привнесены библиотеками STL в
стандарт C++ (со стандарта C++11 перенесено как составная часть языка). В Go таблицы
вводятся как встроенный тип языка. Ниже приведен пример создания хеш-таблицы:
var mp = map[string] float {"first":1, "second":2.0001}

Таблицы могут инициализироваться как и структуры, но нужно обязательное указание ключа для
каждого элемента:
map1 :
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
func main() {
fmt.Println(m)
}
$ ./map1
map[Google:{37.42202 -122.08408} Bell Labs:{40.68433 -74.39967}]

Хэш-таблицы можно и не инициализировать, тогда у нас образуется пустая таблица не
содержащая (ещё) элементов. Обращаться к элементам можно как в ассоциативном массиве в
PHP: mp["second"]. Понятно, что массив подобен хэш-таблице с типом ключа int, а хэштаблицы можно условно рассматривать как массивы, индекс которых может иметь произвольный
тип.
В Go существует удобная форма for: range для итерации по значениям строки, массива или
хеш-таблицы (то есть по любым перечислимым, индексируемым типам), как показано ниже
(range — это одно, из не так многих, ключевых слов в Go, которое не может быть использовано
ни в каком другом качестве):
for key, value := range mp {
fmt.Printf("key %s, value %g\n", key, value)
}

Основные операции над таблицами (назовём таблицу условно m):

82

1) вставить новый или обновить значение уже существующего элемента:
m[key] = elem

2) возвратить значение элемента:
elem = m[key]

3) удалить элемент из таблицы:
delete(m, key)4) проверить присутствие элемента с заданным ключом — присвоение 2-х значений:
elem, ok = m[key]

В последней операции: если ключ присутствует в таблице, ok возвращается true. Если нет — то
ok устанавливается false, а значение elem — нулевое значение в соответствии с типом
элементов таблицы. Вообще, когда читается из таблицы элемент с отсутствующим ключом,
возвращается нулевое значение в соответствии с типом элементов:
map2 :
package main
import "fmt"
func main() {
m := make(map[string]int)
m["Answer"] = 42
fmt.Println("The value:",
m["Answer"] = 48
fmt.Println("The value:",
delete(m, "Answer")
fmt.Println("The value:",
v, ok := m["Answer"]
fmt.Println("The value:",
}
$ ./map2
The value:
The value:
The value:
The value:

m["Answer"])
m["Answer"])
m["Answer"])
v, "Present?", ok)

42
48
0
0 Present? false

Динамическое создание переменных
В Go есть встроенная функция new(), которая принимает тип и выделяет пространство в куче
под объект такого типа. Выделяемое пространство будет инициализировано нулем для данного
типа. Например, new(int) выделит новый int в куче (а не в стеке как при объявлении
локальной переменной), инициализирует его значением 0 и вернет его адрес, который имеет тип
*int. В отличии от C++, new() это функция, а не оператор, поэтому запись new int приведет к
синтаксической ошибке.
Покажется удивительным, но new() не часто используется в Go программах. В Go взятие адреса
переменной всегда безопасно и не может создать висячий указатель. Если программа где-то
использует адрес переменной, она будет размещаться в хипе столь долго, сколько это будет
необходимо (пока сохраняется последняя ссылка на эту переменную). Поэтому вот такие
функции эквивалентны:
type S { I int }
func f1() *S {
return new(S)
}
func f2() *S {
var s S
return &s
}
func f3() *S {

83

// More idiomatic: use composite literal syntax.
return &S{}
}

В противоположность этому, в C/C++ всегда опасно возвращать адрес локальной переменной
(размещённой в стеке, и уничтожаемой вместе со стеком):
// C++
S* f2() {
S s;
return &s;
}

// INVALID -- contents can be overwritten at any time

Значения словарей (map) и каналов (channel) должны выделяться с помощью встроенной
функции make(). Переменная с типом map или channel без инициализации будет автоматически
инициализировано nil. Вызов make(map[int]int) вернет новую переменную типа map[int]
int (таблица целых значений, индексируемых целочисленным индексом). Отметим, что make()
возвращает значение, а не указатель. Это согласуется с тем фактом, что значения map и channel
передаются по ссылке. Вызов make() с типом map принимает необязательный аргумент,
обозначающий зарезервированный объем словаря. Вызов make() с типом channel принимает
необязательный аргумент, который устанавливает объем буфера канала (по умолчанию равен 0 —
не буферизируемый канал, что будет разобрано позднее):
mp = map([string]int, 200)
ch = map(chan int, 3)

Функция make() может также использоваться сразу для выделения среза. В таком случае, будет
выделена память и под соответствующий базовый массив, и возвращен срез, ссылающийся на
массив. Требуется один аргумент — количество элементов среза. Второй, необязательный, это
объем среза. Например, make([]int, 10, 20) аналогично new([20]int)[0:10]. Так как в Go
реализована сборка мусора, новый выделенный массив будет уничтожен только (и сразу) после
того, как не останется ссылок на возвращаемый таким make() срез.

Конструкторы и составные литералы
Функция new() создает объект, инициализированный нулями. Не всегда это подходящий вариант.
Но в Go не предоставляется конструкторов и деструкторов для структур и объектов класса. Если
нужно что-то вроде конструктора, то следует создать конструирующую функцию. Например, как
это делается в стандартном пакете с именем os:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File);
f.fd = fd;
f.name = name;
f.dirinfo = nil;
f.nepipe = 0;
return f;
}

Но можно сократить количество лишних операций за счет составных литералов:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0};
return &f;
}

84

Две последних строки этого варианта можно еще подсократить. Они эквивалентны следующей
строке:
return &File{fd, name, nil, 0};

В составном литерале все атрибуты должны указываться в порядке их перечисления в исходной
структуре (позиционное указание полей). Но можно использовать и перечисления вида
field:value — указываются только необходимые атрибуты (ключевое указание полей), а
остальные обнуляются:
return &File{fd:fd, name:name}

В предельном случае запись new(File) эквивалентна записи &File{} — все атрибуты получают
нулевые значения.
Составные литералы могут использоваться для инициализации массивов, слайсов и map-ов:
// Массив, чей размер определяется автоматически по содержимому
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// Это слайс
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// Это map (хэш-таблица)
m := map[int]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

Операции
Go допускает множественные инициализации и присваивания, выполняемые параллельно для
нескольких переменных:
i, j := k, m

// Инициализировать новые переменные

Оператор ':=' определяет новую переменную, вводит её в пространство имён задачи и
инициализирует её указанным значением. Оператор ' =' выполняет присвоение значения (сколь
угодно структурированного) уже ранее существующей переменной, или только-что объявленной
в var с явным указанием типа (что тоже означает уже существующую переменную):
var β, ω complex128 = 0.0 + 1i, math.Pi

Это позволяет, например, обменивать значения переменных в одном операторе:
i, j, k = j, k, i

// Циклически обменять местами значения i, j и k

В Go не требуются круглые скобки вокруг условия в выражениях if, условий для выражения for
или значения выражения в switch:
for θ := 0; θ < n ; θ++ {
if β == 0 {
...
}
}

Но, с другой стороны, требуется обязательно заключать в фигурные скобки тело выражений if и
for, даже если это тело состоит всего из одного оператора:
if a < b {f()}
if(a < b) {f()}
if a < b f()
for i = 0; i < 10; i++ {}
for(i = 0; i < 10; i++) {}

//
//
//
//
//

Корректно
Корректно
НЕКОРРЕКТНО
Корректно
НЕКОРРЕКТНО

Подобно циклу for, оператор if также может начинаться с короткого утверждения,
выполняемого раньше проверки условия. Переменные, объявленные в таких утверждениях имеют
область существования только до конца if оператора. Переменные, объявленные в ветке if,
могут также использоваться и в ветке else:
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {

85

return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// can't use v here, though
return lim
}

В Go вообще нет ни выражения while, ни выражения do { ... } while. Выражение for
может быть использовано с одним условием, что делает его аналогичным выражению while:
sum := 1
for sum < 1000 {
sum += sum
}

Если же условия вообще опущены, то будет создан бесконечный цикл, из которого выходим по
оператору break:
for {
...
if ... { break }

}

В выражении switch, метки case не являются проходными (здесь радикальное отличие от C/C+
+, Python … да и большинства других языков программирования — за этим нужно внимательно
следить). Вы можете сделать их проходными с помощью ключевого слова fallthrough. Это
применяется даже для смежных альтернатив:
switch i {
case 0:
case 1:
f()
}

// пустое тело case, ничего не делать
// f не вызовется, когда i == 0!

Также в case может быть объявлено несколько значений.
switch i {
case 0, 1:
f()
// f будет вызвана если i == 0 || i == 1.
}

Значения переменной (i) в case не обязательно должны быть константами, или даже целыми
числами. Любые виды типов и выражений, которые поддерживает оператор сравнения, — такие,
как строки или указатели, — могут быть использованы. И если значение для switch вообще
опущено, по умолчанию типом условия становится true, а значениями в ветках case становятся
логические условия:
switch {
case i < 0:
f1()
case i == 0:
f2()
case i > 0:
f3()
}

Оператор switch может использоваться и для динамической диагностики типа интерфейсной
переменной (run-time рефлексия). Такой type switch использует синтаксис type утверждения
типа с помощью ключевого слова type внутри скобок (и это единственное использование такого
ключевого слова). Если switch объявляет переменную в выражении, то эта переменная будет
иметь соответствующий свой тип в каждой альтернативе. Если использовать это имя в ветвях
выбора case, то эффектом будет объявление новой переменной с тем же именем, но с разным

86

типом в каждом конкретном случае:
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t)
//
case bool:
fmt.Printf("boolean %t\n", t)
//
case int:
fmt.Printf("integer %d\n", t)
//
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) //
case *int:
fmt.Printf("pointer to integer %d\n", *t) //
}

%T prints whatever type t has
t has type bool
t has type int
t has type *bool
t has type *int

Постфиксные операторы ++ и -- могут быть использованы только в утверждениях, но не в
выражениях. Вы не можете написать c = *p++. Выражение *p++ воспринимается, как (*p)++, а
не как *(p++). Префиксных операций ++p и --p в языке Go вообще не существует.
Операции имеют различающиеся приоритеты в Go и C++. Как итог, вычисление одного и того же
выражения 7 & 3 & &^
+ - | ^
== != < >=
&&
||

В C++ приоритеты операций (показаны только релевантные операции, имеющие эквиваленты в
Go):
1. * / %
2. + 3. >
4. < >=
5. == !=
6. &
7. ^
8. |
9. &&
10. ||

Функции
Функции объявляются при помощи ключевого слова func. После (списка) параметров в скобках
указываются типы возвращаемых значений. В случае с одним возвращаемым значением скобки
не используются. Параметры функций и методов, и возвращаемые ними результаты объявляются
таким образом:
func f(i, j, k int, s, t string) string { ... }

Однотипные параметры, следующие друг за другом, могут объявляться списком (как показано
выше), но могут объявляться и индивидуально (что не типично для стиля Go):
func f(i int, j int, k int, s string, t string) string { ... }

Поскольку функция рассматривается Go как и любой объект данных, то функция может
объявляться присвоением (функциональной) именованной переменной тела функции. Позже по
этому имени будет вызываться функция, или передаваться в вызов других функций:
newfunc = func (n int) string { ... }
...

87

s := newfunc(1000)
...
oldfunc(newfunc)
...

Функции могут возвращать несколько (2 или более) значений, типы таких множественных
значений в определении функции заключаются в скобки:
func f(a, b int) (int, string) {
return a + b, "сложение"
}

Подобный подход устраняет необходимость передавать указатель на возвращаемое значение,
чтобы имитировать ссылочный параметра (в C++ это решается объявлением аргумента-ссылки:
func(int& x)). Вот простая функция, которая захватывает численное значение из позиции в
байтовом срезе, и возвращает преобразованное число и следующую позицию в срезе:
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x * 10 + int(b[i]) - '0'
}
return x, i
}

Такую функцию можно использовать для сканирования числовых значений во входном срезе b
подобно следующему:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}

Если несколько значений, возвращаемых функцией, должны присваиваться переменным, то их
перечисляем в присвоении через запятую:
first, second := incTwo(1, 2) // first = 2, second = 3

Если какое-то из множества возвращаемых значений не представляет интереса в контексте
конкретного вызова функции (игнорируется), то для этой переменной в списке присвоения
используется специальное имя _ (подчёркивание):
длина, _ := os.Stdin.Read(буфер) // 2-е значения (ошибка) игнорируется

В присвоении может использоваться больше одной таких не используемых переменных,
например, только для проверки потенциальной возможности (безошибочности) выполнения
операции:
_, _ := os.Stdin.Read(буфер) // оба значения игнорируются

В Go нет возбуждаемых исключений. Множественные возвращаемые функцией значения
являются в Go базовым механизмом для реакции на ошибки вызова:
func MySqrt(f float) (v float, ok bool) {
if f >= 0 { v, ok = math.Sqrt(f), true }
else { v, ok = 0, false }
return v, ok
}
...
result, ok := MySqrt(...)
if !ok {
// Something bad happened.
return nil
}

88

// Continue as normal.
...

Результаты возвращаемые функцией (хоть одиночный, хоть множественные), также как и входные
аргументы, могут быть именованными, тогда нет необходимости (но можно) их указывать при
возврате результатов из функции:
func incTwo(a, b int) (c, d int) {
c = a + 1
d = b + 1
return
}

Как и во всех языках семейства C, всё в Go передается по значению. То есть, функция всегда
получает копию того, что передавалось, так как если бы оператор присвоения присваивал
значение параметру перед вызовом. Например, при передаче значение int в функцию делается
копия int, а при передаче указателя делает копию указателя, но не данных, на которые он
указывает.
Таблицы и срезы ведут себя подобно указателям: они являются дескрипторами, которые содержат
указатели на базовую таблицу или срез. Копирование объекта таблицы или среза не копирует
данные, на которые они указывают. Копирование объекта интерфейса делает копию всего того,
что загружено в интерфейс. Если интерфейсный объект содержит структуру, то копируя объект
интерфейса — делаете копию структуры. Если интерфейсный объект содержит указатель, то
копируя значение интерфейса — делаете копию указателя, но опять-таки, не данных, на которые
он указывает.
Функция является таким же объектом как и любые другие данные, она может присваиваться
другим именованным переменным:
func1.go :
package main
import (
"fmt"
"math"
)
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3, 4))
}
$ ./func1
5

Этот пример попутно показывает, что в Go функции могут быть на произвольную глубину
вложены друг в друга (не видимы за пределами обрамляющей функции) 14. Это очень частая
практика при параллельном запуске сопрограмм (оператор go), когда функция определяется
локально в момент её запуска.
Более того, функция может быть создана даже без имени, анонимно, выполнена и тут же
утилизирована сборкой мусора15:
func2.go :
package main
import "fmt"

14 Это обычная практика для языков Pascal или Modula-2, но такое не допустимо в стандарте языков C и C++.
15 То что определяется как lambda-выражение в языках функционального программирования.

89

func main() {
sum := func(a, b int) int { return a + b }(3, 4)
fmt.Println(sum)
}
$ ./func2
7

Или даже так:
func main() {
fmt.Println(func(a, b int) int { return a + b } (3, 4))
}

Поскольку функция рассматривается Go как объект данных, язык позволяет реализовать многие
из приёмов функционального программирования (хотя анонимные функции — это уже сам по
себе трюк из функционального программирования). Один из таких приёмов, из области функций
высших порядков (выше 1-го, явно определяемого), является функциональное замыкание, или
просто замыкание (closure). Замыкание — это функциональный объект, который ссылается к
переменным вне тела самих этих функций (в этом смысле функция «привязана» к переменным).
Примечание: Дэвид Мертц приводит следующее определение замыкания: "Замыкание - это процедура вместе с
привязанной к ней совокупностью данных" (в противовес объектам в объектном программировании, которые по его же
словам: "данные вместе с привязанным к ним совокупностью процедур" ).

func3.go :
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(pos(i), neg(-2*i))
}
}

В этом примере функция adder() возвращает замыкание (возвращает функцию!).
Использующие это замыкание функциональные переменные pos и neg работают каждый со
своим экземпляром накапливающей переменной sum:
$ ./func3
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

90

Вариативные функции
Вариативная функция – это функция, которая в качестве единого аргумента принимает ноль,
одно или несколько значений16. Хотя вариативные функции используются не так часто, в
отдельных случаях они могут сделать ваш код чище и читабельнее.
Функция с параметром, которому предшествует многоточие (… — или можно так считать, что за
именем параметра следует многоточие), считается вариативной функцией. Многоточие означает,
что предоставленный параметр может принимать ноль, одно или несколько значений.
В функции может быть только один вариативный параметр – он обязательно должен быть
последним параметром, определенным в функции. Определение параметров в вариативной
функции без учета порядка приведет к ошибке компиляции.
Функция Go может принимать сразу список аргументов определённого типа (во многих случаях
это эквивалентно по возможностям функциям с переменным числом параметров в C, и
возможности C++ определения параметров вызова с значениями по умолчанию). Вот
синтаксический пример такой записи (все примеры этой главы находятся в каталоге function):
arglist.go :
package main
func Min(a ...int) int {
min := int(^uint(0) >> 1) // largest int
for _, i := range a {
print(i, " ")
if i < min {
min = i
}
}
print(" => ", min, "\n")
return min
}
func main() {
Min(-11)
Min(11, 7, 3)
Min(-1, 2, -3, 4, -5, 6)
}
$ ./arglist
-11 => -11
11 7 3 => 3
-1 2 -3 4 -5 6

=> -5

Более того, можно указать, что функция может принять произвольное число аргументов
произвольного типа (arbitrary type). А затем уже внутри такой функции динамически определить
последовательно тип каждого из параметров вызова, и произвести адекватные действия. Вот как
подобным образом определяется функция Printf() в пакете fmt:
func Printf(format string, v ...interface{}) (n int, err error)

Интерфейсному типу interface{} может быть присвоено любое значение. В теле функции
Printf() переменная v действует как переменная типа []interface{}, но если её нужно
дальше передать следующей другой функции с переменным числом аргументов (variadic), то эта
переменная выступает как обычный список аргументов. Вот возможная реализация вашей
функции xxx.Println(). Она передает свои аргументы непосредственно в fmt.Sprintln() на
фактическое форматирование:.
// Println осуществляет вывод на стандартный регистратор в манере fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output принимает параметры (int, string)
}

16 В некоторых языках программирования это называют «функция с переменным числом параметров».

91

Мы пишем здесь ... после параметра v во вложенном вызове fmt.Sprintln(), чтобы
сообщить компилятору, что нужно рассматривать v как список аргументов. Иначе v должен
рассматриваться как единичный параметр типа среза. (именно поэтому мы в примере выше
применяли к вариативному параметру оператор range).
Чтобы не усложнять объяснения сложным значащим примером, я приведу здесь пример,
заимствованный из одной из публикаций:
typelist.go :
package main
import (
"fmt"
"reflect"
)
func main() {
variadicExample(1, "red", true, 10.5, []string{"foo", "bar", "baz"},
map[string]int{"apple": 23, "tomato": 13})
}
func variadicExample(i ...interface{}) {
for _, v := range i {
fmt.Println(v, "--", reflect.ValueOf(v).Kind())
}
}
$ ./typelist
1 -- int
red -- string
true -- bool
10.5 -- float64
[foo bar baz] -- slice
map[apple:23 tomato:13] — map

Здесь в коде всё ясно без объяснений. Но этот фрагмент приведен для того, чтобы он мог быть
использован в качестве образца того, как а). посредством пакета reflect (рефлексия периода
выполнения) диагностировать тип переменной и б). затем использовать этот тип как
переключатель ветвей switch, как это показывалось немногим ранее.

Стек процедур завершения
Еще одна возможность Go, стоящая особняком, но непосредственно связанная с вызовами
некоторых функций — это оператор defer:
defer foo();

Здесь конструкция defer регистрирует функцию завершения, и функция foo() будет вызвана по
достижении return в вызывающей единице (это может быть и главная функция main() или
любая другая функция). Если defer используется в функции несколько раз, то соответствующие
методы выстраиваются в стек и будут выполняться при завершении функции в порядке, обратном
их помещению.
Утверждение defer может быть использовано для указания финальных действий, которые нужно
выполнить при завершении вызывающей единицы кода (функции, главной программы):
fd := open("filename")
defer close(fd)

// fd будет закрыт после завершения функции

Некоторые авторы считают, что польза от подобного нововведения не очевидна, особенно при
большом объеме кода. Так программисту, незнакомому с кодом, придется держать в памяти весь
этот стек вызовов со всеми параметрами. При использовании defer структурное

92

программирование не обеспечивается.
С другой стороны (противоположное мнение), это механизм, непосредственно повторяющий
логику стека процедур завершения потока pthread_t из стандарта POSIX 1003.b:
pthread_cleanup_push() и pthread_cleanup_pop(). И стандарт рекомендует к
использованию именно эти механизмы. При нескольких returnв теле функции defer будет
выполняться по любому из них, и тем самым гарантируется единообразность действий
завершения. Как завершающее действие в defer должен обязательно указываться вызов функции
(это не может быть оператор или последовательность операторов). Но это может быть и
непосредственный фрагмент кода, оформленный как анонимная функция (мы это уже умеем), по
типу такого:
defer func() { ... } ()

Аргументы отложенной функции (в defer) вычисляются когда определяется defer, а не когда
вызывается функция завершения. В документации приводится такой любопытный пример:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}

Откладывание функции в LIFO очередь (в каждом прохождении цикла), приведет к следующей
работе функции при завершении фрагмента кода и печати на экран: 4 3 2 1 0.

Обобщённые функции
Достаточно часто нужно создать «видовую» функцию (generic), которая выполняла бы
единообразные действия, но над данными разного типа. Простейшим примером такой функции
может быть Minimum() — поиск минимального значения среди полученных параметров вызова.
Но для различных типов данных смысл сравнения (>, 15
{{Green 3 5} 2} => 30

Механизм встраивания, дополненный возможностями интерфейсов, обеспечивает полную
функциональность наследования классов в C++ или Java.

Функции как объекты
Из того, что функции Go это производные типы объектов первого класса (точно так же как

101

элементарные типы данных) следует очень интересное свойство (вряд ли представленное в
других языках): к функции как к типу можно определить методы. И чем это объяснять, это проще
показать на предельно упрощённом примере:
objfun.go :
package main
import "fmt"
type Multiply func(...int) string
func (f Multiply) Apply(i int) Multiply {
return func(values ...int) string {
values = append([]int{i}, values...)
return f(values...)
}
}
func main() {
var multiply Multiply = func(values ...int) string {
var total int = 1
for _, value := range values {
total *= value
}
return fmt.Sprintf("%v => %d", values, total)
}
fmt.Println("multiply:", multiply(3, 4), "(expect 12)")
var times2 Multiply = multiply.Apply(2)
fmt.Println("times 2:", times2(3, 4), "(expect 24)")
// ... и можно даже каскадно применять к производным от Multiply объектам
times6 := times2.Apply(3)
fmt.Println("times 6:", times6(2, 3, 5, 10), "(expect 1800)")
}

В этом примере метод возвращает функцию, внутри которой вызывается функция базового типа
Multiply (в данном случае Apply() трансформирует параметры передаваемые вызову, а затем в
конце передаёт их на выполнение оригинальному Multiply, но это не обязательно и
взаимодействие может быть самым замысловатым):
$ ./objfun
multiply: [3 4] => 12 (expect 12)
times 2: [2 3 4] => 24 (expect 24)
times 6: [2 3 2 3 5 10] => 1800 (expect 1800)

Этот пример условный и упрощён. Но за ним стоит возможность построить целое семейство
родственных функций с модификацией поведения.

Интерфейсы
Там где в некоторых традиционных объектно ориентированных языках используются классы, в
Go задействованы интерфейсы. Интерфейсы Go, по своей сути, похожи на интерфейсы в языке
Java. При объявлении интерфейса в нем объявляется набор методов, и все (любые) типы,
реализующие этот интерфейс, совместимы с переменной этого интерфейса. Интерфейсы также
похожи и на абстрактные классы C++.
В Go каждый тип, предоставляющий все методы, обозначенные в интерфейсе, может
трактоваться как реализация интерфейса, никакого явного объявления для этого не требуется.
type myInterface interface {
get() int
set(i int)
}

102

Объявленный в предыдущем обсуждении (глава «Методы» выше) тип myType также реализует
интерфейс myInterface, хотя это нигде и не указано явно (сам тип myType об этом «не знает», и
когда мы там писали пример с типом myType, мы сами ещё не подозревали о существовании
интерфейса myInterface).
Интерфейс к конкретному типу определяет для него набор методов. Переменная интерфейсного
типа может принимать любое значение (переменную, объект), которое реализует эти методы:
object3.go :
package main
import (
"fmt"
"math"
)
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
fmt.Println(a.Abs())
a = &v // a *Vertex implements Abser
fmt.Println(a.Abs())
// In the following line, v is a Vertex (not *Vertex)
// and does NOT implement Abser.
// a = v
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
$ ./object3
1.4142135623730951
5

Переменная типа интерфейс Abser благополучно принимает как объект типа MyFloat (оператор:
a = f), так и указатель на объект *Vertex (оператор: a = &v). Но 3-й (комментированный)
случай присвоения не пройдёт компиляцию из-за синтаксической ошибки: тип Vertex не
удовлетворяет интерфейсу Abser потому что Abs() метод определен только для *Vertex, но не
для Vertex.
Тип реализует интерфейс путем реализации методов этого интерфейса. Нет никакой явной

103

декларации о намерениях. Подобным образом интерфейсы отделяют пакеты реализации от
пакетов, которые определяют интерфейсы: одни не зависят от других. Этот механизм также
вынуждает определять точные интерфейсы, потому что вы не должны искать все реализации и
помечать их с новым именем интерфейса. В примере ниже:
- определяются типы интерфейсов Reader и Writer;
- интерфейсы объявляют методы Read() и Write();
- тем самым, переменные типов Reader и Writer (или объединённого интерфейса ReadWriter)
наследуют методы от типа File из пакета с именем os (реализующим штатный в составе Go
платформенно независимый интерфейс к операционным системам: https://pkg.go.dev/os)
- потому как именно тип File реализует объявленные интерфейсами операции:
func (f *File) Read(b []byte) (n int, err error)
func (f *File) Write(b []byte) (n int, err error)

- это позволяет присваивать переменным таких интерфейсных типов значения переменных из
пакета os:
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

- и для этих переменных интерфейсных типов применимы впоследствии все операции из пакета
fmt, например:
func Fprint(w io.Writer, a ...interface{}) (n int, err error)

- которые используют аргументы интерфейсного типа io.Writer (пакет io — базовые
примитивы ввода-вывода: https://pkg.go.dev/io), который также наследует (является обёрткой)
для базового типа File и его метода Write() (который был показан 3-мя пунктами выше):
type Writer interface {
Write(p []byte) (n int, err error)
}

Вот так замыкается вся цепочка связей. И нам нет необходимости определять полную
реализацию операций ввода-вывода для переменных своего нового определяемого типа
ReadWriter:
object4.go :
package main
import (
"fmt"
"os"
)
type Reader interface {
Read(b []byte) (n int, err error)
}
type Writer interface {
Write(b []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}

104

func main() {
var w ReadWriter
// os.Stdout implements Writer
w = os.Stdout
fmt.Fprintf(w, "hello, writer\n")
}
$ ./object4
hello, writer

Именование интерфейсов
По соглашению (разработчиков Go), интерфейсы объявляющие единственный метод называются
по имени этого метода плюс суффикс ...-er, или аналогичный простой модификатор,
позволяющий построить существительное: Reader, Writer, Formatter, CloseNotifier и др.
Уже существует ряд таких имен, произведенных в честь имен функций, которые они реализуют.
Read, Write, Close, Flush, String и так далее имеют каноническое написание и смысл. Чтобы
избежать путаницы, не давайте своему методу одно из таких имён, если только он не имеет ту же
самую сигнатуру и смысл. И наоборот, если ваш тип реализует метод с таким же смыслом, что и
метод широко известного типа, дайте ему то же имя и сигнатуру: вызывайте для конвертера
вашего типа в строку String(), но не ToString().

Контроль интерфейса19
Как мы видели выше, тип не нуждается в том, чтобы декларировать явно, что он реализует какойто интерфейс. Вместо этого, тип реализует интерфейс только с помощью того, что он реализует
методы для этого интерфейса. На практике, большинство интерфейсных преобразований
статические и поэтому проверяются во время компиляции. Например, передача *os.File
функции, ожидающей io.Reader не будет компилироваться до тех пор, пока *os.File не
реализует интерфейс io.Reader.
Но иногда проверка интерфейса требуется во время выполнения, тем не менее. Одним из
демонстрирующих экземпляров является пакет encoding/json, определяющий интерфейс
Marshaler. Когда JSON кодировщик получает значение, которое реализует этот интерфейс,
кодировщик вызывает метод Marshaler чтобы преобразовать его в формат JSON вместо того,
чтобы делать стандартное преобразование. Кодер проверяет это свойство во время выполнения
используя проверку типа, подобно следующему:
m, ok := val.(json.Marshaler)

Если необходимо только опросить является ли некоторый тип реализацией интерфейса,
фактически не используя сам интерфейс, то возможно только проверять ошибку, используя
пустой идентификатор для игнорирования результата проверки:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}

В одном из тонких случаев, в такой ситуации возникает проблема, когда необходимо
гарантировать, в рамках реализации пакета, что тип на самом деле удовлетворяет интерфейсу.
Если тип — например, json.RawMessage — нуждается в индивидуальном представлении JSON,
то он должен реализовать json.Marshaler, но нет статических преобразований, которые мог бы
вызвать компилятор, чтобы это проверить автоматически. Если тип случайно не удовлетворяет
интерфейсу, JSON кодировщик по-прежнему будет работать, но не будет использовать
индивидуальную реализацию. Чтобы гарантировать что реализация корректная, глобальная
декларации с помощью пустого идентификатора может быть использована в пакете:
var _ json.Marshaler = (*RawMessage)(nil)

19 Заимствовано из книги «Effective Go».

105

В этой декларации присвоение с участием преобразования от *RawMessage к Marshaler
требует, чтобы *RawMessage реализовывал Marshaler, и что это обстоятельство будет
проверено во время компиляции. В случае, если json.Marshaler интерфейс изменится со
временем, то этот пакет больше не будет компилироваться, и мы будем знать, что он нуждается в
обновлении.
Использование пустого идентификатора в этой конструкции указывает на то, что декларация
существует только для проверки типа, но не для создания переменной. Не делайте этого для
каждого типа, который удовлетворяет интерфейсу, тем не менее. По соглашению, такие
объявления используются только когда отсутствуют статические преобразования, уже
присутствующие в коде, что является довольно редким событием.

Обработка ошибочных ситуаций
В Go нет возбуждаемых исключений. Это связано не со сложностями реализации, или
минималистичностью языка, но является твёрдой позицией его разработчиков. Вот их позиция:
Мы уверены, что сопряжение возбуждаемых исключений со структурами управления, таких как
try-catch-finally идиома, имеет своим результатом запутанный код. Оно также имеет
тенденцию стимулировать программистов к оформлению слишком многих самых элементарных
ошибок (например, из-за невозможности открыть файл) как возбуждение исключения.
Go использует другой подход. Для простоты обработки ошибок, Go предлагает
множественные возвращаемые значения, что делает лёгкой возможность сообщить об ошибке
не переписывая само возвращаемое значение. Встроенный тип error, в сочетании с другими
возможностями Go, делает обработку ошибок приятной, но и весьма отличающейся от других
языков.
Go также имеет несколько встроенных функций для сигнализации и восстановления из
действительно экстремальных ситуаций. Механизм восстановления выполняется только как
часть состояния функции, которая будет прервана в результате ошибки, которая достаточно
катастрофична. Но это не требует создания каких-то дополнительных структур управления и,
при правильном использовании, может привести в итоге к чистому коду обработки ошибок.
Ошибкой (https://pkg.go.dev/errors) в Go может быть всё, что может описать себя как строка
ошибки (это смешная фраза из документации). Идея реализуется предопределённым встроенным
интерфейсным типом error, с его единственным (требуемым к реализации) методом Error(),
который должен формировать и возвращать строку описания ошибки (каталог errors архива
примеров):
error1.go :
package main
import (
"fmt"
"time"
)
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s", e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}

106

}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
$ ./error1
at 2022-04-13 20:45:23.565002072 +0300 EEST m=+0.000083744, it didn't work

Из упоминавшихся, в мотивации принятой в Go идеологии обработки ошибок, «встроенных
функций для сигнализации и восстановления» имеются в виду уже упоминавшийся (при
рассмотрении функций) оператор defer и встроенные функции panic() и recover(),
описываемые ниже.
func panic(v interface{})
func recover() interface{}

Встроенная функция panic() прекращает нормальное выполнение текущей сопрограммы
(goroutine). Когда функция F() вызывает panic(), нормальное исполнение F() немедленно
прекращается. После прекращения выполняются какие-либо функции, исполнение которых было
отложено (defer) в функции F(), и затем F() возвращает управление её вызвавшему. Абонент G(),
вызывавший ранее F(), также ведёт себя точно так, как при прямом вызове panic():
прекращение исполнения G() и выполнение любых ею отложенных ( defer) функций. Это
продолжается (распространяется наверх) до тех пор, пока все функции в исполняемой
сопрограмме последовательно не будут остановлены в обратном порядке. В этот момент
программа (сопрограмма) завершается и регистрируется код ошибка, включающий, в том числе, и
значение аргумента первоначального вызова panic(). Эта последовательность завершений
называется паникованием, и может управляться с помощью встроенной функции recover().
Встроенная функция recover() позволяет программе управлять поведением паникующей
сопрограммы. Выполнение вызова recover() внутри любой из отложенных ( defer) функций
(но не в какой либо функции в коде непосредственно) прерывает распространение панической
последовательности вверх, восстанавливает нормальное исполнение и возвращает значение
error, переданное вызовом panic(). Если recover() вызовется вне отложенных функций, он
не сможет остановить паникующую последовательность. В этом случае, или когда сопрограмма
не паникует, или если аргумент вызова panic() был nil, recover() возвращает nil. Таким
образом, возвращаемое recover() значение является индикатором того, паникует ли
сопрограмма.
Не программисту объяснить такое невозможно, а программисту синтаксис всех таких
взаимодействий гораздо проще показать на примере кода, который заимствован из публикаций по
Go:
panic.go :
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")

107

g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
$ go build panic.go
$ ./panic
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Это чем-то похоже на структурную обработку исключений (SEH) в Windows, но в гораздо более
изящном исполнении.

Структура пакетов (библиотек) Go
Каждая программа и каждый подключаемый программный компонент в Go является пакетом (см.
объявление: package main в листингах всех примеров), а точнее — набором пакетов.
Программы собираются из пакетов, и то, что обычно называется библиотеками, в Go называется
набором пакетов.
Исполнимая программа должна иметь пакет main и метод main(), с которого и начинается
выполнение программы, и по завершении которого программа закрывается.
Каждая программа и каждый подключаемый программный компонент в Go является пакетом.
Программы начинают выполнение из пакета main.
Программа может использовать (импортировать) функциональность
(программных компонент) импортируя пакет, объявляя оператор import:

других

пакетов

package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}

Эта программа использует пакеты с импортируемых путей "fmt" и "math/rand" (заданы
символьными константами). По соглашению, имя пакета совпадает с последним элементом
файлового пути импорта.

108

Выражение import "package" дает доступ к методам, переменным и константам пакета
package (только видимым вне пакета, имена которых начинаются с большой буквы).
Обращаться к ним следует через оператор . (точка), например, package.Foo().
Предыдущая показанная запись импорта, так как в этой записи каждая строка пути пакета
завершается неявным ';', эквивалента такой форме её записи (с явным разделителем ';'):
import("fmt"; "math/rand")

Также можно записывать множественные описатели импорта:
import "fmt"
import "math/rand"

Область видимости в Go заметно отличается от таковой в языке С. Внутри пакета все
переменные, функции, константы и типы имеют глобальную видимость, даже если пакет
представлен несколькими файлами. Но для обращения к ним из клиента, импортирующего этот
пакет, имена последних должны начинаться с большой буквы. Например, клиент импортировал
пакет package, в котором объявлены следующие переменные:
const hello = "Im not visible in client, just in package" //видна только в пакете
const Hello = "I can say hello to client" //видна также при импорте

Вторая константа будет доступна клиенту по имени package.Hello. Первая будет недоступна
вне рамок самого пакета package вовсе.
Вы можете как создавать свои собственные целевые пакеты как составные части крупного
разрабатываемого проекта, так и, наверняка, использовать API предоставляемый набором
стандартных пакетов, предоставляемых с системой Go.
Полную иерархия стандартных пакетов (необходимую для указания путевых имён в импорте) в
вашей установленной версии GoLang, и откомпилированных в форму объектных архивов
(статических библиотек вида *.a) вы можете найти в своей системе по полному путевому имени
/lib/go/pkg/linux_amd64 (здесь компонент имени go — это просто ссылка на актуальную
установленную версию, например, go-1.18 в моём случае ... но при корректной инсталляции
GoLang номер версии не имеет значения в этом путевом имени):
$ ls -l /lib/go
lrwxrwxrwx 1 root root 7 мар 23

2022 /lib/go -> go-1.18

Полная иерархия файлов (пакетов) необходимая для правильного указания путевых имён в
строках импорта — очень важна, она очень объёмна, но стоит того, чтобы её изучить детально
приступая к работе:
$ pwd
/lib/go/pkg/linux_amd64
$ ls -w80
archive
bufio.a
bytes.a
cmd
compress
container
context.a
crypto
crypto.a

database
debug
embed.a
encoding
encoding.a
errors.a
expvar.a
flag.a
fmt.a

go
hash
hash.a
html
html.a
image
image.a
index
internal

io
io.a
log
log.a
math
math.a
mime
mime.a
net

net.a
os
os.a
path
path.a
plugin.a
reflect
reflect.a
regexp

regexp.a
runtime
runtime.a
sort.a
strconv.a
strings.a
sync
sync.a
syscall.a

testing
testing.a
text
time
time.a
unicode
unicode.a
vendor

$ tree | grep \.a$ | wc -l
442

Вот такое число пакетов (терминальные файлы *.a этого дерева) вы получаете в распоряжение
своей исполнимой системой (с учётом версии, естественно):
$ tree | head -n25
.

109

├──


├──
├──
├──



















archive
├── tar.a
└── zip.a
bufio.a
bytes.a
cmd
├── asm

└── internal

├── arch.a

├── asm.a

├── flags.a

└── lex.a
├── compile

└── internal

├── abi.a

├── amd64.a

├── arm64.a

├── arm.a

├── base.a

├── bitvec.a

├── deadcode.a

├── devirtualize.a

├── dwarfgen.a

├── escape.a

$ tree | tail

└── cpu.a
└── text
├── secure

└── bidirule.a
├── transform.a
└── unicode
├── bidi.a
└── norm.a
119 directories, 442 files

И относительно каждой терминальной вершины мы можем видеть что это статический
библиотечный архив (и именно в таком путевом виде пакеты должны указываться в строках
описания импорта):
$ file math.a
math.a: current ar archive
$ file net/rpc/jsonrpc.a
net/rpc/jsonrpc.a: current ar archive

Подобная же иерархия пакетов, но для реализации gccgo (если вы используете Go проект GCC)
находится совсем в другом месте, и она, в основном, соответствует тому что показано выше:
$ cd /lib/x86_64-linux-gnu/go/11/x86_64-linux-gnu

Здесь элемент пути «11» определяется версией:
$ gccgo --version
gccgo (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Вы можете убедиться, что дефаултная версия gccgo будет та же, что и для всех продуктов
проекта GCC в вашей системе:
$ gcc --version

110

gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Состав здесь (gccgo) несколько отличается (от GoLang), отличается и формат представления
пакетов:
$ ls -w80
archive
bufio.gox
bytes.gox
compress
container
context.gox
crypto
crypto.gox
database
debug

embed.gox
encoding
encoding.gox
errors.gox
expvar.gox
flag.gox
fmt.gox
go
hash
hash.gox

html
html.gox
image
image.gox
index
internal
io
io.gox
log
log.gox

math
math.gox
mime
mime.gox
net
net.gox
os
os.gox
path
path.gox

reflect.gox
regexp
regexp.gox
runtime
runtime.gox
sort.gox
strconv.gox
strings.gox
sync
sync.gox

syscall.gox
testing
testing.gox
text
time
time.gox
unicode
unicode.gox

$ tree | wc -l
188
$ tree | tail

└── template.gox
├── time

└── tzdata.gox
├── time.gox
├── unicode

├── utf16.gox

└── utf8.gox
└── unicode.gox
35 directories, 150 files

Как мы уже видели относительно GoLang, число терминальных файлов — это и есть число
доступных в этом релизе пакетов Go. Относительно формата пакетов (здесь это *. gox):
$ file math.gox
math.gox: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), stripped

Можно предположить (?) что по формату это динамические (разделяемые) библиотеки, в отличие
от статических, используемых GoLang.
Наконец, основные оригинальные пакеты проекта GoLang в виде самых свежих исходных кодов,
если возникает необходимость, вы можете найти для изучения на сайте разработчиков проекта —
https://go.dev/src/. Например, в каталоге https://go.dev/src/fmt/ мы находим все необходимые
файлы из уже неоднократно использовавшегося пакета fmt:
doc.go export_test.go fmt_test.go format.go
print.go scan.go scan_test.go stringer_test.go

И можете текстуально изучить реализационную часть пакета чтобы разрешить тонкие детали:
print.go :
...
func (b *buffer) Write(p []byte) (n int, err error) {
*b = append(*b, p...)
return len(p), nil
}
func (b *buffer) WriteString(s string) (n int, err error) {
*b = append(*b, s...)
return len(s), nil

111

}
func (b *buffer) WriteByte(c byte) error {
*b = append(*b, c)
return nil
}
...

doc.go :
/*
...
General:
%v
%#v
%T
%%

the value in a default format.
when printing structs, the plus flag (%+v) adds field names
a Go-syntax representation of the value
a Go-syntax representation of the type of the value
a literal percent sign; consumes no value

...
*/

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

Функция init
Каждый исходный файл может (но вовсе не обязательно) определить свою собственную initфункцию без аргументов, чтобы настроить все требуемые начальные состояния. (На самом деле
каждый файл может иметь даже несколько функций init().) И выглядит это так: init()
вызывается после того, как все объявленные переменные в пакете пройдут инициализацию, что
может произойти только после того, как все импортированные пакеты будут инициализированы.
Помимо этого, все инициализации, которые не могут быть выражены в виде деклараций —
обычное использование init-функции: проверить и восстановить корректность состояний
программы перед тем, как начнётся реальное выполнение.
Вот пример из документации использования init-функции:
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath may be overridden by --gopath flag on command line.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

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

Импорт для использования побочных эффектов
Ранее объяснялось, что неиспользование в коде импортируемых пакетов — грубая ошибка,

112

прерывающая компиляцию. Временно эту ситуацию решают «пустые идентификаторы» (имя —
символ подчёркивания), как объяснялось, но их наличие маркирует код как «находящийся в
развитии, черновой», и, в конце концов, они должны быть удалены.
Но иногда полезно импортировать пакет только для использования его побочных эффектов, без
какого-либо прямого использования. Например, в коде своей функции init(), пакет
net/http/pprof регистрирует обработчики HTTP-данных, которые содержат информацию для
отладки. Пакет имеет экспортируемый API, но большинству клиентов нужна только регистрация
обработчика и доступ к данным через веб-страницу. Чтобы импортировать пакет только для
использования его побочных эффектов, предлагается переименовать пакет в пустой
идентификатор:
import _ "net/http/pprof"

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

Некоторые полезные и интересные стандартные пакеты
Выше описан состав и иерархия некоторых стандартных пакетов системы Go и алгоритм поиска
информации в ней. Как легко видеть, это очень объёмная (и всё расширяющаяся по ходу
развития) система. Обзор пакетов системы поимённо вы найдёте в документации: Standard
library — https://pkg.go.dev/std.
Пакеты GoLang, конкретно установленные в вашей системе, находятся в каталоге (иерархии
каталогов) /usr/lib/go/pkg/linux_amd64. Их число рано:
$ tree /usr/lib/go/pkg/linux_amd64 | grep \.a | wc -l
494

Изучите предварительно, перед работой, состав стандартных пакетов Go, предоставляемых вашей
версией GoLang.
Ниже будут бегло затронуты только отдельные пакеты Go из этой иерархии, по минимуму,
которые автору показались особо необходимыми для целей экспериментирования с Go. Этот
выбор очень субъективный.
По затрагиваемым пакетам показываются лишь ключевые понятия, позволяющие представить
назначение, состав и направления использования пакетов. Детальное описание было бы слишком
объёмным. Но по каждому пакету даётся URL страницы с полным и детальным описанием.

Пакет runtime
Пакет времени выполнения содержит переменные, константы и функции, которые повязаны с
взаимодействием кода с исполняющей системой Go, такие, например, как функции управления
сопрограммами.
Например, переменная GOGC устанавливает начальный процент для срабатывания сборшика
мусора Go. Сборка мусора инициируется когда соотношение свежих выделенных данных к
данным, оставшихся в живых после предыдущей сборки мусора, превысит этот процент. По
умолчанию GOGC=100. Установка GOGC=off вообще запрещает работу сборщика мусора.
Изменить величину GOGC позволяет функция из пакета runtime/debug:
func SetGCPercent(percent int) int

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

113

Функция из пакета runtime/debug:
func SetMaxStack(bytes int) int

Эта функция задает максимальный объем памяти, который может использоваться одной
сопрограммой под стек. Если любая сопрограмма превышает этот предел при росте её стека, то
программа завершает работу. Функция возвращает предыдущее установленное значение.
Установка по умолчанию составляет: 1 Гб на 64-битных системах, 250 Мб на 32-битных
системах. Функция SetMaxStack() полезна, главным образом, для ограничения ущерба,
наносимого сопрограммами, когда они входят в бесконечную рекурсию.
Это были только некоторые примеры из пакета runtime. Полную информацию вы найдёте по
ссылке: https://pkg.go.dev/runtime.

Форматированный ввод-вывод
Без форматированного ввода вывода не обходится ни одна программа, начиная с «Hello World!».
Поэтому именно такой пакет мы рассматриваем в первую очередь. Эти возможности собраны в
пакет fmt, и содержат функции аналогичные printf(), sprintf() и scanf() из C. Само
понятие форматной строки заимствовано из C, но сделано проще, и расширено функционально.
Большинство элементов формата следуется привычным из C (%d, %b, %x, %c, %s, %f, %e, %p ...).
Но есть и весьма специфичные:
- %v — форматирование в умалчиваемом (default) представлении для каждого типа данных
(для структур добавление флага плюс: %+v — будет добавлять имена полей)
- %t — представление логических значений как true или false
...
Пакет определяет достаточно много функций форматированного ввода-вывода. Вот только
некоторые (для примера):
- вывод в формате по умолчанию:
func Print(a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

- вывод в явно определяемом формате:
func Printf(format string, a ...interface{}) (n int, err error)

- сканирование текста со стандартного ввода:
func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)

- форматированный вывод в строку:
func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string

Обращаем внимание на то, что функции Print() и Printf() близки к своим аналогам в C, но
вот функции Sprint() и Sprintf() имеют совершенно другую семантику: они не
модифицируют строку указанную 1-м параметром (как в C за счёт побочного эффекта), а
возвращают новую строку как результат. Это связано с тем, что в Go строки неизменяемые.
Чтобы пояснить это не очень внятное объяснение, зацитирую одну строку из примеров кода
находящихся в архиве (там множество подобных строк для того чтобы их рассмотреть):
return fmt.Sprintf("[%02d,%X]", p.n, p.t)

Кроме большого набора функций форматного ввода-вывода, пакет содержит целый ряд
интересных определений (интерфейсных) типов, например:
type Stringer interface {
String() string
}

Интерфейс Stringer реализуется любым новым типом (вашим собственным!), который имеет

114

определение метода String(), и именно этот метод будет определять «родной» формат ( %v, см.
выше) для значений (объектов) этого типа. Метод String() используется для печати значений,
передаваемых в качестве операнда в любой формат, который принимает строку, или в не
форматированный вывод, такие как Print() или Println(). Пример реализации интерфейса
Stringer для нового определяемого типа point (2D точка) показан в примере triangle.go
(находится в каталоге архива compare/triangle):
type point struct {
xy complex128
}
func (p *point) String() string {
// формат вывода
return fmt.Sprintf("[%.2f,%.2f] ", real( p.xy ), imag( p.xy))
}

Вот как может выглядеть индикация типа элемента данных, что может оказаться очень полезным
при отладке:
fmt.Printf("%T | %T | %T\n ", "строка", 1, 2.2)

Результат:
string | int | float64

Примеры использования функция форматного ввода-вывода раскиданы во множестве по всем
кодам архива примеров, поэтому не будут показываться отдельно.
Детальную информацию по пакету fmt вы найдёте по ссылке: https://pkg.go.dev/fmt.

Строки и пакет strings
Практически ни одно современное приложение не обходится без использования символьных
строк и разного уровня глубины обработки этих строк. В ранних языках программирования,
FORTRAN, а позже C и далее C++, акценкт техники программирования делался на
вычислительных аспектах, а текстовым строкам отводилась роль, главным образом, для
представления строчных литералов, украшающих результаты вывода на терминал или печать,
форматирующих их, и делающих более понятными. Поэтому обработка символьной информации
— это самое слабое место языков C и C++ (в C++ оно как-то обходится использованием
шаблонной реализации типа std::string из STL — внешними относительно языка средствами).
Язык Go предоставляет встроенный (или как они это называют типы 1-го уровня) тип string.
Но с годами актуальность текстовой обработки возрастала всё более и более, её доля становилась
превалирующей относительно самих вычислительных аспектов, а способы обработки
становились всё изощрённее: поиск, замена, регулярные выражения… Что вызвало даже
появление языков программирования заметно ориентированных на текстовую обработку: Perl,
Python...
Разработчики Go это учли, и язык предоставляет разнообразнейшие средства обработки
текстовой информации. Как уже отмечалось, в Go всё представляется в UNICODE в кодировке
UTF-8: сама запись программного кода, имена переменных, содержимое символьных строк. А раз
это так, то средства символьной обработки Go решают все проблемы мультиязычности. Важно
отметит, что это относится не только к текстам на самых разнообразных языках мира, но, с тем
же успехом и
инструментарием, к любым последовательностям из разных областей
деятельности…
Вот как это формулируется в статье относительно последовательностей-палиндромов (примеры
которых мы будем рассматривать в последнем разделе, посвящённом примерам кодов):
В молекулах ДНК присутствует от 100 тысяч до 1 млн коротких палиндромных
последовательностей. Принцип их формирования несколько отличается от того, как это
происходит для слов и предложений. Поскольку молекула ДНК состоит из двух
комплементарных цепочек нуклеотидов, а нуклеотиды всегда соединяются одним и тем же
образом (адеин (А) с тимином (Т), цитозин (С) с гуанином (G)), считается, что одноцепочечная
последовательность ДНК является палиндромом, если она равна своей комплементарной
последовательности, прочитанной задом наперёд. Например, последовательность ACCTAGGT

115

является палиндромной, так как комплементарной ей будет последовательность TGGATCCA,
которая совпадает с исходной, прочитанной задом наперёд.
Или (там же) относительно последовательностей музыкальных нотных знаков:
Пьесу играют «как обычно», но после того, как она заканчивается, ноты переворачивают и
произведение играют заново, причём музыка не изменится. Итераций может быть сколько
угодно и неизвестно, что является верхом, а что низом. Такие произведения можно играть
вдвоём, читая ноты с разных сторон. Примерами таких музыкальных палиндромов могут
являться произведения «Застольная мелодия для двоих» Моцарта и «Путь Мира» Мошелеса, а
также «Прелюдия и постлюдия» из фортепианного цикла Пауля Хиндемита «Ludus tonalis».
Наконец,

строка

может

быть

набор

просто

символов-значков

(каталог

примеров

strings/runes), по типу смайликов, не относящихся ни к какому языковому алфавиту

(последний UNICODE символ в 1-й строке, 🔥 — это вообще символ расширения имён файлов
исходных кодов на новом языке программирования Mojo):
smail.go :
package main
import "fmt"
func main() {
fmt.Println("😊 🦰 🔥")
fmt.Println(string([]rune{0x1F468, 0x1F9B0, 0x2318})) // 👨 🦰 ⌘
}
$ go build smile.go
$ ./smile
😊 🦰 🔥
👨🦰⌘

В большинстве своём, все (осмысленные) строки Go встроенного типа string, с которыми мы
зачастую будем иметь дело — это последовательность символов UNICODE в кодировке UTF-8
неограниченной длины. Но, в самом общем случае, строка Go может содержать любую
последовательность байт, которая может не соответствовать вообще никаким символам
UNICODE / UTF-8. Строго говоря, строки Go представляют собой срезы (слайсы) произвольных
байт, и доступны эти срезы только для чтения (это ещё одна, и более точная формулировка
неизменяемости строк Go).
Любопытства ради, и в подтверждение сказанного, можем рассмотреть такое маленькое
приложение (каталог strings/runes):
nulstr.go :
package main
import "fmt"
func main() {
buf := make([]byte, 10)
fmt.Printf("%v\n", buf)
str := string(buf)
fmt.Printf("%x\n", []rune(str))
}
$ go run nulstr.go
[0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0]

Здесь мы создали строку, состоящую исключительно из байт с нулевыми значениями! Это может
быть большой неожиданностью для программистов привыкших к правилам C/C++!
К строкам применима встроенная функция len() (которая вообще очень широко используется в
языке). Но функция len() работает с байтами, но не символами UTF-8 строки, это очень важно
помнить!

116

Над значениями типа string выполнимы основные операции:
+ — конкатенация, объединение содержимого 2-х строк в одну новую строку
[] — индексация, выборка из строки по порядковому номеру, индексация начинается с номера 0.
Операция индексации строки производится по байтам. Но выборка вовсе не означает, что этот
отдельный байт строке можно изменить — строки в Go неизменяемые! Пример этого (каталог
примеров strings/runes):
chars.go :
package main
import "fmt"
func main() {
sample := "русская строка"
fmt.Printf("[%d]: %s", len(sample), sample); println()
fmt.Printf("%x", sample); println()
for i := 0; i < len(sample); i++ {
fmt.Printf("%x ", sample[i])
}
println()
for i := 0; i < len(sample); i++ {
fmt.Printf("%c ", sample[i])
}
println()
}
$ ./chars
[27]: русская строка
d180d183d181d181d0bad0b0d18f20d181d182d180d0bed0bad0b0
d1 80 d1 83 d1 81 d1 81 d0 ba d0 b0 d1 8f 20 d1 81 d1 82 d1 80 d0 be d0 ba d0 b0
Ñ Ñ Ñ Ñ Ð º Ð ° Ñ
Ñ Ñ Ñ Ð ¾ Ð º Ð °

Пересчитав символы в текстовой строке (14) мы убеждаемся что 27 (len()) — это число байт в
строке, но не символов. Ниже мы видим типичное 2-байтовое представление (d1 80 …) символов
кирилических шрифтов.
Не следует смешивать понятие строки string и байтового среза массива, в который обычно
считывается последовательность вводимых символов (из файлов или терминала). Но они легко
преобразуются один в другой (фрагмент из примера goproc/multy.go в архиве):
buf := make([]byte, 1024)
for {
fmt.Printf("> ")
n, _ := os.Stdin.Read(buf)
str := string(buf[:n - 1])
...
}

Для приведения в соответствие байтового и символьного представления Go вводит
целочисленное представление символов, тип rune. Стандарт UNICODE использует термин
«кодовая точка» («code point») для обозначения элемента, представленного одним значением.
Кодовая точка U+2318 с шестнадцатеричным значением 2318, например, представляет символ
японского алфавита ⌘. Но «кодовая точка» — это нечто вроде труднопроизносимого слова,
поэтому стандарт Go вводит более короткое понятие: руна ( rune). Именно rune соответствует
одному мультиязычному символу UNICOD, независимо от того, сколькими символами в этом
языке представляется символ. Но rune это нисколько не символьный тип, это числовой тип, над
которым можно производить, например, арифметические операции (что мы увидим вскоре). При
выводе в виде содержимой строки срезы rune нужно приводить к типу string, что мы и видим
в примере (каталог strings/runes):
strrune.go :
package main

117

import
import
import
import

"fmt"
"bufio"
"log"
"os"

func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
s := scanner.Text()
fmt.Println(s)
r := []rune(s)
fmt.Println(r)
// hex int32
fmt.Printf("%U\n", r) // UNICODE
fmt.Println(string(r))
fmt.Println("----------------------------")
}
if err := scanner.Err(); err != nil {
log.Println(err)
}
}
$ ./strrune
ASCII string
ASCII string
[65 83 67 73 73 32 115 116 114 105 110 103]
[U+0041 U+0053 U+0043 U+0049 U+0049 U+0020 U+0073 U+0074 U+0072 U+0069 U+006E U+0067]
ASCII string
---------------------------русская строка
русская строка
[1088 1091 1089 1089 1082 1072 1103 32 1089 1090 1088 1086 1082 1072]
[U+0440 U+0443 U+0441 U+0441 U+043A U+0430 U+044F U+0020 U+0441 U+0442 U+0440 U+043E U+043A
U+0430]
русская строка
----------------------------

В пакете strings, который мы рассматриваем, представлено великое множество операций и
методов над строками, практически на все случаи жизни (это один из самых больших пакетов
Go). В плотном контакте с пакетом strings находятся и ряд других пакетов стандартной
библиотеки Go (https://pkg.go.dev/std), с которыми приходится взаимодействовать при
написании кода строчной обработки:
bytes — опер операции над байтовыми срезами;
bufio — строчный ввод-вывод (файлы, теримнал…);
regexp — регулярные выражения;
strconv — константы разнообразные преобразования типов в строки и наоборот;
unicode — константы и предикаты отдельных категорий символов UNICODE;
Все эти пакеты (их использование) будут показаны в последней части книги, когда мы перейдём к
конкретным примерам решения задач.
Некоторое насальное представление о строчных операциях Go, соотношении строк с байтовыми
массивами и операциях вывода строк даёт следующий пример (каталог types):
string1.go :
package main
import (
"fmt"
)

118

func main() {
var (
s1
string = "это первая русскоязычная строка "
s2
string = "и вторая строка"
s5
= "it is a short english string"
sfmt
= "[%d]: %s\n"
)
fmt.Printf(sfmt, len(s1), s1)
fmt.Printf(sfmt, len(s5), s5)
buf := make([]byte, 120)
for i := range s1 {
buf[i] = s1[i]
}
fmt.Printf("[%d]: ", len(buf))
fmt.Println(buf)
buf = []byte(s1)
fmt.Printf("[%d]: ", len(buf))
fmt.Println(buf)
s3 := string(buf)
fmt.Printf(sfmt, len(s3), s3)
fmt.Printf("[%d]: %s \n", len(s1+s2), s1+s2)
fmt.Printf("%c => %d\n", s5[0], s5[0])
fmt.Printf("%c => %d\n", s1[0], s1[0])
//
s5[ 0 ] = '+'
buf = []byte(s5)
buf[0] = byte('+')
fmt.Printf("%s\n", buf)
}
$ go run string1.go
[60]: это первая русскоязычная строка
[28]: it is a short english string
[120]: [209 0 209 0 208 0 32 208 0 208 0 209 0 208 0 208 0 209 0 32 209 0 209 0 209 0 209 0 208
0 208 0 209 0 208 0 209 0 209 0 208 0 208 0 209 0 32 209 0 209 0 209 0 208 0 208 0 208 0 32 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0]
[60]: [209 141 209 130 208 190 32 208 191 208 181 209 128 208 178 208 176 209 143 32 209 128 209
131 209 129 209 129 208 186 208 190 209 143 208 183 209 139 209 135 208 189 208 176 209 143 32
209 129 209 130 209 128 208 190 208 186 208 176 32]
[60]: это первая русскоязычная строка
[88]: это первая русскоязычная строка и вторая строка
i => 105
Ñ => 209
+t is a short english string

Как показывает этот пример:
Хотя и декларируется представление UTF-8 для символьной информации (и с ним отлично
работает операция конкатенации '+'), функция len() (длина строки) и индекс символа в строке
('[]') оперируют с байтами, но не символами UNICODE. Это аналогично ситуации с char[] в
языке C, и использованием мультибайтовых функций типа mblen() и других mb*(), и требует
перехода к представлению в широких символах wchar_t.
Если вы попробуете изменить символ в строке (попробуйте!), оператором типа:
s5[0] = 22

То в таком случае получите ошибку вида:
./string1.go:25: cannot assign to s5[0]

Потому, что строки string в Go неизменяемые. Это в точности напоминает решение той же
проблемы в Python: или строки должны представляться простым нуль-терминальным массивом в
манере C со всеми вытекающими «удобствами», либо строки должны быть неизменяемыми в
своём внутреннем содержании.

119

Но вы вполне можете сделать:
buf = []byte(s5)
buf[0] = byte('+')
fmt.Printf("%s\n", buf)

И иметь в итоге в выводе (но это уже тип не string!):
...
+t is a short english string

Для посимвольной работы со строками может с успехом использоваться цикл в форме итератора,
как и для всяких агрегатных данных Go (каталог types):
string3.go :
package main
import "fmt"
func main() {
for pos, char := range "строка+\x80+Ф" { // \x80 is an illegal UTF-8 encoding
fmt.Printf("символ %#U в байтовой позиции %d\n", char, pos)
}
}
$ go build -o string3string3.go
$ ./string3
символ U+0441
символ U+0442
символ U+0440
символ U+043E
символ U+043A
символ U+0430
символ U+002B
символ U+FFFD
символ U+002B
символ U+0424

'с'
'т'
'р'
'о'
'к'
'а'
'+'
'�'
'+'
'Ф'

в
в
в
в
в
в
в
в
в
в

байтовой
байтовой
байтовой
байтовой
байтовой
байтовой
байтовой
байтовой
байтовой
байтовой

позиции
позиции
позиции
позиции
позиции
позиции
позиции
позиции
позиции
позиции

0
2
4
6
8
10
12
13
14
15

Для строк итератор range делает для вас существенно больше работы, чем просто перебор
байтов строки — в он выбирает отдельные символы в кодировке UNICODE, анализируя UTF-8.
Если встречается байт, содержащий ошибочный код, не представимый в UTF-8, то цикл-итератор
заменяет его на фиксированное значение U+FFFD, ассоциированное с встроенным типом rune (в
терминологии Go rune — это числовое значение одного символа UNICODE, UTF-32, см. ниже).
Но кроме непосредственных возможностей встроенных операций для типа string (достаточно
обширных), язык Go сопровождается ещё пакетом символьной обработки strings, содержащем
много функций для операций со строками. Здесь содержаться все эквиваленты библиотеки C
и ещё более (показаны для иллюстрации только некоторые из более 40 прототипов):
func
func
func
func
...
func
func
...
func
func
...
func
...

120

Contains(s, substr string) bool
ContainsAny(s, chars string) bool
ContainsRune(s string, r rune) bool
Count(s, sep string) int
Fields(s string) []string
FieldsFunc(s string, f func(rune) bool) []string
Index(s, sep string) int
IndexByte(s string, c byte) int
Join(a []string, sep string) string

func
func
func
...
func
...
func
...

Repeat(s string, count int) string
Replace(s, old, new string, n int) string
Split(s, sep string) []string
Trim(s string, cutset string) string
TrimSpace(s string) string

В символьных операциях фигурирует тип rune. По определению — это псевдоним для int32 и
эквивалент int32 во всех отношениях. Этот тип используется, по соглашению, чтобы различать
символьные (UNICODE) значения (в каком-то смысле это эквивалентно широкому, 4-байтному
типу wchar_t в C) и целочисленного значения для вычислительных операций.
Несколько примеров использования функций из пакета strings, заимствованные из
документации пакета и собранные «под одной крышей» (каталог types), показаны в примере
(все коды Go показываются не в том виде, как они нам, возможно, «нравятся», а так как они
выглядят после форматирования их командой go fmt ...):
string2.go :
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
fmt.Printf("Fields are: %q\n", strings.Fields(" foo bar baz
"))
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
fmt.Printf("Fields are: %q\n", strings.FieldsFunc(" foo1;bar2,baz3...", f))
fmt.Println(strings.Index("chicken", "ken"))
fmt.Println(strings.Index("chicken", "dmr"))
f = func(c rune) bool {
return unicode.Is(unicode.Han, c)
}
fmt.Println(strings.IndexFunc("Hello, 世界", f))
fmt.Println(strings.IndexFunc("Hello, world", f))
s := []string{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
rot13 := func(r rune) rune {
switch {
case r >= 'A' && r = 'a' && r false
7 in [9 8 7 6 5 4] => true
8.3 in [9 8.1 7.2 6.3 5.4 4.5] => false
(7+2i) in [(9+0i) (8+1i) (7+2i) (6+3i)] => true

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

И, наконец, пример который мы уже упоминали выше при рассмотрении модулей — поиск
большего из 2-х значений:
max.go :
package main
import "fmt"
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a T, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max("Hello", "world"))
fmt.Println(Max(9, 4))
fmt.Println(Max(4.5, 5.4))
// fmt.Println(Max(8 + 1i, 7 + 2i))
}

Здесь ограничение типа (констрейнт) импортируется из соответствующего пакета, содержащего
достаточно много частных констрейнтов на разные случаи жизни (как импортировался сам пакет
обсуждалось в предыдущем разделе).
И в итоге:
$ go fmt max.go
max.go
$ go build max.go
$ ./max
world
9
5.4

А вот если вы раскомментируете последний вызов и попытаетесь сравнить комплексные
числовые значения, то получите ошибку компиляции вида:
$ go build max.go
# command-line-arguments
./max.go:17:17: complex128 does not implement constraints.Ordered (complex128 missing in ~int |
~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 | ~string)

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

130

соотношения больше-меньше (эти операции имеют смысл, например, для модулей комплексных
векторов, но не самих значений). И это убедительно показывает как работает механизм
ограничения типа в дженериках.
Наконец, в завершение, рассмотрим какие ограничения типов предоставляет этот импортируемый
пакет (его местонахождение в файловой системе мы нашли в предыдущем разделе):
$ grep ^type `go env GOPATH`/pkg/mod/golang.org/x/exp@v0.0.0-20230310171629-522b1b587ee0/
constraints/constraints.go
type Signed interface {
type Unsigned interface {
type Integer interface {
type Float interface {
type Complex interface {
type Ordered interface {

Источники информации
[1] Введение в систему модулей Go, 27 авг 2018 — https://habr.com/ru/post/421411/
[2] Введение в модули, 03.12.2021 — https://metanit.com/go/tutorial/5.2.php
[3] constraints — https://pkg.go.dev/golang.org/x/exp/constraints
[4] Tutorial: Getting started with generics — https://go.dev/doc/tutorial/generics
[5] Дженерики в языке Go, 2 июн 2021 — https://habr.com/ru/company/karuna/blog/552944/
[6] Введение в использование дженериков в Golang, 2 мая 2022 —
https://golang-blog.blogspot.com/2022/05/generics-intro.html
[7] Дженерики в Go — подробности из блога разработчиков, 29.03.22 —
https://itnan.ru/post.php?c=1&p=657853
[8] Golang пощупаем дженерики, 13 фев 2022 — https://habr.com/ru/post/651229/
[9] Дженерики могут замедлить ваш код на Go, 9 апр 2022 — https://habr.com/ru/post/660007/
[10] Брифинг по дженерикам Go 1.18, 27 фев 2023 — https://habr.com/ru/company/otus/blog/719262/

131

Часть 2. Конкурентность и многопроцессорность
На момент создания Go и 30-40 лет до того подавляющее большинство находящихся в обиходе
компьютеров были однороцессорные, и редкие серверные образцы представляли собой
конструкции с немногими (2-4) отдельными процессорными расположенными раздельно на
системной плате… Относительно архитектуры таких редких серверных экземпляров и сложился
термин и обозначение SMP (Symmetric MultiProcessing). В этот период превалирования
однопроцессорных архитектур, на протяжении нескольких десятков лет, параллелизм (в
практических, не теоретических целях) рассматривался как модель выполнения, когда
параллельные ветви развития вычисления чередовались во времени, конкурируя за доступ к
единственному процессу, и вытесняя друг-друга в пассивное состояние ожидания процессора
(квази-параллельное выполнение). Квази-параллельное выполнение не увеличивает (и даже,
зачастую, несколько несколько снижает) эффективность использования вычислительных
мощностей оборудования, но часто благотворно отображается на логической ясности решаемой
проблемы.
История развитие многоядерных процессоров (несколько автономных процессоров, на едином
кристалле, под единой крышкой чипа) начинается с 2000-х годов (в 1999 году анонсирован
первый двухъядерный процессор в мире — IBM Power4, предназначенный для промышленных
серверов). А массовый выпуск 2-х ядерных и начало их широкой доступности относится только к
2005 году. Собственно, история многоядерных процессоров начинается тогда, когда их
производители, из своих коммерческих интересов, поняли (опережая понимание этого рынком),
что эра повышения производительности за счёт повышения тактовой частоты — закончилась.
Закончилась потому, что при постоянном уменьшении масштаба литографии, в технологическом
процессе начали сказываться фундаментальные квантовомеханические ограничения. И
единственным путём увеличения производительности остался путь увеличения числа ядер на
одной процессорной подложке.
Но если у вас будет даже 200 или 1000 процессоров (ядер) — это вовсе ещё не значит, что ваша
программа механически станет хоть на йоту быстрее. Для этого программное обеспечение
должно быть написано в специальных техниках, позволяющих использовать под задачу более
одного процессора. Таким образом, стремительный прогресс в технологии железа потребовал
радикально пересмотреть принципы проектирования программного обеспечения. Но осознание
этого пришло не сразу, и в среде практиков относится, пожалуй, к 2010-2012 годам (лет на 5-7
позже начала производства многоядерных процессоров).
Но прежде нужно внимательно посмотреть на то, как и во что отдельные процессоры
отображаются в операционной системе Linux … раз уж мы говорим конкретно о Linux, хотя всё
то же, в основной мере, будет относиться и ко всем POSIX, UNIX-like операционным системам.

132

Процессоры в Linux
Основная информация о процессоре формируется ядром Linux в квази-файловой системе procfs,
рассмотрим что там есть для простейшего (который на сегодня можно найти) процессора (для
более развитых моделей вывода может быть во много раз больше):
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family
: 6
model
: 158
model name
: Intel(R) Celeron(R) CPU G3930 @ 2.90GHz
stepping : 9
microcode : 0xea
cpu MHz
: 800.058
cache size
: 2048 KB
physical id
: 0
siblings : 2
core id
: 0
cpu cores : 2
apicid
: 0
initial apicid
: 0
fpu
: yes
fpu_exception
: yes
cpuid level
: 22
wp
: yes
flags
: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36
clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art
arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64
monitor ds_cpl vmx est tm2 ssse3 sdbg cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt
tsc_deadline_timer aes xsave rdrand lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti
ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust smep erms
invpcid mpx rdseed smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm arat pln pts
hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d
bugs
: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs
itlb_multihit srbds
bogomips : 5799.77
clflush size
: 64
cache_alignment
: 64
address sizes
: 39 bits physical, 48 bits virtual
power management:
processor : 1
vendor_id : GenuineIntel
cpu family
: 6
model
: 158
model name
: Intel(R) Celeron(R) CPU G3930 @ 2.90GHz
stepping : 9
microcode : 0xea
cpu MHz
: 800.062
cache size
: 2048 KB
physical id
: 0
siblings : 2
core id
: 1
cpu cores : 2
apicid
: 2
initial apicid
: 2
fpu
: yes
fpu_exception
: yes
cpuid level
: 22
wp
: yes
flags
: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36
clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art
arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64
monitor ds_cpl vmx est tm2 ssse3 sdbg cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt

133

tsc_deadline_timer aes xsave rdrand lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti
ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust smep erms
invpcid mpx rdseed smap clflushopt intel_pt xsaveopt xsavec xgetbv1 xsaves dtherm arat pln pts
hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d
bugs
: cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs
itlb_multihit srbds
bogomips : 5799.77
clflush size
: 64
cache_alignment
: 64
address sizes
: 39 bits physical, 48 bits virtual
power management:

Видим, что объём информации более чем исчерпывающий в полноте, и именно оттуда
(/proc/cpuinfo) черпают информацию команды-утилиты Linux (да и любой желающий в своём
программном коде), которые предоставляют её в более компактном, читабельном виде:
$ lscpu
Архитектура:
x86_64
CPU op-mode(s):
32-bit, 64-bit
Порядок байт:
Little Endian
Address sizes:
39 bits physical, 48 bits virtual
CPU(s):
2
On-line CPU(s) list:
0,1
Потоков на ядро:
1
Ядер на сокет:
2
Сокетов:
1
NUMA node(s):
1
ID прроизводителя:
GenuineIntel
Семейство ЦПУ:
6
Модель:
158
Имя модели:
Intel(R) Celeron(R) CPU G3930 @ 2.90GHz
Степпинг:
9
CPU МГц:
800.067
CPU max MHz:
2900,0000
CPU min MHz:
800,0000
BogoMIPS:
5799.77
Виртуализация:
VT-x
L1d cache:
64 KiB
L1i cache:
64 KiB
L2 cache:
512 KiB
L3 cache:
2 MiB
NUMA node0 CPU(s):
0,1
Vulnerability Itlb multihit:
KVM: Vulnerable
Vulnerability L1tf:
Mitigation; PTE Inversion
Vulnerability Mds:
Mitigation; Clear CPU buffers; SMT disabled
Vulnerability Meltdown:
Mitigation; PTI
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl and
seccomp
Vulnerability Spectre v1:
Mitigation; usercopy/swapgs barriers and __user pointer
sanitization
Vulnerability Spectre v2:
Mitigation; Full generic retpoline, IBPB conditional, IBRS_FW,
STIBP disabled, RSB filling
Vulnerability Srbds:
Mitigation; Microcode
Vulnerability Tsx async abort:
Not affected
Флаги:
fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov
pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdt
scp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid
aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 sdbg cx16 xtpr pdcm pcid sse4_1
sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave rdrand lahf_lm abm 3dnowprefetch
cpuid_fault invpcid_single pti ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad
fsgsbase tsc_adjust smep erms invpcid mpx rdseed smap clflushopt intel_pt xsaveopt xsavec
xgetbv1 xsaves dtherm arat pln pts hwp hwp_notify hwp_act_window hwp_epp md_clear flush_l1d

Или так, кратко20:
$ inxi -Cxxx

20 Этой утилиты inxi в системе по умолчанию нет, но устанавливается стандартным способом из пакетной системы.

134

CPU:

Topology: Dual Core model: Intel Celeron G3930
bits: 64 type: MCP arch: Kaby Lake rev: 9 L2 cache: 2048 KiB
flags: lm nx pae sse sse2 sse3 sse4_1 sse4_2 ssse3 vmx bogomips: 11599
Speed: 800 MHz min/max: 800/2900 MHz Core speeds (MHz): 1: 800 2: 800

Все 3 показанные команды диагностики относятся, естественно, к одному и тому же процессору,
про который мы узнаём то, что важно для нашего дальнейшего рассмотрения: 64-битная
архитектура x86_64 (AMD), 2 физических ядра (2 процессора), с размером кеш-памяти верхнего
уровня (L3 cache) 2 MiB (важно для задач высокой производительности).

Процессоры, ядра и гипертриэдинг
Вообще то, каждое физическое ядро является полностью автономным независимым процессором,
но промышленные сервера, например, могут ещё иметь конструктивно несколько (2-4)
установленных чипов (микросхем, сокетов...) процессоров. Для целей нашего рассмотрения
многоядерные и многопроцессорные системы не представляют существенной разницы 21.
Но если мы посмотрим диагностику чуть более развитого (чем выше) процессора, то увидим
(почти наверняка для подавляющего большинства современных процессоров) что-то подобное
следующему:
$ inxi -Cxxx
CPU:
Topology: Dual Core model: Intel Core i5 660
bits: 64 type: MT MCP arch: Nehalem rev: 5 L2 cache: 4096 KiB
flags: lm nx pae sse sse2 sse3 sse4_1 sse4_2 ssse3 vmx bogomips: 26601
Speed: 1451 MHz min/max: N/A Core speeds (MHz): 1: 1451 2: 1502 3: 1478 4: 1716

Всё в порядке? 4 процессора (ядра)? Ура! … Не совсем так. При более внимательном
рассмотрении:
$ lscpu
Архитектура:
CPU op-mode(s):
Порядок байт:
Address sizes:
CPU(s):
On-line CPU(s) list:
Потоков на ядро:
Ядер на сокет:
Сокетов:
NUMA node(s):
ID прроизводителя:
Семейство ЦПУ:
Модель:
Имя модели:
Степпинг:
CPU МГц:
BogoMIPS:
Виртуализация:
L1d cache:
L1i cache:
L2 cache:
L3 cache:
NUMA node0 CPU(s):
...

x86_64
32-bit, 64-bit
Little Endian
36 bits physical, 48 bits virtual
4
0-3
2
2
1
1
GenuineIntel
6
37
Intel(R) Core(TM) i5 CPU
660
5
1996.993
6650.38
VT-x
64 KiB
64 KiB
512 KiB
4 MiB
0-3

@ 3.33GHz

Здесь ядер (процессоров) только 2 («Ядер на сокет»), но каждый из этих процессоров имеет 2
потока выполнения (гипертрэдинг, hyper-threading, HT). Это второе, сопутствующее физическому
ядру, логическое ядро при некоторых условиях может выполнять поток команд параллельно
основному физическому ядру. Технологию HT производитель Intel впервые применил в Pentium
21 Вообще то, многоядерные (несколько ядер процессора под одной крышкой) и многопроцессорные (конструктивно
различные процессоры) отличаются с точки зрения «обвязки», работы с прерываниями, использования
контроллеров прерываний APIC др. Но, с интересующей нас точки зрения производительности, эти отличия не
вносят такой уж существенной разницы.

135

4, но по настоящему широко стали применять (после некоторого перерыва) только в линии Core
i3/i5/i7 и в серверных процессорах. У производителя AMD есть своя, отличающаяся, технология
подобная HT.
Но особенность HT состоит в том, что более-менее существенного повышения суммарной
производительности пары (физическое + логическое ядра) наблюдается только при соблюдении
определённых условий … грубо состоящих в том, чтобы ядра в паре выполняли как можно более
разнородные задачи. Но и в этом случае, максимальный выигрыш производительности 2-х ядер с
HT оценивается самой Intel по максимуму в 10-30%, и то это наблюдается чаще всего в
серверных задачах22.
На некоторых задачах распределение вычислительной работы на все ядра (логические и
физические) может не только не увеличивать, а снижать итоговую производительность, причём
существенно, до 70% от максимальной, при условии использования только физических ядер
(реальныз процессоров)! (Такое наблюдается, в качестве тестинга, в майнинге криптовалют, в
частности, проверено для Monero … и, в общем случае, это, похоже, кроме самого характера
задач, радикально зависит от размеров кеш-памяти верхних уровней и числа процессоров,
которые эту память разделяют).
В связи с вышесказанным возникает потребность: а). выяснить какие процессоры (по
порядковому номеру в inxi и аналогичных утилитах) могут быть отобраны как физические
(реальные) ядра и б). как указать задаче какой набор процессоров (по номерам) использовать и
как это сделать?
Примечание: Зачем отбирать только физические ядра? На некоторых задачах, как показывает эксперимент,
использование числа CPU сверх числа физических ядер не увеличивает, а только уменьшает суммарную
производительность — вот экспериментальные данные производительности (число хэш в секунду) майнинга
криптовалюты Monero (только одна из наугад выбранных задач высокой нагрузки) от числа CPU на сервере с 20
физическими и 40 логическими ядрами:
10 CPU - 4666 H/s - 58.9%
12 CPU - 5564 H/s - 70.2%
16 CPU - 7174 H/s - 90.5%
20 CPU - 7920 H/s - 100%
40 CPU - 5754 H/s - 72.7%

Пик отчётливо наблюдается при при числе CPU совпадающем с числом физических ядер! Это связано по-видимому, с
конкуренцией (при экстремально высокой нагрузке) CPU за другие ресурсы, в частности за кэш-память различных
уровней: L3, L4 (если он есть) ...

Загадочная нумерация процессоров
Если знаешь, где искать, то
найдёшь скелет в любом шкафу.
Чак Паланик «Бойцовский клуб»
Смотрим ещё один серверный процессор, Xeon E3-1240 v3:
$ inxi -Cxxx
CPU:
Topology: Quad Core model: Intel Xeon E3-1240 v3
bits: 64 type: MT MCP arch: Haswell rev: 3 L2 cache: 8192 KiB
flags: avx avx2 lm nx pae sse sse2 sse3 sse4_1 sse4_2 ssse3 vmx bogomips: 54275
Speed: 3592 MHz min/max: 800/3800 MHz Core speeds (MHz):
1: 3592 2: 3592 3: 3592 4: 3592 5: 3592 6: 3592 7: 3592 8: 3592

Здесь системой диагностируюся 8 процессоров. Какие из этих 8-ми могут быть отобраны так,
чтобы использовать только физические (реальные) ядра? Здесь нам может помочь:
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
MAXMHZ
MINMHZ
0
0
0
0 0:0:0:0
да 3800,0000 800,0000
1
0
0
1 1:1:1:0
да 3800,0000 800,0000
2
0
0
2 2:2:2:0
да 3800,0000 800,0000
3
0
0
3 3:3:3:0
да 3800,0000 800,0000
4
0
0
0 0:0:0:0
да 3800,0000 800,0000

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

136

5
6
7

0
0
0

0
0
0

1 1:1:1:0
2 2:2:2:0
3 3:3:3:0

да 3800,0000 800,0000
да 3800,0000 800,0000
да 3800,0000 800,0000

Пары ядер (логические относительно физических — колонка CORE) здесь группируются в пары
так: 0 + 4, 1 + 5, 2 + 6, 3 + 7.
Важно: Может сложиться ложное представление что в оборудовании существуют некие
различающиеся «физические» и некие «логические» ядра, которые, и те и другие Linux
воспринимает как доступные ему процессоры. Это не так! Есть только симметричные пары
«логических» ядер (за счёт гипертрэдинга), принадлежащие одному аппаратному «физическому»
ядру. Это же хорошо можно видеть на диагностике сенсоров температурного нагрева процессоров
(потому что их здесь только 4, но не 8):
$ sensors coretemp-isa-*
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +80.0°C (high
Core 0:
+78.0°C (high
Core 1:
+80.0°C (high
Core 2:
+79.0°C (high
Core 3:
+75.0°C (high

=
=
=
=
=

+80.0°C,
+80.0°C,
+80.0°C,
+80.0°C,
+80.0°C,

crit
crit
crit
crit
crit

=
=
=
=
=

+100.0°C)
+100.0°C)
+100.0°C)
+100.0°C)
+100.0°C)

Поэтому, если на этом процессоре мы захотим использовать только его физически разделённые
ядра, то мы можем с одинаковым успехом взять либо 0, 1, 2, 3 либо 4, 5, 6, 7… но точно также
можно использовать и 0, 1, 6, 7 или 4, 5, 2, 3 и т.д.
По показанному выше выводу lscpu может показаться что порядок нумерации процессоров
следующий: сначала одна половинка пары логических ядер, относящихся к физическим, а только
затем соответствующие им вторые половинки.
Но посмотрим уже рассматривавшийся ранее 2-ядерный процессор Intel Core i5:
$ inxi -Cxxx
CPU:
Topology: Dual Core model: Intel Core i5 660
bits: 64 type: MT MCP arch: Nehalem rev: 5 L2 cache: 4096 KiB
flags: lm nx pae sse sse2 sse3 sse4_1 sse4_2 ssse3 vmx bogomips: 26601
Speed: 1324 MHz min/max: N/A Core speeds (MHz): 1: 1324 2: 1961 3: 1702 4: 1879

Здесь:
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
0
0
0
0 0:0:0:0
да
1
0
0
0 0:0:0:0
да
2
0
0
1 1:1:1:0
да
3
0
0
1 1:1:1:0
да

Большая неожиданность! Здесь сначала нумеруется пара для 1-го физического ядра, а только
затем — для 2-го, это полная противоположность тому что мы только что видели выше.
Точно такая же разносортица, от модели к модели, и у процессоров AMD, поэтому приводить
детально её здесь избыточно (это замечено и обсуждается в обсуждениях в сообществах).
И, наконец, посмотрим процессоры (2 физически раздельных сокета, процессорных чипов)
сервера промышленного уровня DELL PowerEdge R420:
$ inxi -Cxxx
CPU:
Topology: 2x 10-Core model: Intel Xeon E5-2470 v2
bits: 64 type: MT MCP SMP arch: Ivy Bridge rev: 4 L2 cache: 50.0 MiB
flags: avx lm nx pae sse sse2 sse3 sse4_1 sse4_2 ssse3 vmx bogomips: 192104
Speed: 2800 MHz min/max: 1200/3200 MHz Core speeds (MHz): 1: 2800 2: 2800
3: 2800 4: 2800 5: 2800 6: 2800 7: 2800 8: 2800 9: 2800 10: 2800 11: 2801
12: 2800 13: 2800 14: 2804 15: 2804 16: 2800 17: 2797 18: 2800 19: 2803
20: 2800 21: 2801 22: 2801 23: 2800 24: 2800 25: 2802 26: 2801 27: 2800
28: 2800 29: 2800 30: 2800 31: 2800 32: 2801 33: 2800 34: 2800 35: 2800

137

36: 2800 37: 2800 38: 2800 39: 2800 40: 2800

Здесь 40 процессоров (ядер), которые нумеруются так (SOCKET — конструктивно отдельный чип
SMP, CORE — номер ядра):
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
MAXMHZ
MINMHZ
0
0
0
0 0:0:0:0
да 3200,0000 1200,0000
1
1
1
1 1:1:1:1
да 3200,0000 1200,0000
2
0
0
2 2:2:2:0
да 3200,0000 1200,0000
3
1
1
3 3:3:3:1
да 3200,0000 1200,0000
4
0
0
4 4:4:4:0
да 3200,0000 1200,0000
5
1
1
5 5:5:5:1
да 3200,0000 1200,0000
6
0
0
6 6:6:6:0
да 3200,0000 1200,0000
7
1
1
7 7:7:7:1
да 3200,0000 1200,0000
8
0
0
8 8:8:8:0
да 3200,0000 1200,0000
9
1
1
9 9:9:9:1
да 3200,0000 1200,0000
10
0
0
10 10:10:10:0
да 3200,0000 1200,0000
11
1
1
11 11:11:11:1
да 3200,0000 1200,0000
12
0
0
12 12:12:12:0
да 3200,0000 1200,0000
13
1
1
13 13:13:13:1
да 3200,0000 1200,0000
14
0
0
14 14:14:14:0
да 3200,0000 1200,0000
15
1
1
15 15:15:15:1
да 3200,0000 1200,0000
16
0
0
16 16:16:16:0
да 3200,0000 1200,0000
17
1
1
17 17:17:17:1
да 3200,0000 1200,0000
18
0
0
18 18:18:18:0
да 3200,0000 1200,0000
19
1
1
19 19:19:19:1
да 3200,0000 1200,0000
20
0
0
0 0:0:0:0
да 3200,0000 1200,0000
21
1
1
1 1:1:1:1
да 3200,0000 1200,0000
22
0
0
2 2:2:2:0
да 3200,0000 1200,0000
23
1
1
3 3:3:3:1
да 3200,0000 1200,0000
24
0
0
4 4:4:4:0
да 3200,0000 1200,0000
25
1
1
5 5:5:5:1
да 3200,0000 1200,0000
26
0
0
6 6:6:6:0
да 3200,0000 1200,0000
27
1
1
7 7:7:7:1
да 3200,0000 1200,0000
28
0
0
8 8:8:8:0
да 3200,0000 1200,0000
29
1
1
9 9:9:9:1
да 3200,0000 1200,0000
30
0
0
10 10:10:10:0
да 3200,0000 1200,0000
31
1
1
11 11:11:11:1
да 3200,0000 1200,0000
32
0
0
12 12:12:12:0
да 3200,0000 1200,0000
33
1
1
13 13:13:13:1
да 3200,0000 1200,0000
34
0
0
14 14:14:14:0
да 3200,0000 1200,0000
35
1
1
15 15:15:15:1
да 3200,0000 1200,0000
36
0
0
16 16:16:16:0
да 3200,0000 1200,0000
37
1
1
17 17:17:17:1
да 3200,0000 1200,0000
38
0
0
18 18:18:18:0
да 3200,0000 1200,0000
39
1
1
19 19:19:19:1
да 3200,0000 1200,0000

Чередование достаточно сложное (но понятное, чтобы его описывать словесно). Из него можно
выделить 2 подмножества (из многих возможных, вообще то говоря) номеров процессоров, когда
будут использовать только физические ядра: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19 или, с тем же успехом, могут быть использованы 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
32, 33, 34, 35, 36, 37, 38, 39.
Предварительные итоги по выбору процессоров для максимально производительного выполнения
могут выглядеть так:
Последовательная нумерация процессоров меняется в зависимости от производителя,
семейства и даже модели процессорного чипа;



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

138





Хорошим решением может оказаться вообще отключение гипертрэдинга в процессорах,
что можно сделать, зачастую, в установках BIOS вашего компьютера;



Для истинно многопроцессорных конструкций (с несколькими процессорными чипами на
плате) равномерное распределение нагрузки по процессорам может быть существенным
ещё и с точки зрения наличия или отсутствия перегрева, что контролируется утилитой
sensors (или другой из того же класса: psensor, xsensor, …).

Управление процессорами Linux
Два самых известных продукта, созданных в
Университете Беркли — это UNIX и LSD. Это
не может быть просто совпадением.
Jeremy S. Anderson
Описанная выше дифференциация CPU по принадлежности процессорам и ядрам имеют какойлибо смысл только тогда, когда мы умеем распределить вычислительную нагрузку между ними.
Для этого предназначается афинити маска.

Аффинити маска
Аффинити маска (маска cродства) — это битовая маска допустимых к использованию CPU, в
POSIX API определяемая в отдельным типом cpu_set_t : 0 бит — 1-й CPU, 1
бит — 2-й CPU и т.д. последовательно. Точный вид cpu_set_t определённый в
зависит от версий… но это и не особенно важно, хотя бы потому, что
использовать структурность cpu_set_t в коде непосредственно присвоениями нельзя, для этого
определено () большое множество макросов такого вот вида (семантика каждого из
них понятна из названия):
CPU_SET, CPU_CLR, CPU_ISSET, CPU_ZERO, CPU_COUNT, CPU_AND, CPU_OR, CPU_XOR,
CPU_EQUAL, CPU_ALLOC, CPU_ALLOC_SIZE, CPU_FREE, CPU_SET_S, CPU_CLR_S,
CPU_ISSET_S, CPU_ZERO_S, CPU_COUNT_S, CPU_AND_S, CPU_OR_S, CPU_XOR_S,
CPU_EQUAL_S
Примечание: Включение макроопределения имени _GNU_SOURCE в начало любого файла исходного кода,
использующего макросы CPU_* — обязательно!: #define _GNU_SOURCE

Аффинити маска может применяться (относиться) либо ко всему процессу в целом, либо к
отдельному потоку ядра (pthread_t) этого процесса. В отношении процесса используются API
sched_getaffinity() и sched_setaffinity() из . В отношении потока
используются pthread_getaffinity_np() и pthread_setaffinity_np(), соответственно.
Напишем 2 игрушечных приложения, иллюстрирующих сказанное (эти приложения полезны для
экспериментов далее, каталог архива goproc/concurent):
how-many-p.c :
#define _GNU_SOURCE
#include
#include
#include
#include
int main(int argc, char *argv[]) {
cpu_set_t mask;
if(sched_getaffinity(getpid(), sizeof(cpu_set_t), &mask) != 0 )
printf("ошибка sched_getaffinity() %m\n"), exit(1);
printf("в системе процессоров: %d\n", CPU_COUNT(&mask));
return 0;
}

139

how-many-t.c :
#define _GNU_SOURCE
#include
#include
#include
#include
int main(int argc, char *argv[]) {
cpu_set_t mask;
if(pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &mask) != 0)
printf("ошибка pthread_setaffinity_np() %m\n"), exit(1);
printf("в системе процессоров: %d\n", CPU_COUNT(&mask));
return 0;

}

И то, как это выполняется:
$ gcc how-many-p.c -Wall -o how-many-p
$ ./how-many-p
в системе процессоров: 40
$ gcc how-many-t.c -Wall -lpthread -o how-many-t
$ ./how-many-t
в системе процессоров: 40

Но в первом случае число процессоров (взведенных бит в слове аффинити маске) мы определяем
из информации о процессах, то во втором — исходя из информации о потоках ядра системы.
И, наконец, для сравнения — что это полностью соответствует независимой диагностике
утилитами системы Linux:
$ inxi -C
CPU:

Topology: 2x 10-Core model: Intel Xeon E5-2470 v2
bits: 64 type: MT MCP SMP L2 cache: 50.0 MiB
Speed: 2800 MHz min/max: 1200/3200 MHz Core speeds (MHz): 1: 2800 2: 2800
3: 2800 4: 2800 5: 2801 6: 2800 7: 2800 8: 2801 9: 2800 10: 2800 11: 2800
12: 2800 13: 2800 14: 2799 15: 2804 16: 2800 17: 2799 18: 2800 19: 2798
20: 2798 21: 2795 22: 2802 23: 2802 24: 2800 25: 2800 26: 2802 27: 2800
28: 2802 29: 2800 30: 2800 31: 2802 32: 2802 33: 2799 34: 2802 35: 2800
36: 2800 37: 2800 38: 2800 39: 2800 40: 2800

На этом мы оставим вопрос интерфейса POSIX к аффинити маскам из программного кода (это
предмет совсем другого разговора, касающийся модели параллелизма C/C++) и обратим
внимание на то, что аффинити маской (по крайней мере для процесса в целом) можно управлять
при запуске любого приложения в Linux — команда taskset:
$ taskset --help
Usage: taskset [options] [mask | cpu-list] [pid|cmd [args...]]
Show or change the CPU affinity of a process.
Options:
-a, --all-tasks
-p, --pid
-c, --cpu-list
-h, --help
-V, --version

operate on all the tasks (threads) for a given pid
operate on existing given pid
display and specify cpus in list format
показать эту справку
показать версию

The default behavior is to run a new command:
taskset 03 sshd -b 1024
You can retrieve the mask of an existing task:

140

taskset -p 700
Or set it:
taskset -p 03 700
List format uses a comma-separated list instead of a mask:
taskset -pc 0,3,7-11 700
Ranges in list format can take a stride argument:
e.g. 0-31:2 is equivalent to mask 0x55555555
Для более детальной информации смотрите taskset(1).

Причём, как видно из этой краткой справки, команда taskset может как заказать аффинити
маску любого приложения при его запуске, так и переопределить маску для уже выполняющегося
(очевидно, долговременного) приложения по его PID.
Выглядит это как-то так:
$ taskset -c 0-3 ./how-many-p
в системе процессоров: 4
$ taskset -c 1,3,5,7,9 ./how-many-t
в системе процессоров: 5

Аналогично можно изменять аффинити маску и выполняющихся потоков ядра в рамках единого
процесса. Но делается это изнутри программного кода используя POSIX API (вызов
pthread_getaffinity_np()) … но это не понадобится нам в дальнейшем рассмотрении.
Отметим только то (не совсем очевидная вещь), что в многопоточном приложении каждому из
потоков может быть предписано индивидуальной список дозволенных ему к использованию
список физических процессоров, причём при этом разные потоки выполнения можно, в
частности, «развести» на разные процессоры.

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

Планировщик Linux (диспетчер, шедулер) — это часть ядра, отвечающая за распределение
процессорного времени между процессами и потоками в многозадачной операционной системе.
Диспетчер ядра предоставляет процессам три алгоритма планировщика: один для обычных
процессов и два для потоков (процессов) реального времени.
Примечание: Словосочетание «реальное время» применительно к Linux не имеет никакого разумного отношения к
понятию реальному времени или к выполнению с соблюдением требований реального времени. Здесь он означает
только то, что при таких дисциплинах потоки и процессы планируются по более строгим алгоритмам,
предусмотренным расширением стандарта POSIX для требований реального времени POSIX 1003.b.

Подавляющее большинство процессов, выполняющихся в Linux, выполняются как обычные
процессы (так запускаются процессы по умолчанию), для них политика планирования
обозначается константой политики SCHED_OTHER. Многие пользователи Linux твёрдо убеждены
(из литературы), что их операционная система обеспечивает для обычных процессов
(SCHED_OTHER), а таких 99.9% в работающей системе, вытесняющую мультизадачность
(preemptive multitasking). На самом деле это не совсем так! В Linux для обычных процессов
используется модифицированный (изобретённый в Linux) упрощённый метод, основанный на
массиве всех выполняющихся процессов, каждому из которым выделяются различающиеся квоты
времени на выполнение (зависящие от характера их активности) в течении одного периода
диспетчирования … о чём будет сказано чуть ниже.

141

Потоки и процессы реального времени могут иметь политики планирования SCHED_FIFO
(обслуживание в порядке очереди поступления, кооперативная многозадачность) и SCHED_RR
(round-robin, круговое обслуживание с вытеснением по таймеру, вытесняющая многозадачность).
Любой поток или процесс имеет статический приоритет (приоритет реального времени),
определяемый структурой:
struct sched_param {
int sched_priority;
};

Примечание: Стандарт POSIX 1003.b (расширение реального времени) предусматривает для struct sched_param
более сложное определение, но в Linux структура выродилась именно в такое единичное значение.

Приоритет и политику планирования для потока можно изменить и диагностировать системными
вызовами (POSIX API):
#include
int sched_setscheduler( pid_t pid, int policy, const struct sched_param *p );
int sched_getscheduler( pid_t pid );
int sched_setparam( pid_t pid, const struct sched_param *p );
int sched_getparam( pid_t pid, struct sched_param *p );
int getpriority( int which, int who);
int setpriority( int which, int who, int prio);

Если pid в в вызовах равен 0, то вызов относится к текущему процессу. Последние два вызова
могут работать с процессом, группой процессов, или процессами конкретного пользователя.
Для потока аналогичные действия делаются вызовами:
#include
int pthread_setschedparam( pthread_t __target_thread, int __policy,
const struct sched_param *__param );
int pthread_getschedparam( pthread_t __target_thread,
int *__restrict __policy,
struct sched_param *__restrict __param );
int pthread_setschedprio( pthread_t __target_thread, int __prio);

Для обычных процессов и потоков (SCHED_OTHER) статический приоритет может иметь только
единственное значение 0, попытка установить другой значение будет приводить к ошибке. Для
процессов и потоков с планированием реального времени ( SCHED_FIFO или SCHED_RR)
статический приоритет может иметь значение в диапазоне 1 ... 99 (значение 0 недопустимо, в
противовес SCHED_OTHER).
Статический приоритет, больший, чем 0, может быть установлен только у суперпользовательских
процессов (выполняющихся с правами root), то есть только эти процессы могут иметь алгоритм
планировщика SCHED_FIFO или SCHED_RR (но здесь вы можете воспользоваться установкой
флага SUID для разрешений ординарному пользователю выполнять такие программы).
Если на процессоре выполняется активный процесс или поток с планированием реального
времени (со статическим приоритетом больше 0), то ни один обычный процесс и никогда не
получит вообще кванта времени на этом процессоре, до освобождения его выполняющимся
потоком (завершения или переходом в блокированное состояние). То же самое (не получит
никогда кванта) относится и потокам реального времени, но с меньшим статическим
приоритетом.
В свою очередь, обычные процессы, которых, как упоминалось, в системе подавляющее
большинство, имеют дополнительный приоритет (nice-приоритет), на основе которых и
производится их взаимное планирование (потоки не могут иметь самостоятельный niceприоритет, а потоки с планированием SCHED_OTHER будут все иметь nice-приоритет своего
процесса). Допускается 40 значений nice-приоритетов для SCHED_OTHER диспетчеризации, в
диапазоне от -20 до +19 — максимальный приоритет -20 (см. далее).
Таким образом, в Linux может быть 140 (препроцессорная константа MAX_PRIO) приоритетов:
100 приоритетов реального времени и 40 nice-приоритетов.

142

Планирование SCHED_OTHER процессов в Linux выполняется строго по системному таймеру, на
основании динамически пересчитываемых приоритетов. Каждому процессу с сформированным
приоритетом nice на каждом периоде диспетчирования, в зависимости от этого значения
приоритета процесса, назначается период активности (timeslice, квант) — 10-200 системных
тиков, который динамически в ходе выполнения этого процесса может быть ещё расширен в
пределах 5-800, в зависимости от характера интерактивности процесса (процессам, активно
загружающим процессор, timeslice задаётся ниже, а активно взаимодействующим с
пользователем, диалоговым — повышается). На этом построена схема диспетчеризации
процессов в Linux сложности O(1) - не зависящая по производительности от числа подлежащих
планированию процессов, которой очень гордятся разработчики ядра Linux (возможно, что и
вполне оправдано). Но это совсем другая система планирования, не имеющая прямого отношения
к вытеснению!
Примечание: Новая система диспетчеризации O(1) построена на основе 2-х очередей: очередь ожидающих
выполнения процессов, и очередь отработавших свой квант процессов. Из первой из них выбирается поочерёдно
следующий процесс на выполнение, и после выработки им своего кванта, он сбрасывается во вторую. Когда очередь
ожидающих опустошается, очереди просто меняются местами: очередь отработавших становится новой очередью
ожидающих, а пустая очередь ожидающих — становится очередью отработавших. Но всё это происходит так только
при отсутствии процессов с установленной реалтайм диспетчеризацией (RR или FIFO), с ненулевым приоритетом
реального времени. До тех пор, пока в системе будет находиться хотя бы один реалтайм процесс в состоянии
готовности к выполнению (активный), ни один процесс нормального приоритета не будет выбираться на исполнение
(на данном процессоре!).

Период системного тика определяется символьной препроцессорной константой ядра HZ, которая
для большинства аппаратных процессорных архитектур равна 1000, а период системного тика,
соответственно — 1 миллисекунда. Таким образом период активности (максимальный интервал
непрерывного выполнения) для различных обычных процессов может находиться в диапазоне 101000 миллисекунд.
Описанная процедура приводит к тому, что, рано или поздно, любой процесс, с самым малым
приоритетом (nice=19), планируемый по стандартному алгоритму планировщика с разделением
времени (SCHED_OTHER) получит некоторый квант процессорного времени (не менее 10
системных тиков, 10 миллисекунд).

Приоритеты nice
Приоритеты nice, вообще то говоря, приоритетами в общепринятом смысле вовсе и не является,
а, напротив, означает «уступчивость»: ветви с большими числовыми значениями nice уступают
большую часть времени диспетчирования соседям с меньшими (или отрицательными)
значениями nice.
Приоритеты nice имеют смысл и значение только для обычных процессов. Изменить приоритет
обычного процесса можно командой nice (с консоли, терминала), или программным вызовом (из
кода):
#include
int nice(int inc);

Диапазон параметра (значение nice процесса) находится в пределах -19 … +20 — чем выше, тем
выше «уступчивость», тем ниже приоритет выполнения. И в командном, и в API варианте,
отрицательные значения параметра, для повышения приоритета, допускаются только с правами
суперпользователя root. Ещё для работы с nice-приоритетами используются упоминавшиеся
уже программные вызовы getpriority() и setpriority().
Команда nice изменяет приоритет запускаемого вами приложения не путём установки значения,
а корректировкой его относительно умалчиваемого (равновесного) значения:
$ nice --help
Использование: nice [ПАРАМЕТР] [КОМАНДА [АРГ]…]
Запускает КОМАНДУ с изменённым значением nice, что влияет на приоритет
при планировании. Если КОМАНДА не задана, печатает текущее значение
nice. Значения nice лежат в диапазоне от -20 (наибольший приоритет) до 19
(наименьший).

143

Аргументы, обязательные для длинных параметров, обязательны и для коротких.
-n, --adjustment=N
увеличить nice на целое число N (по умолчанию 10)
--help
показать эту справку и выйти
--version показать информацию о версии и выйти
ЗАМЕЧАНИЕ: ваша оболочка может включать свою версию nice, которая,
обычно, заменяет версию, описанную здесь. Пожалуйста, обратитесь к
документации по оболочке, чтобы узнать, какие параметры она
поддерживает.

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

Приоритеты реального времени
Для того, чтобы узнать возможный диапазон значений статических приоритетов (приоритетов
реального времени) данного алгоритма планировщика, можно использовать функции:
#include
int sched_get_priority_max(int __algorithm);
int sched_get_priority_min(int __algorithm);

Это может понадобиться в переносимых в другие системы программах для того, чтобы они
соответствовали стандарту POSIX.1b.
Период времени квантования (переключений), установленный для планирования с дисциплиной
SCHED_RR, можно узнать вызовом:
int sched_rr_get_interval(__pid_t __pid, struct timespec *__t);

Зачастую период квантования установлен (для Intel x86) в 1 миллисекунду (параметр ядра
HZ=1000), но это очень сильно меняется в зависимости от аппаратной платформы.
Приложению можно установить приоритет реального времени только переведя его в режим
планирования по схеме реального времени: SCHED_FIFO или SCHED_RR. Это делается командой
chrt:
$ chrt --help
Show or change the real-time scheduling attributes of a process.
Set policy:
chrt [options] [...]
chrt [options] --pid
Get policy:
chrt [options] -p
Параметры политики:
-b, --batch
-d, --deadline
-f, --fifo
-i, --idle
-o, --other
-r, --rr

set
set
set
set
set
set

Scheduling options:
-R, --reset-on-fork
-T, --sched-runtime
-P, --sched-period
-D, --sched-deadline

policy
policy
policy
policy
policy
policy

Другие параметры:

144

to
to
to
to
to
to

SCHED_BATCH
SCHED_DEADLINE
SCHED_FIFO
SCHED_IDLE
SCHED_OTHER
SCHED_RR (default)

set SCHED_RESET_ON_FORK for FIFO or RR
runtime parameter for DEADLINE
period parameter for DEADLINE
deadline parameter for DEADLINE

-a,
-m,
-p,
-v,

--all-tasks
--max
--pid
--verbose

-h, --help
-V, --version

operate on all the tasks (threads) for a given pid
show min and max valid priorities
operate on existing given pid
display status information
показать эту справку
показать версию

Как уже должно быть понятно, сделать изменение политики можно только с административными
правами root.
Так же, как и рассматриваемая раньше команда taskset, команда chrt позволяет либо изменить
статический приоритет при запуске процесса, либо изменить приоритет динамически, «по ходу»,
для уже выполняющегося процесса (по его PID):
# chrt -r 50 bash
# ps
PID
3068
3074
3100

TTY
pts/2
pts/2
pts/2

TIME
00:00:00
00:00:00
00:00:00

CMD
sudo
bash
ps

# chrt -p 3074
pid 3074's current scheduling policy: SCHED_RR
pid 3074's current scheduling priority: 50
# chrt -r -p 5 3074
# chrt -p 3074
pid 3074's current scheduling policy: SCHED_RR
pid 3074's current scheduling priority: 5

Для запущенного процесса таким же образом (при наличии соответствующих прав) можно
произвольно произвольно менять и политику и приоритеты:
# chrt -f 20 bash
# ps
PID
3288
3294
3320

TTY
pts/2
pts/2
pts/2

TIME
00:00:00
00:00:00
00:00:00

CMD
sudo
bash
ps

# chrt -p 3294
pid 3294's current scheduling policy: SCHED_FIFO
pid 3294's current scheduling priority: 20
# chrt -r -p -r 10 3294
# chrt -p 3294
pid 3294's current scheduling policy: SCHED_RR
pid 3294's current scheduling priority: 10
# chrt -r -p -o 0 3294
# chrt -p 3294
pid 3294's current scheduling policy: SCHED_OTHER
pid 3294's current scheduling priority: 0
# exit

145

Источники информации
[1] Краткий экскурс в историю десктопных многоядерных процессоров, 11 января 2022 —
https://i2hard.ru/publications/29369/
[2] Олег Цилюрик, «Параллелизм, конкурентность, многопроцессорность в Linux», 2014 —
http://mylinuxprog.blogspot.com/2014/09/linux.html
http://flibusta.is/b/523510

146

Параллелизм и многопроцессорность
Эволюция модели параллелизма
– Мы не крысы ... Мы музыканты.
«Титаник» тонет, а мы – сидим на палубе
и играем на виолончелях.
Андрей Рубанов, «Патриот»

Параллельные процессы и fork
Исторически первые модели параллелизма были созданы на уровне процесса многозадачной
операционной системы как единицы параллельного выполнения. И ранее использовалась
возможность запуска из кода дочерних процессов — несколькими родственными системными
вызовами группы exec: execl(), execlp(), execle(), execv(), execvp(), execvpe(), а в
некоторых операционных системах (QNX) и группы spawn(). Но настоящий «разгул»
параллелизма начался с появления системного вызова fork() в операционных системах класса
UNIX (POSIX совместимых).
Концепция ветвления процессов впервые описана в 1962 году Мелвином Конвеем, а к 1964 году
относятся первые реализации, которые были заимствованы Томпсоном при реализации
операционной системы UNIX, и позже была включена в стандарты POSIX как обязательное
требование для систем этого класса совместимости. 23
Вызов fork() разветвляет текущий процесс на родительский (текущий) и дочерний (вновь
созданный). В системах с виртуальной памятью (а это практически любая аппаратная
архитектура на сегодня), за счёт механизма copy-on-write (COW), создание полной копии
адресного пространства родительского процесса происходит без фактического копирования
(просто переотражениемнового виртуального адресного пространства на уже существующее
физическое). А поэтому происходит создание нового процесса очень быстро.
Модель ветвления процессов fork() породила целую новую парадигму построения
программных систем: клиент-серверную, причём с параллельными серверами. Она была
использована во множестве крупнейших информационных продуктов.
Логика работы fork() следующая: сразу же после вызова у нас возникает дубликат вызвавшего
(родительского) процесса. Единственная разница между ними (родительским и дочерним
процессом) в этой точке ветвления в том, что вызов fork() возвратит: 0 — в дочернем процессе,
и значение PID (process ID) нового дочернего процесса — в родительском процессе, и 1 && atoi(argv[1]) > 0) ?
atoi(argv[1]) : 1;
pid_t pid[numpar];
for(int i = 0; i < numpar; i++)
switch (pid[i] = fork()) {
case -1: perror("ошибка fork"), exit( EXIT_FAILURE );
case 0: // дочерний процесс
cout [4]
7F3B457FA700 => [4]
7F3B9D685700 => [2]
7F3B64FF9700 => [3]

189

7F3B267FC700
7F3B667FC700
7F3B067FC700
7F3B65FFB700
7F3B277FE700
7F3B44FF9700
7F3B87FFF700
7F3B86FFD700
7F3B077FE700
7F3B24FF9700
7F3B9E687700
7F3B04FF9700
7F3BA10DA740
7F3B257FA700
7F3B467FC700
7F3B05FFB700
7F3B867FC700
7F3B67FFF700
7F3B27FFF700
7F3B057FA700
7F3B25FFB700
7F3B85FFB700
7F3B46FFD700
7F3B9DE86700
7F3B26FFD700
7F3B477FE700
7F3B45FFB700

=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>

[2]
[2]
[3]
[2]
[1]
[1]
[4]
[1]
[1]
[4]
[2]
[1]
[3]
[3]
[3]
[3]
[2]
[1]
[5]
[3]
[1]
[5]
[2]
[3]
[7]
[3]
[1]

Хорошо видно более-менее равномерное разбрасывание порождаемых горутин по системным
потокам их выполняющим. А время выполнения 100 «порций» работы превышает единичное
(всего) в 4.5 раз.
И снова обратимся к другой крайности - миниатюрные микрокомпьютеры ARM.
Raspberry Pi 2 :
$ ./mlpar2
число процессоров в системе: 4
число ветвей выполнения: 3
[00,76FE01C0,995]
[03,76FE01C0,998]
[02,64FFE440,1007]
[01,661FF440,1024]
итоговое время параллельного выполнения: 1.024868641s
76FE01C0 => [2]
64FFE440 => [1]
661FF440 => [1]
$ ./mlpar 7
число процессоров в системе: 4
число ветвей выполнения: 7
[03,76F021C0]
[05,64EFE440]
[04,656FF440]
[07,656FF440]
[06,660FF440]
[01,76F021C0]
[02,64EFE440]
итоговое время выполнения: 1.034522514s
$ ./mlpar2 7
число процессоров в системе: 4
число ветвей выполнения: 7
[00,76FA11C0,966]

190

[01,76FA11C0,1529]
[07,76FA11C0,1648]
[04,661FF440,1673]
[02,657FF440,1714]
[03,76FA11C0,1741]
[06,64FFE440,1757]
[05,661FF440,1767]
итоговое время параллельного выполнения: 1.768285034s
661FF440 => [2]
657FF440 => [1]
64FFE440 => [1]
76FA11C0 => [4]
$ ./mlpar2 -20
число процессоров в системе: 4
число ветвей выполнения: 20
итоговое время параллельного выполнения: 4.826756735s
76F971C0 => [5]
661FF440 => [4]
657FF440 => [6]
64FFE440 => [6]
$ ./mlpar2 -100
число процессоров в системе: 4
число ветвей выполнения: 100
итоговое время параллельного выполнения: 25.274911046s
76FA21C0 => [26]
64FFE440 => [30]
661FF440 => [21]
657FF440 => [24]

Здесь картина T(N) ещё более реалистичная! А распределение горутин по процессорам ещё
нагляднее.
И самый минималистичный вариант — Orange Pi One (и это всё в 512Mb операционной
системы):
$ ./mlpar2
число процессоров в системе: 4
число ветвей выполнения: 3
[00,B6F47D40,957]
[01,A43FF450,937]
[03,B6F47D40,937]
[02,A57FF450,937]
итоговое время параллельного выполнения: 937.670202ms
B6F47D40 => [2]
A43FF450 => [1]
A57FF450 => [1]
$ ./mlpar2 -9
число процессоров в системе: 4
число ветвей выполнения: 9
итоговое время параллельного выполнения: 2.141070976s
A57FF450 => [1]
B6F7AD40 => [5]
A4FFE450 => [3]
A61FF450 => [1]
$ ./mlpar2 -20
число процессоров в системе: 4
число ветвей выполнения: 20
итоговое время параллельного выполнения: 4.435031013s
A43FF450 => [5]

191

A4FFE450 => [6]
A57FF450 => [5]
B6F50D40 => [5]
$ ./mlpar2 -100
число процессоров в системе: 4
число ветвей выполнения: 100
итоговое время параллельного выполнения: 23.508637053s
B6FD0D40 => [31]
A4FFE450 => [24]
A57FF450 => [29]
A61FF450 => [17]

Но отметим одну не очевидную вещь: как бы мы не стремились избежать прерываний и перевода
в блокированные состояния потоков ядра, выполняющих горутины, мы не можем 100%
гарантировать это — помимо своего приложения на Go, мы всё ещё находимся в мультизадачной
операционной системе Linux с вытесняющей многозадачностью, где рабочие потоки нашего
приложения конкурируют за ресурсы с потоками других (в том числе и системных) процессов.

О числе потоков исполнения
Выше была описана схема в которой, по умолчанию, как было описано:


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



При этом каждый поток ядра оказывается прикреплённым к одном из физических
процессоров системы, число которых runtime.NumCPU().



При такой схеме каждый их выполняющихся потоков не вытесняемый (в мультизадачной
операционной системе Linux), по крайней мере, не вытесняемый GOMAXPROCS-1
конкурирующими потоками этого приложения (но могут вытесняться другими
приложениями Linux, что может несколько нарушать оптимизм всей этой модели).



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

Но отсюда может возникнуть мнение (и оно неявно не опровергалось описаниями предыдущих
глав), что число потоков выполнения Go приложения GOMAXPROCS а). всегда устанавливается
равным числу физических процессоров runtime.NumCPU() и б). это жёсткая связь между
числом процессоров и числом потоков диспетчирования горутин. Но это не совсем так!
Если вы хорошо понимаете цели с которыми это делаете, вы можете всегда указать своему
приложению Go использовать число потоков выполнения (горутин) меньше или больше чем
существующее число процессоров в аппаратуре28. Для подтверждения этого утверждения сделаем
почти игрушесное диагностическое приложение (каталог scheduler):
numproc.go :
package main
import ("fmt"; "os"; "runtime"; "strconv")
func main() {
потоки := 1
if len(os.Args) > 1 {
потоки, _ = strconv.Atoi(os.Args[1])
if потоки > 1 {
runtime.GOMAXPROCS(потоки)
}
}

28 Но без понимания цели такое лучше не делать — у Go весьма совершенная схема использования физических
аппаратных ресурсов!

192

fmt.Printf("число процессоров в системе: %v\n", runtime.NumCPU())
fmt.Printf("число потоков исполнения: %v\n", runtime.GOMAXPROCS(-1))
}

Значение переменной окружения GOMAXPROCS (из пакета runtime) можно изменить, и сделать это
можно разными способами:


Из программного кода вызовом runtime.GOMAXPROCS() с положительным значением
единственного параметра (при отрицательном значении вызов только возвращается
текущее установленное значение GOMAXPROCS, при положительном параметре
возвращается предыдущее установленное значение).



Изменением переменной окружения GOMAXPROCS системы при запуске Go приложения
(при этом сам код приложения совершенно ничего «не знает» об изменении числа потоков
исполнения).

А теперь иллюстрируем сказанное... На процессоре (системное приложение nproc Linux даёт
нам, для контроля, физическое число процессорных ядер):
$ nproc
40

В данном случае, мы действительно экспериментируем на сервере промышленного уровня. А
теперь наше созданное приложение:
$ ./numproc
число процессоров в системе: 40
число потоков исполнения: 40
$ ./numproc 5
число процессоров в системе: 40
число потоков исполнения: 5
$ export GOMAXPROCS=7; ./numproc
число процессоров в системе: 40
число потоков исполнения: 7
$ ./numproc
число процессоров в системе: 40
число потоков исполнения: 7
$ GOMAXPROCS=9; ./numproc
число процессоров в системе: 40
число потоков исполнения: 9

Но это ещё далеко не всё! Это мы переопределяли число доступных потоков средствами самой
исполнимой системы Go. Это так и будет работать в любой операционной системе. Но в
операционной системе Linux мы можем гибко управлять распределением любых приложений по
процессорам средствами самой операционной системы. Делается это малоизвестной консольной
Linux-командой taskset, которая управляет аффинити маской любого процесса.
Начнём с того, что вспомним как «попросить» у операционной системы её информацию о числе
доступных физических процессоров. Вот несколько простейших способов:
$ cat /proc/cpuinfo | grep processor | wc -l
40
$ nproc
40
$ lscpu | grep 'CPU(s)'
CPU(s):
On-line CPU(s) list:
NUMA node0 CPU(s):
NUMA node1 CPU(s):

193

40
0-39
0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38
1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39

Каждый процесс в Linux имеет собственную индивидуальную аффинити маску — слово в
котором каждый установленный бит отмечает процессор (в показанной выше нумерации),
который этот процесс может использовать. (Заметим, что и каждому потоку в рамках процесса
также соответствует своя индивидуальная аффинити маска, но нас в данном контексте
интересуют только процессы.) Команда taskset позволяет посмотреть или изменить (что нас
интересует гораздо больше) аффинити маску процесса:
$ taskset --help
Usage: taskset [options] [mask | cpu-list] [pid|cmd [args...]]
Show or change the CPU affinity of a process.
Options:
-a, --all-tasks
-p, --pid
-c, --cpu-list
-h, --help
-V, --version
...

operate on all the tasks (threads) for a given pid
operate on existing given pid
display and specify cpus in list format
показать эту справку
показать версию

Например, в простейшем применении — диагностика:
$ ps
PID TTY
8148 pts/5
11759 pts/5

TIME CMD
00:00:00 bash
00:00:00 ps

$ taskset -p 8148
pid 8148's current affinity mask: ffffffffff

Как видно, в обычном состоянии процессам разрешено распределяться на все физические
процессоры. Но это можно изменить, причём как по числу, так и по выборочному подмножеству
конкретных процессоров.
$ taskset -c 0-3 ./numproc
число процессоров в системе: 4
число потоков исполнения: 4
$ taskset -c 32,34,36,38 ./numproc
число процессоров в системе: 4
число потоков исполнения: 4

Вспомните что мы обсуждали ранее относительно гипертрэдинга и странностей нумерации ядер
процессоров… Используя возможности taskset мы можем запускать свои приложения (и не
только тестовые) так, чтобы они выполнялись только на физических ядрах без их
гипертрэдинговых пар, или в многопроцессорных серверах (SMP) так, чтобы приложения
выполнялись на выбранном чипе. Примеры (выполняемое приложение мы сделали и обсуждали
ранее, а процессоры сервера DELL описывались при рассмотрении нумерации), только для
образца:
- выполнение на всех процессорах без ограничений:
$ ./mlpar -40
число процессоров в системе: 40
число ветвей выполнения: 40
итоговое время выполнения: 1.066720489s

- выполнение 40 горутин на 4-х процессорах выбранных без пересечений в гипертрэдинге:
$ taskset -c 0-3 ./mlpar -40
число процессоров в системе: 4
число ветвей выполнения: 40
итоговое время выполнения: 1.399913902s

194

- выполнение 40 горутин на 3-х процессорах только одного (из 2-х) физического чипа:
$ taskset -c 0,2,4 ./mlpar -40
число процессоров в системе: 3
число ветвей выполнения: 40
итоговое время выполнения: 1.370372032s

- выполнение 40 горутин на 2-х процессорах составляющих гипертрэдинговую пару общего ядра
(специально выбранные):
$ taskset -c 0,20 ./mlpar -40
число процессоров в системе: 2
число ветвей выполнения: 40
итоговое время выполнения: 1.407447393s

Предполагаю, что этого разъяснения вполне достаточно для того, чтобы вы легко могли
варьировать (и изучать) число процессоров (потоков) на которых станет выполняться ваше Go
приложение. Мы проделывали это для экспериментирования, но такие возможности могут
оказаться крайне востребованными а). во встраиваемых системах на уровне оптимизации железа,
так и б). в сложных высоко нагруженных системах непрерывного функционирования, 365х24.

Источники информации
[1] Механизм планирования сопрограмм Golang и настройка производительности GOMAXPROCS —
https://russianblogs.com/article/9411826814/
[2] Armbian. Linux for ARM development boards — https://www.armbian.com/orange-pi-one/
[3] Raspberry Pi Documentation — https://www.raspberrypi.com/documentation/
[4] Raspberry Pi OS — https://www.raspberrypi.com/documentation/computers/os.html
[5] DELL PowerEdge R420. Technical Guide — https://content.etilize.com/User-Manual/1024095456.pdf
[6] О.Цилюрик, Параллелизм, конкурентность, многопроцессорность в Linux, 2014 —
http://mylinuxprog.blogspot.com/2014/09/linux.html

195

Часть 3. Некоторые примеры и сравнения
В восточных странах жара и праздная жизнь
порождает у обывателей досужие и
смертоносные фантазии.
Роберт Грэм Ирвин «Арабский кошмар»
Эта часть менее формализованная чем предыдущие... В этой части будет показана некоторая
подборка приложений, которые автору, из совершенно разных соображений, показались
интересными (по причине жары и праздной жизни, как сказано в эпиграфе). Все они
присутствуют в архиве примеров, и даже с детальными журналами (файлами вида *.hist)
сборки и выполнения. Для многих примеров будут параллельно показаны сравнительные
реализации на Go и на C или C++ (иногда для сравнения скоростных показателей, иногда в
качестве аналогии, и того как это выглядит).

196

Осваиваемся в синтаксисе Go
Язык, который не меняет вашего представления
о программировании,
недостоин изучения.
Alan J. Perlis
В первом разделе этой части текста мы рассмотрим несколько хорошо известных и понятных
задач, выраженных в языке Go. Здесь мы просто в конкретном контексте смотрим использование
тех синтаксических возможностей, которые обсуждали выше.
В следующих разделах мы рассмотрим более тонкие вещи, возвращающие нас к главной цели
работы — параллельное и многопроцессорное выполнение.

Утилита echo
В качестве первого простого примера использования Go посмотрим пример утилиты echo
(каталог hello архива примеров), тот который приводимый в документации:
echo.go :
package main
import (
"flag" // парсер параметров командной строки
"os"
)
var omitNewLine = flag.Bool("n", false, "не печатать знак новой строки")
const (
Space
= " "
NewLine = "\n"
)
func main() {
flag.Parse() // Сканирование списка аргументов и установка флагов
var s string
for i := 0; i < flag.NArg(); i++ {
if i > 0 {
s += Space
}
s += flag.Arg(i)
}
if !*omitNewLine {
s += NewLine
}
os.Stdout.WriteString(s)
}

Как и следовало ожидать:
$ ./echo повторяет то что я пишу
повторяет то что я пишу
$ ./echo -n 12345
12345$

Пакет flag (импортируемый программой) отрабатывает ошибочные ситуации — ввод не
описанных в коде задачи опций командной строки приводит к ошибке:
$ ./echo -z 12345

197

flag provided but not defined: -z
Usage of ./echo:
-n
не печатать знак новой строки

Здесь попутно задействована функциональность пакета flag, который позволяет организовать
обработку параметров и опций командной строки запуска в том стиле, который принят в
UNIX/Linux (известный POSIX вызов getopt()). Это будет приятным подарком пишущим в
Linux консольные приложения в едином принятом стиле, поэтому мы останавливаемся на нём так
подробно.
Есть и небольшое отличие в поведении от привычного getopt() — все опции командной строки
пакета flag должны обязательно предшествовать параметрам командной строки. В противном
случае они рассматриваются уже как последующие параметры:
$ ./echo 12345 -n
12345 -n

P.S. Дефаултные опции подсказки (-h, --help), как это принято в Linux, пакет flag создаёт за
вас сам, исходя из определений списка используемых опций ( flag.Bool у нас, как пример, и
подобные ему), вам этого делать не надо (это ещё один приятный бонус):
$ ./echo -h
Usage of ./echo:
-n
не печатать знак новой строки
$ ./echo --help
Usage of ./echo:
-n
не печатать знак новой строки

Но такая особенность (обязательное предшествование опций параметрам в командной строке) —
это общее поведение всех вообще команд GoLang (в отличие от команд-утилит Linux), это их
специфика.
Подробную информацию (с примерами использования) того, как воспользоваться возможностями
пакета flag см. здесь: https://pkg.go.dev/flag.

Итерационное вычисление вещественного корня
Здесь будет показана реализация функции вычисления квадратного корня z=sqrt(x), как она
может быть выражена на C и на Go (каталог compare/sqrt). Вычисление делается методом
Ньютона, кода на каждой итерации вычисляется: zi+1 = zi - (zi^2 - x) / (2 * zi).
Реализация (всё в каталоге compare/sqrt) на C:
sqrt_c.c :
#include
#include
#include
long double eps = 1e-9;
int itr = 0;
long double Sqrt(long double arg) {
long double z = 1.;
while(1) {
long double z1 = z - (z * z - arg) / 2. / z;
if(fabsl(z1 - z) / z < eps) return z;
z = z1;
itr++;
}
}
int main(int argc, char **argv) {
long double sqr = Sqrt(atof(argv[1]));

198

printf("[%d]: %.16Lf\n", itr, sqr);
return 0;
}

Реализация на Go:
sqrt_go.go :
package main
import (
"fmt"
"math"
"os"
"strconv"
)
const eps = 1e-9
func sqrt(x float64) (float64, int) {
z := float64(1)
var i int = 0
for {
z1 := z - (z*z-x)/2/z
if math.Abs(z1-z)/z < eps {
return z, i
}
z = z1
i++
}
}
func main() {
v, _ := strconv.ParseFloat(os.Args[1], 64)
var n int
v, n = sqrt(v)
fmt.Printf("[%v]: %v\n", n, v)
}

Каждый файл исходного кода (для сравнений) собирается дважды: sqrt_c.c компиляторами
GCC и Clang, а sqrt_go.go — с помощью команд gccgo и go. Файл сборки:
Makefile :
BASE = sqrt
TASK = $(BASE)_c $(BASE)_cl $(BASE)_go $(BASE)_gc
all: $(TASK)
%: %.c
gcc -O0 -lm $< -o $@
%: %.go
gccgo $< -g -O0 -o $@
$(BASE)_cl: $(BASE)_c.c
clang -O0 $< -o $@
$(BASE)_gc: $(BASE)_go.go
go build -o $@ -compiler gc $<
clean:
rm -f $(TASK)

В итоге, после сборки получим 4 приложения:
$ ls -l | grep x
-rwxrwxr-x. 1 Olej
-rwxrwxr-x. 1 Olej
-rwxrwxr-x. 1 Olej
-rwxrwxr-x. 1 Olej

199

Olej
8721 авг
Olej
8769 авг
Olej 2245728 авг
Olej
28827 авг

14
14
14
14

02:13
02:13
02:13
02:13

sqrt_c
sqrt_cl
sqrt_gc
sqrt_go

Результаты сравнительного выполнения (в скобках выводится число итераций, посредством
которых достигается сходимость 1E9):
$ ./sqrt_c 10
[6]: 3.1622776601683793
$ ./sqrt_cl 10
[6]: 3.1622776601683793
$ ./sqrt_go 10
[6]: 3.1622776601683795
$ ./sqrt_gc 10
[6]: 3.1622776601683795

Вычисление числа π
В этой части мы посмотрим как в Go организована работа с большими числами — числами
произвольной разрядности, пакет math/big. В качестве примера использования больших чисел
рассмотрим (каталог архива types) пример приводимый Марком Саммерфильдом в его книге (в
заметно упрощённом виде). Где вычисляется значение числа π с произвольно большим (100, 1000,
...) числом значащих цифр по формуле Мэчина (1706г.), формула выглядит так:
π = 4 * (4 * arccot(5) - arccot(239))
где: arccot(x) = 1/(x) - 1/(3*x3)+ 1/(5*x5) - 1/(7*x7) + ...
Естественно, в форме вещественного числа мы проводить вычисления со столькими значащими
цифрами не можем, поэтому будем вычислять его в целочисленном эквиваленте, сдвинутом на
соответствующее (требуемому) число десятичных позиций (плюс несколько позиций во
избежание округления при вычислениях). Пример на Go базируется на реализации в Python, где
хитрое хранение целочисленных значений в этом языке допускает естественным образом их
представление с произвольным числом значащих цифр, без всяких сторонних пакетом. Поэтому в
качестве идеи разумно посмотреть сначала вариант Python, который проще и понятнее на уровне
алгоритмики:
pi.py :
#!/usr/bin/python3
import sys
def arccot(x, unity):
sum = xpower = unity // x
n = 3
sign = -1
while 1:
xpower = xpower // (x*x)
term = xpower
// n
if not term:
break
sum += sign * term
sign = -sign
n += 2
return sum
def pi(digits):
unity = 10 ** (digits + 10)
pi = 4 * (4 * arccot(5, unity) - arccot(239, unity))
return pi
// 10 ** 10
print(pi(int(sys.argv[1])))

Для вычисления N десятичных знаков числа π после запятой вычисляется большое

200

целочисленное значение π сдвинутое на N позиций влево (умноженное на 10**N). Функция pi()
начинается с вычисления значения переменной unity (10N+10), которое используется как
коэффициент масштабирования для вычислений с использованием целых чисел. Слагаемое +10
увеличивает дополнительно на 10 число цифр, заказанное пользователем, чтобы избежать
ошибок округления. Затем используется формула Мэчина с модифицированной версией функции
arccot(), которой во втором аргументе передается переменная unity. После вычисления
возвращается результат, деленный на 10 10, чтобы устранить эффект увеличения коэффициента
масштабирования unity. Все эти операции, естественно, целочисленные с неограниченным
числом разрядов.
Вариант на Go демонстрирует работу с целочисленными значениями неограниченно большого
размера — ещё один пакет: math/big. Этот вариант реализует в точности ту же схему
вычислений (обе реализации в каталоге примеров types):
pi.go :
package main
import (
"fmt"
"math/big"
"os"
"strconv"
)
func main() {
x, _ := strconv.Atoi(os.Args[1])
fmt.Println(π(x))
}
func π(places int) *big.Int {
digits := big.NewInt(int64(places))
unity := big.NewInt(0)
ten := big.NewInt(10)
exponent := big.NewInt(0)
unity.Exp(ten, exponent.Add(digits, ten), nil)
pi := big.NewInt(4)
left := arccot(big.NewInt(5), unity)
left.Mul(left, big.NewInt(4))
right := arccot(big.NewInt(239), unity)
left.Sub(left, right)
pi.Mul(pi, left)
return pi.Div(pi, big.NewInt(0).Exp(ten, ten, nil))
}
func arccot(x, unity *big.Int) *big.Int {
sum := big.NewInt(0)
sum.Div(unity, x)
xpower := big.NewInt(0)
xpower.Div(unity, x)
n := big.NewInt(3)
sign := big.NewInt(-1)
zero := big.NewInt(0)
square := big.NewInt(0)
square.Mul(x, x)
for {
xpower.Div(xpower, square)
term := big.NewInt(0)
term.Div(xpower, n)
if term.Cmp(zero) == 0 {
break
}
addend := big.NewInt(0)

201

sum.Add(sum, addend.Mul(sign, term))
sign.Neg(sign)
n.Add(n, big.NewInt(2))
}
return sum
}

В итоге:
$ ./pi.py 80
314159265358979323846264338327950288419716939937510582097494459230781640628620899
$ go run pi.go 80
314159265358979323846264338327950288419716939937510582097494459230781640628620899

Вычисления неограниченной точности
Если целочисленные вычисления огромной разрядности — это всё-таки, в некоторой мере,
экзотика, то тот же пакет math/big предоставляет и другую возможность: вещественных
вычислений неограниченно высокой точности. Вещественные величины здесь представляются с
мантиссой произвольной длины (в принципе, представление определяется только доступной
памятью компьютера).
Для иллюстрации этой техники мы проделаем вычисление квадратного корня из числа 2.0
методом Ньютона (нас совершенно не интересует сам выбор алгоритма, выбираем что проще).
Вычисление — итерационное:


Используем произвольное начальное значение, мы станем использовать x[0] = 1.0;



На каждом шаге итерации: x[n+1] = 1/2 * ( x[n] + ( 2.0 / x[n] ) );



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

Поскольку язык Go не допускает переопределение операций (+, -, *, / ...), как C++, то для
математических операций пакет предоставляет соответствующие методы: big.Rat.Add(),
big.Rat.Sub(), big.Rat.Mul(), big.Rat.Quo() и ещё много других, обеспечивающих все
потребности операций и преобразований.
Код вычисления (каталог types):
newton.go :
package main
import (
"fmt"
"math"
"math/big"
)
func main() {
const prec = 250
// точностью 250 бит в мантиссе
steps := int(math.Log2(prec))
// число итераций
two := new(big.Float).SetPrec(prec).SetInt64(2) // инициализации
half := new(big.Float).SetPrec(prec).SetFloat64(0.5)
x := new(big.Float).SetPrec(prec).SetInt64(1)
// начальное значение
t := new(big.Float).SetPrec(prec)
// результат
for i := 0; i 1)
n = atoi(argv[1]);
std::random_device rd{};
std::mt19937 gen{rd()};
std::normal_distribution d{0,1};
double s1 = 0., s2 = 0.;
for(int i = 0; i < n; i++) {
double r = d(gen);
s1 += r;
s2 += r * r;

203

}
s1 /= n;
s2 = s2 / n - s1 * s1;
std::cout 123
123
> 2345
2345
> русская строка
русская строка
> ^C

Здесь же мы, попутно, проверяем то, как мультиязычные строки UNICODE передаются по IP
сети, и как с ними работает код Go29.
Сервер же наш, как мы его построили, будет только приглашением (с напоминанием номера TCP
порта) подтверждать подключение очередного клиента:
$ ./echo_serv
waiting on port
waiting on port
waiting on port
waiting on port
waiting on port
^C

51500
51500
51500
51500
51500

Выше был показан последовательный TCP-сервер, который обслуживает только одного клиента,
следующий клиент обязан будет ожидать завершения работы с предыдущим. По самой
29 Здесь же отметим, вскользь, что клиент telnet, как один из самых старых сетевых инструментов Linux, не
обладает такой способностью, и при передаче/приёме искажает или «проглатывает» некоторые кирилические буквы
в UTF-8, воспринимая их байты как элементы управляющих последовательностей в потоке; telnet будет
безупречно работать только с ASCII символами.

211

поразительной чертой языка Go (и его исполнимой системы!) является то, с какой лёгкостью
последовательный код превращается в параллельный, могущий одновременно и параллельно
обслуживать многих клиентов:
echo_servm.go :
package main
import "net"
import "fmt"
import "bufio"
func main() {
ln, _ := net.Listen("tcp", ":51501")
// Устанавливаем прослушивание порта
for {
// Цикл прослушивания
fmt.Println("waiting on port 51501")
conn, _ := ln.Accept()
// Открываем серверный порт
go func(cn net.Conn) {
defer func() { cn.Close() }()
for {
// Запускаем цикл подключений
// Будем прослушивать все сообщения разделенные \n
message, err := bufio.NewReader(cn).ReadString('\n')
if err != nil {
// Клиент разорвал коннект
return
}
cn.Write([]byte(message))
// Отправить обратно клиенту
}
}(conn)
}
}
$ go build -o echo_servm echo_servm.go
$ ls -l echo_servm*
-rwxrwxr-x 1 olej olej 2759199 янв 12 16:38 echo_servm
-rw-rw-r-- 1 olej olej
944 дек 17 18:26 echo_servm.go

Теперь все наши клиенты работаю параллельно (на одном и том же TCP-порту), никак не
взаимодействуя и не интерферируя по данным (специально по каждому из одновременно
подключенных клиентов сделано по два ввода-подтверждения, чтобы убедиться, что каждый из
них работает со своим экземпляром сервера):
$ ./echo_cli 51501
connect to port 51501
> A: 123 456
A: 123 456
> A: русская строка
A: русская строка
>
$ nc 127.0.0.1 51501
B: asdf ghjk
B: asdf ghjk
B: 12 34 567 890
B: 12 34 567 890
^C
$ socat tcp4:127.0.0.1:51501 STDOUT
C: русская строка
C: русская строка
C: one more string
C: one more string
^C
$ telnet 127.0.0.1 51501
Trying 127.0.0.1...

212

Connected to 127.0.0.1.
Escape character is '^]'.
D: latin string
D: latin string
D: 9876
D: 9876
^]
telnet> quit
Connection closed.

Тривиальный WEB сервер
Пакет net/http обслуживает HTTP-запросы, используя объект (переменную) любого типа,
который реализует интерфейс http.Handler:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}

В показанном примере (каталог http) новый, определённый нами, тип Hello реализует
интерфейс http.Handler:
http.go :
package main
import (
"fmt"
"net/http"
)
type Hello struct{}
func (h Hello) ServeHTTP(
w http.ResponseWriter,
r *http.Request) {
fmt.Fprint(w, "Hello!")
}
func main() {
var h Hello
http.ListenAndServe("localhost:4000", h)
}
$ ./http
...
^C

Для наблюдения эффекта выполнения нашей программы ./http (которая находится после
запуска в блокированном состоянии) заходим браузером по адресу http://localhost:4000/:

213

Рис. 3.2. Локальный веб-сервер

Это тривиальный пример… Однако на использовании пакета net/http в Go легко строят
GUI/WEB интерфейсы к локальным (и не только локальным) приложениям.

Порядок итераций для map: сюрприз
Ещё одна интересная особенность Go, которая обсуждается, как оказалось, в Интернет и активно
и не первый год… (каталог types) — пример удивительно прост:
map4rand.go:
package main
import "fmt"
func main() {
m := map[int]int{
1: 19, 2: 18, 3: 17,
4: 16, 5: 15, 6: 14,
}
fmt.Printf("%v\n", m)
for i := 0; i < 5; i++ {
for key, value := range m {
fmt.Printf("%1d:%2d ", key, value)
}
println()
}
}

А вот его выполнение оказывается не таким простым, и может вызвать удивление (смотрим два
последовательных запуска непосредственно один за другим):
$ ./map4rand
map[1:19 2:18 3:17 4:16 5:15 6:14]
3:17 4:16 5:15 6:14 1:19 2:18
1:19 2:18 3:17 4:16 5:15 6:14
3:17 4:16 5:15 6:14 1:19 2:18
4:16 5:15 6:14 1:19 2:18 3:17
1:19 2:18 3:17 4:16 5:15 6:14
$ ./map4rand
map[1:19 2:18 3:17 4:16 5:15 6:14]
6:14 1:19 2:18 3:17 4:16 5:15
5:15 6:14 1:19 2:18 3:17 4:16
4:16 5:15 6:14 1:19 2:18 3:17
4:16 5:15 6:14 1:19 2:18 3:17
1:19 2:18 3:17 4:16 5:15 6:14

214

Порядок перебора в итерациях map-ов (карт, отображений, таблиц, хэшей, hashmap … как только
их не обзывают) меняется случайным образом не только от запуска к запуску, но и при повторных
последовательных итерациях в уже запущенной программе)! (Точнее, меняется хаотически
порядок выборки, а начальные элемент итератора range … , но и это может смениться в любой
новой версии).
Это не случайность, и не дефект реализации — это сделано разработчиками Go сознательно! См.
[4,5] и в документации [6] в обоснование такого решения … Коротко (в моём переводе) эта
мотивация выглядит так:
Начиная с версии Go 1.0, среда выполнения использует рандомизированный порядок итерации
карт. Программисты начали полагаться на стабильный порядок итераций ранних версий Go,
который варьировался в зависимости от реализации, что приводило к ошибкам
переносимости. Если вам требуется стабильный порядок итераций, вы должны
поддерживать отдельную структуру данных, определяющую этот порядок.

Источники информации
[1] Go в примерах — https://gobyexample.com.ru/
[2] Pi with Machin's formula (Python) — https://literateprograms.org/pi_with_machin_s_formula__python_.html
[3] Подборка проектов для разработки GUI на Go, 6 апреля 2022 —
https://dzen.ru/media/golang/podborka-proektov-dlia-razrabotki-gui-na-go-624d38f4828df173e1426ead
[4] Hashmap(map) по версии Golang вместе с реализацией на дженериках — https://habr.com/ru/post/704796/
[5] Why are iterations over maps random? —
https://stackoverflow.com/questions/55925822/why-are-iterations-over-maps-random
[6] Iterating in maps — https://go.dev/doc/go1#iteration

215

Структуры данных, типы и их методы
Более-менее формально сами типы данных были уже рассмотрены при рассмотрении синтаксиса
Go (так сказать «прямолинейное перечисление»). Здесь же мы вернёмся к рассмотрению на
примерах тех тонких особенностей, которые могут не очевидным образом вытекать из этих
определений.

Массивы и срезы
Массивы и срезы представляют несколько необычные для программиста на C/C++ конструкции в
языке Go. Поэтому вспомним (каталог compare/valadr архива), для начала, что из себя
представляют массивы C (да и C++ тоже):
array_c.c :
#include
void ptrans(int p[], int size) {
int i;
for(i = 0; i < size; i++) p[i]++;
}

// для указателя массива

int main(int argc, char **argv) {
int a1[] = {1, 0, 2, 0, 3, 0, 4},
size = sizeof(a1) / sizeof(a1[0]);
void show(int p[]) {
// для массива
int i;
printf("[ %d ]: ", size);
for(i = 0; i < size; i++) printf("%d ", p[i]);
printf("\n");
}
show(a1);
ptrans(a1, size);
show(a1);
return 0;
}

Массив a1 описан в функции (в данном случае в main(), но это не важно), и в программной
единице, где непосредственно находится его определение, он видится как массив, и для него
может быть вычислен размер как:
size = sizeof(a1) / sizeof(a1[0]);

Но при передаче в качестве параметра вызова любой функции (и всей последующей, возможно,
передаче по цепочке вызовов) массив передаётся по его адресу, как указатель первого элемента
массива, и информация собственно о массиве теряется. Любые изменения параметра,
переданного по адресу, сделанные внутри вызванной функции, отображаются и в вызывающей
единице (побочный эффект):
$ ./array_c
[ 7 ]: 1 0 2 0 3 0 4
[ 7 ]: 2 1 3 1 4 1 5

Примечание: Пример сознательно выписан так, чтобы он был максимально похож на свой эквивалент на языке Go,
который показан далее. В примере использовано такое расширения компилятора GCC (но не допускаемой поздними
стандартами C89 и C99) как вложенное (в main()) описание функция show(). В GCC C++ такое расширение не
допускается.

Теперь рассмотрим аналогичную ситуацию в языке Go:
array_go.go :
package main

216

type arr [7]int

// тип массива

func atrans(v arr) arr {
// для массива
for i, x := range v {
v[i] = x + 1
}
return v
}
func ptrans(p *arr) {
// для указателя массива
for i, x := range *p {
(*p)[i] = x + 1
}
}
func strans(v []int) []int {
// для среза
for i, x := range v {
v[i] = x + 1
}
return v
}
func main() {
show := func(p arr) {
// для массива
print("[ ", len(p), " ]: ")
for _, y := range p {
print(y, " ")
}
print("\n")
}
shows := func(p []int) { // для среза
print("[ ", len(p), " ]: ")
for _, x := range p {
print(x, " ")
}
print("\n")
}
a1 := arr{0: 1, 2: 2, 4: 3, 6: 4}
show(a1)
a2 := *new(arr)
a2 = a1
// присвоение массива
show(a2)
b1 := atrans(a1)
// возврат массива значением
show(a1)
// массив передаётся по значению!
show(b1)
ptrans(&a1)
// передача массива адресом!
show(a1)
a3 := a2[0:len(a2)]
// срез образованный из массива
shows(a3)
strans(a3)
// срез передаётся по адресу
shows(a3)
}

Переменные a1, a2, b1 — это массивы Go. Они имеют тип [7]int (размерность 7 — составная
часть типа массива! — [7]int и [8]int это разные и несовместимые типы). Массивы
передаются в функции show() (описана как вложенная функциональная переменная) и
atrans() по значению, копированием. Поэтому никакие изменения в переданном массиве,
производимые в функции atrans(), не отражаются позже в вызывающей функцию единице.
Более того, массив может так же копированием присваиваться ( a2 = a1) и возвращаться
копированием как результат выполнения функции atrans() (b1 := atrans(a1)). Если нам
нужно иметь побочный эффект изменений переданного массива функцией, следует передавать в

217

функцию (ptrans()) адрес массива.
Но срез a3, образованный над массивом a2, передаются по ссылке, поэтому изменения,
произведенные функцией strans() видны позже в вызвавшей единице.
$
[
[
[
[
[
[
[

./array_go
7 ]: 1 0 2
7 ]: 1 0 2
7 ]: 1 0 2
7 ]: 2 1 3
7 ]: 2 1 3
7 ]: 1 0 2
7 ]: 2 1 3

0
0
0
1
1
0
1

3
3
3
4
4
3
4

0
0
0
1
1
0
1

4
4
4
5
5
4
5

Мы не сможем вызвать функции show(), atrans() и ptrans(), ожидающие параметром
массив типа [7]int, для среза, имеющего тип []int. Симметрично, так же невозможно вызвать
и функции shows() и strans(), ожидающие параметр []int, для массивов [7]int. Это
следствие жёсткой типизации Go.
Отсюда можно наблюдать, что именно срезы в Go ведут себя во многом похоже на массивы C и
C++, и именно срезы наиболее употребимы в практике Go. Отличие их (от C/C++) состоит в том,
что за ними нет необходимости «тянуть» дополнительным параметром их длину — длина всегда
доступна вызовом len() в вызываемой единице.
Ещё одним результатом сравнения может быть то, что срезы Go, являющиеся наложением на
массивы, можно легко изменять в размерах (в пределах len() базового массива!) без накладных
расходов переразмещения в памяти (типа realloc() в C). В чём-то, и в ограниченных пределах,
это напоминает динамические типы STL в C++, например, vector.
Для того, чтобы более наглядно представить что из себя представляют массивы Go, реализуем
аналогичный тип данных в C:
garray_c.c :
#include
#define size 7
typedef struct {
int data[size];
} garrey_t;
void show(garrey_t a) {
int i;
printf("[ %d ]: ", size);
for(i = 0; i < sizeof(a) / sizeof(*a.data); i++)
printf("%d ", a.data[i]);
printf("\n");
}
void ptrans(garrey_t* p) {
// для указателя массива
int i;
for(i = 0; i < size; i++) p->data[i++]++;
show(*p);
}
void atrans(garrey_t a) {
// для массива по значению
int i;
for(i = 0; i < sizeof(a) / sizeof(*a.data); i++)
a.data[i]++;
show(a);
}
int main(int argc, char **argv) {
garrey_t a1 = {{1, 0, 2, 0, 3, 0, 4}};
show(a1);

218

atrans(a1);
show(a1);
ptrans(&a1);
show(a1);
return 0;
}

Это (переменные типа garrey_t) также будут массивы фиксированного размера, которые
передаются в качестве параметров в функции по значению (3-я строка вывода):
$
[
[
[
[
[

./garray_c
7 ]: 1 0 2
7 ]: 2 1 3
7 ]: 1 0 2
7 ]: 2 0 3
7 ]: 2 0 3

0
1
0
0
0

3
4
3
4
4

0
1
0
0
0

4
5
4
5
5

В завершение рассмотрения массивов C, C++ и Go, и передачи параметров вызова в функцию,
вернёмся к уже высказанному ранее утверждению, что во всех этих языках этой группы
параметры любых типов (простых и агрегатных) передаются только по значению, то есть
копированием переданного значения. И для полной ясности незначительно модифицируем уже
показанный выше пример array_c.c:
cpptr.c :
#include
void ptrans(int *p, int size) {
printf("%p ... ", p);
while(size-- > 0) (*p++)++;
printf("%p ... \n", p);
}

// для указателя массива

int main(int argc, char **argv) {
int a1[] = {1, 0, 2, 0, 3, 0, 4},
size = sizeof(a1) / sizeof(a1[0]),
*pa1 = &a1[0];
void show(int *p) {
int i;
printf("[ %d ]: ", size);
for(i = 0; i < size; i++) printf("%d ", p[i]);
printf("\n");
}
printf("%p ... \n", pa1);
show(a1);
ptrans(a1, size);
show(a1);
return 0;
}
$ ./cpptr
0x7fffd1623ab0
[ 7 ]: 1 0 2 0
0x7fffd1623ab0
[ 7 ]: 2 1 3 1

...
3 0 4
... 0x7fffd1623acc ...
4 1 5

Здесь в вызванной функции ptrans() модифицируется как указатель начала переданного
массива, так и его длина. Но вызвавшая функция main() после вызова благополучно продолжает
работать с не испорченным массивом. Это происходит потому, что в ptrans() передавались
копия указателя массива (в 3-й строке вывода видно как эта копия меняется) и копия длины
массива.
Поэтому в корне неверно говорить, что массивы C/C++ и срезы Go передаются при вызове по

219

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

Многомерные срезы и массивы
Массивы и срезы Go, так же как практически во всех других языках программирования, могут
быть многомерными. Чаще всего на практике используются 2-мерные массивы (матрицы), но
могут быть и большей размерности. Элементы многомерного массива индексируются
последовательностью индексов по измерениям, например: A[i][j]. Здесь пока прямые аналогии
с другими языками программирования.
А теперь перейдём к особенностям… Многомерные срезы (но не массивы) могут быть не
прямоугольными! Например (каталог types), треугольная матрица (достаточно широко
применяется в математических расчётах):
3slice.go :
package main
import ("fmt")
func main() {
const dim = 5
twoD := make([][]int, dim)
for i := 0; i < dim; i++ {
innerLen := i + 1
twoD[i] = make([]int, innerLen)
for j := 0; j < innerLen; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2D:", twoD)
fmt.Printf("2D: %v\n", twoD)
}

Обращаем внимание как функции вывода пакета fmt форматируют (пытаются представить) при
выводе данные сложно структурируемых типов: Println() — массивы, а Printf() — данные в
предопределённом для них формате (%v).
$ ./3slice
2D: [[0] [1 2] [2 3 4] [3 4 5 6] [4 5 6 7 8]]
2D: [[0] [1 2] [2 3 4] [3 4 5 6] [4 5 6 7 8]]

Это возможность происходит из того, что массивы и срезы реализуют интерфейс Stringer,
определяя метод String() в своих реализациях. (О чём самому интерфейсу Stringer вовсе не
обязательно «знать» — он только описывает как должен выглядеть прототип метода String()) А
все эти функции пакета fmt, для любого типа, реализующего интерфейс Stringer, должны
только вызывать метод String(). Это подробно описывалось ранее, и составляет фундамент
объектно-ориентированной техники в Go.

Функции с множественным возвратом
Функции Go могут возвращать не одно значение, и не несколько значений запакованных в
единую составную структуру (как в некоторых более старых языках, C/C++), а сколь угодно
много различных значений, в возвращающем return эти значения просто перечисляются через
запятую (это напоминает Python). Такие функции они называют: функции с множественным
возвратом. Самый частый (но не самый хитроумный в использовании) случай их использования
вы будете встречать в Go повсеместно, когда функция своим 2-м возвращаемым значением
возвращает код завершения (ошибки). Основной способ реакции на ошибочные ситуации в Go.
Но область использования такого способа может быть много шире, и определяется только вашей

220

фантазией... Небольшой, но показательный пример (в каталоге function):
marg.go :
package main
import "fmt"
func vals(n int) (int, float64, []string) {
i := n
f := float64(n) / 3
s := []string{"g", "h", "i"}
return i, f, s
}
func main() {
i, f, s := vals(5)
fmt.Println(i, f, s)
_, f, _ = vals(7)
fmt.Printf("%.3f\n", f)
}
$ ./marg
5 1.6666666666666667 [g h i]
2.333

Показательный тем что:


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



Функция, в данном случае, возвращает (специально) значения локальных переменных,
описанных в области видимости внутри тела функции. В C/C++ это абсолютно
недопустимо, но компилировалось бы без ошибок, а затем порождало бы крайне трудно
локализуемые («блуждающие») ошибки. Но Go — это язык с динамической сборкой
мусора, и локально порождённые переменные i, f, s имеют право быть
уничтоженными только тогда, когда исчезнет последняя ссылка на переменные (в
вызывающей функции main()) .



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

У Go очень вариативный синтаксис, отдельные конструкции языка ортогональные (независимо
альтернативные). Имеет смысл для сравнения записать совсем другой вариант записи только-что
показанной функции:
marg2.go :
...
func vals(n int) (i int, f float64, s []string) {
i = n
f = float64(n) / 3
s = []string{"g", "h", "i"}
return;
}
...

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

221

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

222

Строки, руны и UNICODE
Обработка символьной, текстовой информации — это стало на сегодня настолько важной частью
любого языка программирования, что она заслуживает отдельного рассмотрения в примерах. Мы
рассмотрим различные возможности обработки мультиязычных строк: не латинских,
русскоязычных для конкретности, но это может быть и иероглифический китайский, и иврит, с
записью слева-направо, и справа-налево… То, что в традиционных языках (C и C++) делается
совсем не так просто.

Символы, байты и руны
Строки string (зачастую) состоят из последовательности многобайтных (1, 2, 3, 4 …
потенциально до 6 байт) символов UTF-8, представляющих кодовых точек UNICODE ( int32
значения, rune). Структуру string в Go позволяет лучше представить такое простое
приложение (каталог strings/runes):
rune_len.go :
package main
import "fmt"
func main() {
английский := "Hello"
русский := "Привет"
японский := "⌘こんにちは"
show := func(s string) {
fmt.Print(s, " : байт ", len(s),
" символов ", len([]rune(s)), " : ")
for i := 0; i < len(s); i++ {
fmt.Printf(" %02X", s[i])
}
fmt.Print(" [")
for p, c := range s {
fmt.Printf(" %d:%q", p, c)
}
println("]")
}
show(английский)
show(русский)
show(японский)
}

И видим:
$ ./rune_len
Hello : байт 5 символов 5 : 48 65 6C 6C 6F [ 0:'H' 1:'e' 2:'l' 3:'l' 4:'o']
Привет : байт 12 символов 6 : D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82 [ 0:'П' 2:'р' 4:'и' 6:'в'
8:'е' 10:'т']
⌘こんにちは : байт 18 символов 6 : E2 8C 98 E3 81 93 E3 82 93 E3 81 AB E3 81 A1 E3 81 AF [ 0:'⌘'
3:'こ' 6:'ん' 9:'に' 12:'ち' 15:'は']

В итоге:
- итерация по позиции и встроенная функция len() оперируют с отдельными составляющими
байтами UTF-8 символов…
- для того, чтобы иметь дело с последовательными символами, тип string нужно
преобразовывать к срезу []rune
- функция len() для типа string возвращает число байт, эта же функция для преобразованного
среза []rune возвращает число UTF-8 символов в исходной строке;
- итерация, использующая range, ведёт нас по многобайтным символам UTF-8: по порядковым

223

индексам начальных байт каждого символа и позволяет получить rune каждого символа;
Нужно хорошо понимать, что массив (срез) rune не является каким-то «ещё одним»
представлением строки — это числовой int32 массив (2-я строка вывода), и для его вывода
(терминал, печать, графическое окно вывода…), или любого другого использования в кчестве
строки, срез rune должен преобразовываться к типу string:
rune_out.go :
package main
import "fmt"
import "unicode/utf8"
func main() {
j := "世界" // "мир", яп.
fmt.Println("Hello" + j, ": байт", len(j),
", символов", utf8.RuneCountInString(j))
r := []rune(j)
fmt.Printf("%X [%d]\n", r, len(r))
fmt.Printf("%q [%d]\n", r, len(r))
fmt.Println(string(r))
}
$ ./rune_out
Hello 世界 : байт 6 , символов 2
[4E16 754C] [2]
['世' '界'] [2]
世界

(Хотя срез []rune и является срезом именно целочисленных значений int32, для его
форматированного вывода в качестве среза отображаемых символов предоставлен специальный
формат %q).

Изменение содержимого строк
Неоднократно повторялось, и это специально акцентируется в документации Go, что переменные
типа string — не изменяемые! Но не изменяемость эта выражается в том, что нельзя взятьв
строке отдельный символ и поменять его на что-то другое. Но содержимое переменных string
можно сколь угодно изменять дозволенными функциями и методами, которые при этом берут
такую переменную в одном месте памяти и, после её модификации, размещают новую
модифицированную копию в совершенно другом месте памяти. В этом смысле строки Go
совершенно подобны строкам Python, для которых точно таким же образом декларируется
неизменяемость.
Следующее простое приложение демонстрирует изменение любого цифрового символа (0 … 9)
его инкрементом «по кругу» (9 превращается в 0) … приложение бессмысленное по содержанию,
но очень хорошо иллюстрирует общую схему изменения содержимого строки:
map.go :
package main
import (
"bufio"
"fmt"
"os"
"strings"
"unicode"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {

224

инкремент := func(r rune) rune {
if unicode.IsNumber(r) {
return '0' + (r-'0'+1)%10
} else {
return r
}
}
fmt.Println(strings.Map(инкремент, scanner.Text()))
}
}

Первая строка здесь в каждой паре — это исходная строка вводимая нами с терминала, а
следующая — строка изменённая кодом:
$ ./map
12 + 13 = 25
23 + 24 = 36
В 1913 году 1 пуд зерна стоил 34 руб.
В 2024 году 2 пуд зерна стоил 45 руб.

В пакете strings очень много методов подобных strings.Map() — на все потребности и
вкусы! Часть из них выполняют фиксированные действия, по типу strings.TrimLeft() или
strings.TrimRight(), их смысл ясен из названия. Но много и таких, которые включают в свой
прототип функцию преобразования, или предикат фильтрующий символы подлежащие
преобразованию. Для strings.Map(), которую мы только-что видели, прототип записан так:
func Map(mapping func(rune) rune, s string) string

Важно: прототипы функций-операторов в таких вызовах описываются как func(rune) rune
или как func(rune) boolean (это когда логический предикаты отбора для операций) — в
операцию вовлекается одиночный символ UNICODE. (Очень часто, в силу специфики синтаксиса
Go, такие функции-параметры определяются как инлайн лямбда-функции (анонимные),
определяемые непосредственно в месте вызова.) Таким образом нельзя заменить, например, один
символ на 2, или на пустую строку (удалить символ), но вполне возможно 1-байтовый UTF-8
сменить на 4-байтовый. Но подобные методы вполне могут и изменять число символов в строке,
например, функции тримингования:
func TrimLeftFunc(s string, f func(rune) bool) string
func TrimRightFunc(s string, f func(rune) bool) string

Палиндромы
Палиндромы — это слова или целые фразы, которые читаются одинаково в обоих направлениях:
слева направо и справа налево. Самым длинным в мире палиндромом, состоящим из одного
слова, принято считать финское слово saippuakivikauppias (торговец щелоком — мыльным
камнем). В английском языке самый длинный палиндром считают — redivider (своего рода
перегородка).
В русском языке довольно много палиндромов. Нахождение их было модной забавой в
литературных кругах XIX-XX веков, но популярной и на сейчас. Но это и отличная возможность
практики использования строчных функций Go, особенно если применительно не к ASCII
(латинским) наборам символов.
Первой, более простой, задачей мы сделаем тестер слов-палидромов. Это может выглядеть так
(всё в каталоге примеров strings/palindrom):
palindrow.go :
package main
import (
"fmt"
"bufio"
"log"
"os"
)

225

func test(scanner *bufio.Scanner) {
for scanner.Scan() {
ustr := scanner.Text() // введенная строка
rstr := []rune(ustr)
pali := true;
for i := 0; i < len(rstr) / 2; i++ {
pali = (rstr[i] == rstr[len(rstr) - i - 1])
if !pali {
break
}
}
fmt.Printf("%v : %s\n", pali, ustr)
}
}
func main() {
if len(os.Args) > 1 {
file, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer file.Close()
test(bufio.NewScanner(file))
} else {
test(bufio.NewScanner(os.Stdin))
}
}

В главной функции main() только определяется источник исходных строк: либо терминал (если
в командной строке не указан параметр), либо файл со строками (если параметром указано имя
файла). Проверка строк осуществляется в функции test(), которая тут же и выводит вердикт,
является ли строка палиндромом: true или false.
С не-ASCII текстами мы не имеем права перебирать символы в string — это приведёт к
перебору байт в строке, а не многобайтных символов UTF-8 … так же как и функция len()
возвращает длину строки не в символах, а в байтах. Для работы с символами мы должны перейти
от string к срезам из rune!
Консольный режим:
$ ./palindrow
123454321
true : 123454321
123321
true : 123321
123322
false : 123322
радар
true : радар
дерево
false : дерево

Или при чтении из файла (файлы тестовых строк находятся там же в каталоге):
$ ./palindrow words.txt
true : доход
true : шалаш
true : топот
true : радар
true : комок
true : saippuakivikauppias
true : redivider

226

А вот что происходит, если имя файла указать неправильно:
$ ./palindrow worde.txt
2024/01/17 00:04:09 open worde.txt: no such file or directory

Для простейшего случая, для палиндромов-слов это работает достаточно прилично. Но если мы
перейдём к фразам, словам разделённым пробелами, тут нас будут ожидать неприятности …
симитируем слова числовыми значениями:
$ ./palindrow
123 321
true : 123 321
12 33 21
true : 12 33 21
1 23 321
false : 1 23 321

По правилам (достаточно формализованным) составления палиндромов-слов:
- пробелы и знаки препинания в фразе не принимаются во внимание;
- большие и малые литеры не различаются;
- буквы вот в этих парах считаются идентичными: е-ё, и-й, ь-ъ;
Для начала мы исключим пробелы и различия в строчных и прописных буквах … и это нам даст
возможность попрактиковаться в изменениях содержимого string (нас ведь не интересуют сами
палиндромы, а интересуют техники символьной обработки):
palindrob.go :
package main
import (
"fmt"
"bufio"
"log"
"os"
"strings"
)
func test(scanner *bufio.Scanner) {
for scanner.Scan() {
ustr := scanner.Text() // введенная строка
rstr := []rune(strings.ReplaceAll(strings.ToLower(ustr), " ", ""))
pali := true;
for i := 0; i < len(rstr) / 2; i++ {
if pali = rstr[i] == rstr[len(rstr) - i - 1]; !pali {
break
}
}
fmt.Printf("%v : %s\n", pali, ustr)
}
}
...

Вызывающая функция main() остаётся неизменной, поэтому смотрим только тестирующую
функцию и список импорта, который поменялся.
Мы использовали метод

strings.ToLower() — превратить все символы в малые, и
strings.ReplaceAll() — заменить все найденные подстроки в строке, в данном, случае

заменить пробелы на пустую строку, но в общем случае — на что угодно. При этом длина строки,
естественно, изменится! Это очень показательно … это ещё одно разъяснение смысла
неизменяемости строк Go.
Ещё здесь дополнительно показана инициализация значений непосредственно в теле условия
оператора if перед их использованием, после этого эти значения (или даже вновь объявленные

227

переменные) могут использоваться в теле оператора {…} или ветке else {…} (но здесь это не
понадобилось). Это ещё одна, непривычная но приятная, особенность условного оператора Go.
Ручное выполнение:
$ ./palindrob
saippuakivikauppias
true : saippuakivikauppias
Лев осовел
true : Лев осовел
А роза упала на лапу Азора
true : А роза упала на лапу Азора

Здесь всё хорошо, но если мы перейдём к заготовленному файлу фраз, то там не всё так хорошо:
$ ./palindrob palindrom.txt
true : А роза упала на лапу Азора
true : Я иду с мечем судия
true : На в лоб, болван
true : Лев осовел
false : Да, гневен гад
true : Мат и тут и там
false : Лев с ума ламу свёл
true : Кирилл лирик
true : Уж редко рукою окурок держу
true : Коту скоро сорок суток
false : А муза рада музе без ума да разума.
true : Веер веял для евреев
false : Madam, I’m Adam
false : Муха! О, муха! Велика аки лев! Ах, ум! О ах, ум!
true : Sum summus mus
false : Νίψον ανομήματα μη μόναν όψιν
true : Sator Arepo tenet opera rotas
false : Уверена я, а не реву

Здесь нас сбивают знаки препинания и, например, буква ё … И последний вариант, и ещё один
импортированный пакет возможностей: regexp — регулярные выражения!
palindrob.go :
package main
import (
"fmt"
"bufio"
"log"
"os"
"strings"
"regexp"
)
var nonAlphaNum = regexp.MustCompile(`[^a-zа-я0-9 ]+`)
func test(scanner *bufio.Scanner) {
for scanner.Scan() {
ustr := scanner.Text() // введенная строка
s1 := strings.ReplaceAll(strings.ToLower(ustr), " ", "")
s1 = strings.ReplaceAll(s1, "ё", "е")
s1 = strings.ReplaceAll(s1, "й", "и")
rstr := []rune(nonAlphaNum.ReplaceAllString(s1, ""))
pali := true;
for i := 0; i < len(rstr) / 2; i++ {
if pali = rstr[i] == rstr[len(rstr) - i - 1]; !pali {
break
}
}

228

fmt.Printf("%v : %s\n", pali, ustr)
}
}
...

Методы замены strings.ReplaceAll() мы уже видели раньше … а вот пакет regexp стандартной
библиотеки Go покрывает все возможности регулярных выражений Perl, egrep, Python, или
(альтернативно) в синтаксисе POSIX. Метод regexp.MustCompile() компилирует шаблон
регулярного выражения, а метод ReplaceAllString(), применением этого компилированного
шаблона, замещает удовлетворяющие строки (в данном случае удаляет — замещая на пустую
строку).
В итоге:
$ ./palindrom palindrom.txt
true : А роза упала на лапу Азора
true : Я иду с мечем судия
true : На в лоб, болван
true : Лев осовел
true : Да, гневен гад
true : Мат и тут и там
true : Лев с ума ламу свёл
true : Кирилл лирик
true : Уж редко рукою окурок держу
true : Коту скоро сорок суток
true : А муза рада музе без ума да разума.
true : Веер веял для евреев
true : Madam, I’m Adam
true : Муха! О, муха! Велика аки лев! Ах, ум! О ах, ум!
true : Sum summus mus
true : Νίψον ανομήματα μη μόναν όψιν
true : Sator Arepo tenet opera rotas
true : Уверена я, а не реву

Мы на этих 3-х примерах затронули, мельком, все основные техники работы с символьными
строками. Любопытное наблюдение: если для разбора с содержимым отдельных UTF-8 символов
в строке нам требуются типы rune, то библиотечные функции и методы, согласованно меняющие
содержимое строк, работают непосредственно с самими типами string без требования ручных
преобразовавний.

Регулярные выражения
Регулярные выражения — это отдельно стоящая, очень мощная техника работы с текстовой
информацией. Регулярные выражения являются способом описания текстовых шаблонов
(образцов) для сопоставления и выполнения последующих действий по итогам этих
сопоставления. Регулярные выражения являются фундаментальной базой таких инструментов
GNU как grep, egrep, sed, awk и редакторов ed, vi, vim, Emacs. В некотором смысле,
регулярные выражения являются альтернативой функциональным или объектным API строчной
обработки в разных языках программирования. Некоторые авторы считают эту технику излишне
сложной, с высоким порогом вхождения в тему, но все соглашаются с её высокой мощностью.
В Go поддержка регулярных выражений поддерживается пакетом стандартной библиотеки
regexp. В синтаксисе регулярных выражений есть несколько вариантов, разночтений
(диалектов), синтаксис grep несколько отличается от egrep, поэтому говорят о синтаксисе Perl,
Python или POSIX. Синтаксис принятых в Go регулярных выражений — это тот же общий
синтаксис, который используется в Perl, Python … строго, это синтаксис, принятый RE2. Но
может использоваться и синтаксис POSIX, если для подготовки (компиляции) регулярных
выражений использовать regexp.CompilePOSIX() вместо regexp.Compile() (или
regexp.MustCompilePOSIX() вместо regexp.MustCompile(), соответственно).
Техника применения регулярных выражений, хотя знание это и не имеет принципиального
значения для применителя, состоит в том, что символьная строка регулярного выражения
предварительно компилируется в небольшой фрагмент внутреннего кода, который потом

229

применяется к анализируемым текстам (возможно многократно).
Синтаксис записи образцов (шаблонов) сопоставления содержит много спецсимволов, поэтому
для записи строк шаблона документация Go рекомендует использовать не обычную запись (в
двойных кавычках), а сырой (raw) формат, заключаемый в обратные одиночные кавычки (где
спецсимволы никак не интерпретируются).
Вот теперь у нас всё готово чтобы приступить к коду (каталог strings/regexp) … и первый
пример будет выделять нам числовые значения (в примере использовано применение к строкам,
но регулярные выражения могут, и часто, применяться к большим фрагментам обрабатываемого
текста целиком).
finddig.go :
package main
import (
"bufio"
"fmt"
"os"
"regexp"
)
func main() {
re, _ := regexp.Compile(`\d+`)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
s := scanner.Text() // A123 AA455 AAA2 A89
r := re.FindAllString(s, -1)
fmt.Println(r)
// [123 455 2 89]
a := re.FindAllStringIndex(s, -1)
fmt.Println(a)
// [[1 4] [8 11] [15 16] [18 20]]
m := make(map[int]string)
for _, e := range a {
m[int(e[0])] = s[e[0]:e[1]]
}
fmt.Println(m)
// [1:123 8:455 15:2 18:89]
}
}

Особо меня интересовала здесь возможность выделения числовых (ASCII) полей внутри
мультибайтных UTF-8 строк UNICODE, отличных от латиницы:
$ go run finddig.go
A123 AA455 AAA2 A89
[123 455 2 89]
[[1 4] [8 11] [15 16] [18 20]]
map[1:123 8:455 15:2 18:89]
13🔥 + 987 요 + 5£ - 543µ / 4Щ
[13 987 5 543 4]
[[0 2] [9 12] [18 19] [24 27] [32 33]]
map[0:13 9:987 18:5 24:543 32:4]
При Петре I средний заработок неквалифицированного работника составлял 5-8 копеек в день, 1 пуд
мяса тогда стоил 30 копеек, 1 пуд хлеба - 10 копеек.
[5 8 1 30 1 10]
[[133 134] [135 136] [163 164] [203 205] [220 221] [242 244]]
map[133:5 135:8 163:1 203:30 220:1 242:10]
в одном километре 1000 метров, или 100000 сантиметров, или 1000000 милиметров
[1000 100000 1000000]
[[33 37] [59 65] [97 104]]
map[33:1000 59:100000 97:1000000]

Это же пример (при объёмном тестировании) можно запускать из файла предварительно
заготовленных тестовых строк (файл dig.txt в архиве). Так:

230

$ go run finddig.go < dig.txt
...

Или так:
$ cat dig.txt | go run finddig.go
...

Следующий пример чуть сложнее, и показывает не только поиск по шаблону, но и разнообразные
трансформации исходной строки:
replw.go :
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)
func main() {
//
re, _ := regexp.Compile(`\w+`)
//
re, _ := regexp.Compile(`([Ё-Яa-ё]|[a-zA-Z])+`)
//
re, _ := regexp.Compile(`\S+`)
re, _ := regexp.Compile(`\pL+`)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
s := scanner.Text()
r1 := re.FindAllString(s, -1)
fmt.Printf("[%d] => ", len(r1))
for _, c := range r1 {
fmt.Printf(" ", string(c))
}
fmt.Printf("\nfunc: ")
r2 := re.ReplaceAllStringFunc(s, rep2up)
println()
fmt.Printf("[%d] => %v\n", len(r2), string(r2))
println("----------------------------")
}
}
func rep2up (s string) string {
fmt.Printf(" ", s)
return strings.ToLower(s)
}

Здесь упор сделан на операции над мультиязычными UTF-8 строками UNICODE. Здесь шаблон
выбран так, чтобы любую строку-фразу разбить на отдельные слова. Но часто упоминаемый в
публикациях шаблон `\w+` здесь не работает … то есть он работает, но только на ASCII
(англоязычных) строках. Остальные 3 показанных формы будут работать для разных языков.
Здесь некоторые комментарии…
Если мы хотим определить диапазон символов конкретного языка, то прежде должны
пересмотреть кодовые таблицы UNICOD этого языка, потому что символы не всегда следуют
национальным алфавитам. Для русского языка, например, большая буква `Ё` (U+0401)
предшествует `А` (U+0410), а малая `ё` (U+0451) следует за `я` (U+04FF), поэтому шаблон будет:
`([Ё-Яa-ё]|[a-zA-Z])+`.
Метасимвол в шаблоне `\S+` соответствует «любой не пробельный символ». А вот шаблон из
числа новых `\pL+` означает любой символ UNICODE. Поскольку это всё может заметно
отличаться от традиционного синтаксиса регулярных выражений описываемого в литературе,

231

настоятельно рекомендуется изучить краткое руководство по синтаксису для Go, датированное
2023 годом, и ссылка на которое даётся в конце текста.
Но вернёмся к коду… Результатом re.FindAllString() будет массив слов составляющих
исходную фразу — для убедительности выводится размер этого массива. Далее мы для каждого
слова в тестируемой строке (тот же шаблон) делаем замену re.ReplaceAllStringFunc(), но не
просто фиксированную замену, а для каждого найденного слова вызываем нами же определённую
собственную функцию вида func rep2up (s string) string, которая, вообще то говоря,
может делать со строкой всё что угодно! В нашем случае она довольно тривиально преобразует
слово к малым буквам.
И вот как это выглядит (для контроля, функция замены rep2up многократно выводит значения
получаемых ею параметров на обработку):
$ ./replw
Ёлки пойдут на палки
[4] =>
func:
[37] => ёлки пойдут на палки
---------------------------世界
[2] =>
func:



[7] => 世 界
---------------------------В начале было Слово, и Слово было у Бога, и Слово было Бог
[13] =>
func:
[102] => в начале было слово, и слово было у бога, и слово было бог
---------------------------It Is A Short English String
[6] =>
func:
[28] => it is a short english string
----------------------------

Поскольку для отладки утомительно повторять на вводе тестовые строки, то вы их можете
заготовить заранее в файл (образец такого файла прилагается в архиве), и выполнять приложение
так:
$ ./replw < wrd.txt
...

Вариант не со строками string, но с байтовыми срезами []byte (с функцией замены func
rep2up (b []byte) []byte ) очень похож, и приводится в архиве примеров как replwb.go.

Источники информации
[1] Strings, bytes, runes and characters in Go — https://go.dev/blog/strings
[2] Syntax, Aug 11, 2023 — Синтаксис регулярных выражений Go —
https://github.com/google/re2/wiki/Syntax

232

Элементы функционального программирования
Функциональный стиль программирования может показаться, по внешнему виду порождаемых
кодов, искусственным и усложнённым. Но у него много сторонников. Вопрос: так в чём же
особенно хорош функциональный стиль? Да в том, что при многочисленных вызовах чистых (в
идеале) функций не возникает побочных эффектов, изменений данных промежуточных
переменных. А это оберегает от тонких ошибок, с трудом подлежащих локализации и
исправлению.
Язык Go рассматривает функции как объекты первого класса. В частности, это означает, что язык
поддерживает передачу функций в качестве аргументов другим функциям, возврат их как
результат других функций, присваивание их переменным или сохранение в структурах данных.
Здесь имена функций не имеют никакого специального статуса, они рассматриваются как
обычные значения, тип которых является функциональным. А значит это позволяет определять
функции высшего порядка, которые могут работать с функциональными значениями (принимать
на вход функции и возвращать их в качестве результата). А это является основой
функционального программирования. Дополнительную гибкость предоставляют возможности
анонимных функций и функциональных литералов.
И если Go не является и не позиционируется как язык функционального программирования (как
Common Lisp, Scheme, Ocaml, или Haskell), тем не менее он позволяет в очень существенной
степени использовать приёмы функционального программирования.
Основные приёмы функционального стиля программирования кажутся очень необычным,
непонятными … и, временами, даже неприятными для тех, кто работает в императивном
программировании (традиционном). Поэтому в этой части рассмотрения мы будем особо
детально разбирать каждый из элементов и приёмов функционального программирования.

Функциональные замыкания
О функциональных замыканиях (closure) было уже сказано вcкользь при рассмотрении функций
ранее. Это одно из самых полезных понятий функционального программирования. Эта идея
оказалась настолько заманчивой для многих разработчиков, что была реализована даже в
некоторых совершенно не функциональных языках программирования (Perl). Девид Мертц
приводит следующее определение замыкания:
Замыкание - это процедура вместе с привязанной к ней совокупностью данных (в противовес
объектам в объектном программировании, как: «данные вместе с привязанным к ним
совокупностью процедур»).
Смысл замыкания состоит в том, что определение функции «замораживает» окружающий её
контекст на момент определения этой функции (это очень важно, что не на момент вызова).
Функциональные замыкания — это такая мощная техника, которую заимствуют самые
разнообразные языки, не являющиеся языками функционального программирования. Широко
приёмы функционального программирования используются (или предоставляются средства для
реализации) в языке Python (степень «предрасположенности» здесь практически та же, что мы
увидим в Go). Здесь есть уже широта выбора метода реализации для всё того же замыкания,
вариантность — это может делаться разными способами, например, за счёт параметризации
создания функции. Для начала парочка элементарнейших первый пример для понимания того чем
является замыкание, потому что это не так просто как кажется:
clos1.py
#!/usr/bin/python3
def multiplier(n):
def mul(k):
return n * k
return mul

# multiplier возвращает функцию умножения на n

mul3 = multiplier(3)

# mul3 - функция, умножающая на 3

print(mul3(3), mul3(5))

233

Выполнение:
$ ./clos1.py
9 15

Аналогичная «функциональная конкретизаций» посредством замыкания на Go могла бы
выглядеть так:
clos1.go
package main
import "fmt"
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y
}
}
func main() {
add5 := makeAdder(5)
add10 := makeAdder(10)
fmt.Printf("%d %d\n", add5(2), add10(2))
}

// 7 12

С ожидаемым результатом:
$ go run clos1.go
7 12

Другой, по смыслу, способ создания замыкания — это использование значения параметра по
умолчанию в точке определения функции, как показано в следующем листинге:
clos3.py
#!/usr/bin/python3
n = 3
def mult(k, mul = n):
return mul * k
n = 7
print(mult(3))
n = 13
print(mult(5))
n = 10
mult = lambda k, mul = n: mul * k
print(mult(3))
$ ./clos3.py
9
15
30

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

234

может быть реализована в различных языках программирования (каталог всех примеров
functionals/closure).
В C++ ранее версии C++11 (стандарт 2011г.) наиболее распространенный способ создания
функции с скрытым состоянием было использование класса, который перегружает оператор (),
чтобы сделать его экземпляры внешне похожими на вызов функции (функторы). Например,
следующий далее код определяет my_transform() функцию (упрощенная версия STL
std::transform()), которая применяет заданный унарный оператора ( op) к каждому элементу
массива (in), сохраняя результат действия в другой массив ( out). Для накапливающего
сумматора (т.е., { x[0], x[0]+x[1], x[0]+x[1]+x[2], ...}) код создает функтор ( MyFunctor), который
отслеживает сохраняемое состояние ( total) и передает его экземпляр функтору для выполнения
my_transform():
clos_cc.cc :
#include
#include
template
void my_transform(size_t n_elts, int* in, int* out, UnaryOperator op) {
for(size_t i = 0; i < n_elts; i++)
out[i] = op(in[i]);
}
class MyFunctor {
public:
int total;
int operator()(int v) {
return total += v;
}
MyFunctor() : total(0) {}
};
int main( void ) {
int data[7] = {8, 6, 7, 5, 3, 0, 9};
const int len = sizeof(data) / sizeof(data[0]);
int result[len];
MyFunctor accumulate;
my_transform(len, data, result, accumulate);
std::cout *(j + 1)) {
data_t k = *j;
*j = *(j + 1);
*(j + 1) = k;
m++;
}
return m;
}
void show(vector v) { // отладка-контроль
cout 1,

char debug = 0;
ulong nopr = 0;
void put(int from, int to) {
++nopr;
if(!debug) return;
printf("%d => %d,
", from, to);
if(0 == (nopr % 5)) printf("\n");
}
int temp(int from, int to) {
int i = 1;
for(; i 1) move(from, temp(from, to), n - 1);
put(from, to);
// единичное перемещение
if(n > 1) move(temp(from, to), to, n - 1);
}
int main(int argc, char **argv, char **envp) {
if(argc != 2)
printf("usage: %s [-]\n", argv[0]), exit(1);
int size = atoi(argv[1]);
// число переносимых фишек
if(size < 0) size = -size, debug = 1;
if(debug) printf("размер пирамиды: n=%d\n", size);
move(1, 3, size);
// вот и всё решение!
if(debug && (nopr % 5) != 0) printf("\n");
printf("число перемещений %ld\n", nopr);
return 0;
}

Как можно видеть, всю работу выполняет рекурсивная функция move(int from, int to, int
n) - переместить верхнюю под-пирамидку размером n колец со штыря номером from [1, 2, или 3]
на штырь to [1, 2, или 3] :
- если требуется переместить только одно верхнее кольцо (n == 1), то просто взять его и
переложить;
- а вот если n > 1, то а). переложить всю верхнюю под-пирамидку размерностью (n — 1) на
оставшийся свободным промежуточный штырь (не from и не to), б). переместить одно
оставшееся (последнее) кольцо на место назначения to, в). после чего опять же всю подпирамидку размерностью (n — 1) с промежуточного штыря также перенести поверх кольца,
уложенного на место назначения to.
Теперь то же самое, выраженное на языке Go:
hanoy_go.go :
package main
import("os"; "strconv")
var debug bool = false
var nopr uint64 = 0
func move(from, to, сколько int) {
put := func(from, to int) {
nopr++
if !debug { return }
print(from, " => ", to, ",
")

253

if 0 == (nopr % 5) { print("\n") }
}
temp := func(from, to int) int {
// промежуточная позиция
for i := 1; i 1 { move(from, temp(from, to), сколько - 1) }
put(from, to)
// единичное перемещение
if сколько > 1 { move(temp(from, to), to, сколько - 1) }
}
func main() {
if len(os.Args) != 2 {
print("usage: ", os.Args[0], " [-]\n")
return
}
debug = false
размер, _ := strconv.Atoi(os.Args[1])
if размер < 0 {размер, debug = -размер, true}
if debug { print("размер пирамиды: n=", размер, "\n") }
move(1, 3, размер)
// вот и всё решение!
if debug && (nopr % 5) != 0 { print("\n") }
print("число перемещений ", nopr, "\n")
}

Скомпилируем всё это (для широты сравнения) различными компиляторами:
$ make
gcc -O3 -Wall hanoy_c.c -o hanoy_c
clang -O3 -xc -Wall hanoy_c.c -o hanoy_cl
gccgo -O3 -Wall hanoy_go.go -g -o hanoy_go
go build -o hanoy_gol -compiler gc hanoy_go.go

В результате:
$ time ./hanoy_c 30
число перемещений 1073741823
real
0m3,991s
user
0m3,991s
sys
0m0,000s
$ time ./hanoy_cl 30
число перемещений 1073741823
real
0m3,564s
user
0m3,560s
sys
0m0,004s
$ time ./hanoy_go 30
число перемещений 1073741823
real
0m4,577s
user
0m4,556s
sys
0m0,032s
$ time ./hanoy_gol 30
число перемещений 1073741823
real
0m8,455s
user
0m8,442s
sys
0m0,028s

На таком типе задач (при максимальном уровне оптимизации доступном GCC) GoLang
реализация на таком объёме вычислений показывает результат только вдвое хуже GCC, что

254

можно считать вполне приемлемым. Отличные результаты показывает и относительно новый
компилятор C/C++ динамично развивающегося проекта Clang, немногим даже лучше GCC.

Решето Эратосфена
Ещё один хорошо известный алгоритм: поиск всех простых чисел (меньше N) прореживанием
натурального ряда чисел [1 ... N], который приписывают древнегреческому математику
Эратосфену Киренскому. В схематичном описании это выглядит так: нахождение всех простых
чисел не больше заданного числа n, нужно выполнить следующие шаги:
1.
2.
3.
4.

Выписать подряд все целые числа от двух до n (2, 3, 4, …, n).
Пусть переменная p изначально равна 2 — первому простому числу.
Зачеркнуть в списке числа от p+1 до n кратные p: 2p, 3p, 4p, …).
Найти первое ещё не вычеркнутое число в списке, большее чем p, и присвоить значению
переменной p это число.
5. Повторять шаги 3 и 4, пока p не достигнет n.
Есть некоторые алгоритмические улучшения этой схемы (в коде далее), но мы не будем
сосредотачиваться на этих деталях (каталог compare/erastof).
На языке C реализация может выглядеть так:
erastof_c.c :
#include
#include
typedef long long data_t;
void eratos(char *arr, ulong size) {
ulong i, j;
for(i = 2; i < size; i++)
// цикл по всему массиву от первого простого числа
if(1 == arr[i])
for(j = i + i; j < size; j += i) // вычеркивание всех чисел кратных i
arr[j] = 0;
}
int main(int argc, char **argv) {
long n;
char debug = 0;
if(argc != 2)
printf("usage: %s [-]\n", argv[0]), exit(1);
n = atol(argv[1]);
// максимальное число
if(n < 0) n = -n, debug = 1;
ulong k, j;
char *a = calloc(n + 1, sizeof(char));
a[0] = a[1] = 0;
// вычёркиваем "0" и "1"
for(k = 2; k < n; k++) a[k] = 1;
// остальные размечаем как простые
eratos(a, n);
for(k = 0, j = 0; k < n; k++)
j += (a[k] != 0 ? 1 : 0);
printf("простых чисел %lu\n", j)
if(debug) {
#define INLINE 10
for(k = 0, j = 0; k < n; k++)
if(1 == a[k]) {
j++;
printf("%lu%s", k, (0 == j % INLINE ? "\n" : "\t"));
}
if(j % INLINE != 0) printf("\n");
}
free(a);
return 0;
}

255

Эквивалент на языке Go может выглядеть так:
erastof_go.go :
package main
import("os"; "strconv")
func main() {
type data_t int64
var срез []bool;
debug := false
eratos := func () {
count:= func () data_t {
var j data_t
for i := range срез { if срез[i] { j++ } }
return j
}
for i := range срез {
if срез[i] {
for j := i + i; j < len(срез); j += i {
срез[j] = false // вычеркивание всех чисел кратных i
}
}
}
print("простых чисел ", count(), "\n")
}
show := func (s []bool) {
// диагностика среза
const inlin = 10
var j data_t
for i := range s {
if s[i] {
j++
print(i, "\t")
if 0 == (j % inlin) { print("\n") }
}
}
if(j % inlin != 0) { print("\n") }
}
if len(os.Args) != 2 {
print("usage: ", os.Args[0], " [-]\n")
return
}
n, _ := strconv.Atoi(os.Args[1])
var длина data_t = data_t(n)
if длина < 0 { длина, debug = -длина, true }
срез = make([]bool, длина)
for i := range срез { if i > 1 { срез[i] = true } }
eratos()
if debug { show(срез) }
}

Помимо прочего, здесь показана вложенность описаний функций глубиной больше единичной
(функция count() вложена в функцию eratos(), которая, в свою очередь вложена в функцию
main()). Такую иерархию вложенных описаний можно строить на произвольную глубину. И
здесь же показано то, как эти вложенные функции используют глобальные по отношению к их
собственным описаниям переменные (описанные вне тела функций): функции и count() и
eratos() работают с переменными срез и debug, описанными на внешнем по отношению к
функциям уровне. Это напоминает области видимости имён, как они определены, например, в
языках Н.Вирта Pascal и Modula-2, и открывает весьма широкие перспективы использования.
Но вернёмся к сравнению реализаций...
$ make

256

gcc -O3 -Wall erastof_c.c -o erastof_c
clang -O3 -xc -Wall erastof_c.c -o erastof_cl
gccgo -O3 -Wall erastof_go.go -g -o erastof_go
go build -o erastof_gol -compiler gc erastof_go.go

И вот сравнительное выполнение полученных бинарных исполнимых файлов:
$ time ./erastof_c 50000000
простых чисел 3001134
real
0m1,232s
user
0m1,196s
sys
0m0,036s
$ time ./erastof_cl 50000000
простых чисел 3001134
real
0m1,237s
user
0m1,205s
sys
0m0,032s
$ time ./erastof_go 50000000
простых чисел 3001134
real
0m1,339s
user
0m2,453s
sys
0m0,104s
$ time ./erastof_gol 50000000
простых чисел 3001134
real
0m1,334s
user
0m1,436s
sys
0m0,029s

На этом классе задач (многократное сканирование массивов) языки (и C и Go) и используемые
компиляторы (GCC, Clang, GoLang) показывают практически идентичные цифры
производительности (но заметим при этом, что мы здесь из GCC выжимаем масимально
возможный уровень оптимизации кода: -O3, без оптимизации это время станет порядка 8.5s).

257

Многопроцессорные параллельные вычисления
Скорость активации параллельных ветвей
Скорость имеет значение.
Павел Дуров.
Прежде чем рассматривать примеры конкретных задач, зададимся таким интересным вопросом
— раньше уже речь шла о 3-х моделях параллелизма, исторически внедрявшиеся именно в такой
последовательности: 1). параллельные процессы, вызов fork(), 2). параллельные потоки ядра
pthread_t, 3). лёгкие go-рутины (сопрограммы). Попробуем оценить (каталог архива
goproc/gotime) время активации для N параллельных ветвей в каждой из моделей. И начнём
именно с go-рутин, потому что именно это есть основной предмет нашего интереса…
gotim1.go :
package main
import ("os"; "strconv"; "fmt"; "time")
func sequgo(n int, c chan bool) {
if n == 0 {
c 1 {
n, _ = strconv.Atoi(os.Args[1])
}
c := make(chan bool)
t0 := time.Now()
for i := 0; i < n; i++ {
go func() { c 1 ? atol(argv[1]) : 1000;
pthread_t tid[n];
struct timeval t0, t1;
gettimeofday(&t0, NULL);
for(int i = 0; i < n; i++)
pthread_create(tid + i, NULL, tfunc, NULL);
for(int i = 0; i < n; i++)
pthread_join(tid[i], NULL);
gettimeofday(&t1, NULL);
double t = (t1.tv_usec - t0.tv_usec) + (t1.tv_sec - t0.tv_sec) * 1000000;
if(t < 1000)
printf("время выполнения: %.3fµs\n", t);
else
printf("[%ld] время выполнения: %.3fms\n", n, t / 1000.);
return 0;
}

Сборка:
$ make
go build -o gotim1 -compiler gc gotim1.go
go build -o gotim2 -compiler gc gotim2.go
gcc -Wall forktim.c -lpthread -o forktim
gcc -Wall thretim.c -lpthread -o thretim

И теперь можно запустить и сравнить все варианты сразу:
$ ./gotim1
[1000] время выполнения: 2.364592ms
$ ./gotim2
[1000] время выполнения: 2.689867ms
$ ./forktim
[1000] время выполнения: 59.344ms
$ ./thretim
[1000] время выполнения: 36.257ms
$ ./gotim1 10
[10] время выполнения: 66.441µs
$ ./gotim2 10
[10] время выполнения: 114.853µs
$ ./forktim 10
время выполнения: 670.000µs
$ ./thretim 10
время выполнения: 508.000µs
$ ./gotim1 10000
[10000] время выполнения: 24.849198ms
$ ./gotim2 10000
[10000] время выполнения: 28.417926ms
$ ./thretim 10000
[10000] время выполнения: 354.031ms
$ ./forktim 10000
[10000] время выполнения: 625.808ms

260

Результаты говорят само за себя, впечатляют и не требуют, думаю, комментариев. При
существенно больших значениях N (100000 и далее, в зависимости от ресурсов конкретного
компьютера) варианты и для fork() и для pthread_create() просто сходят с ума, а компьютер
в целом ведёт себя загадочным образом… вплоть до неустойчивости операционной системы,
потому что это механизмы ядра системы. Но … наблюдаем сюда:
$ ./gotim1 1000000
[1000000] время выполнения: 2.242563217s
$ ./gotim2 1000000
[1000000] время выполнения: 5.352434703s
$ ./gotim1 10000000
[10000000] время выполнения: 22.5808485s
$ ./gotim2 10000000
[10000000] время выполнения: 53.33367164s

P.S. Тем не менее, чтобы быть совсем честным и корректным в сравнениях, сейчас и на будущее,
нужно отметить здесь, для объективности, что в системе Linux установлены ограничения на а).
число процессов которое может одновременно создать один пользователь и б). общее число
потоков ядра, которые могут существовать в системе.
Число процессов на пользователя:
$ ulimit -u
386094

И этот предел может быть программно изменён (обращаем внимание, что все эти,
принципиальные для системы, установки мы можем делать только от имении root, о чём и
напоминает показанный значок приглашения командной строки):
# ulimit -u 500000
# ulimit -u
500000

Общее число потоков ядра в системе:
$ cat /proc/sys/kernel/threads-max
772189

Что так же может быть изменено программно:
# echo 1100000 > /proc/sys/kernel/threads-max
# cat /proc/sys/kernel/threads-max
1100000

Но, кроме этих системных ограничений (для чего они и введены) число и процессов и
потоков может ограничиваться конечностью физических ресурсов (в первую очередь оперативной
памятью) вашего конкретного компьютера. Нужно учитывать, что каждый поток (и процесс,
представленный хотя бы единичным потоком) резервирует блок под стек вызовов (и блок под
обработку ожидающих сигналов):
$ ulimit —help
...
-i
the maximum number of pending signals
...
-s
the maximum stack size
$ ulimit -i
386094
$ ulimit -s
8192

Если же вы хорошо знаете, что ваш код не делает глубоких вызовов (например рекурсивных)

261

и не использует больших объёмов стека, то вот такие установки, например, позволяют выполнять
до 100000 ptread_t:
# ulimit -s
# ulimit -i

256
120000

Гонки
Безумие — это точное повторение одного и
того же действия. Раз за разом, в надежде на
получение другого результата.
Альберт Эйнштейн
Даже квази-параллельное выполнение на одном процессоре создаёт серьёзные проблемы,
неизвестные «последовательностным» программистам, и проявляющееся как серьёзные ошибки,
с трудом поддающиеся диагностике и локализации . А в многопроцессорной среде их частота их
возникновения тысячекратно возрастает. Это проблемы связанные с доступом и модификация
значений переменных из различных ветвей одновременно. В общем виде весь класс таких
ошибок, а они могут возникать и выявляться по-разному называют как гонки, или состояние
гонок. Общая природа их в том, что, хотя каждая параллельная ветвь развивается
последовательно и детерминировано, в параллельной среде мы не можем сказать событие x в
ветви A предшествует событию y в ветви B, происходит с ним одновременно, или позже него.
Частота выявления гонок тем выше, чем легче механизм переключения ветвей. Поэтому в Go в
параллельных горутинах они выявляются чаще, их проще наблюдать. Но выявляются или не
выявляются состояния гонок — от этого скрытых ошибок в коде остаётся ровным счётом столько
же. Поэтому в многопроцессорном исполнении места потенциальных возникновений гонок
нужно прогнозировать заранее и их предотвращать.
Первым примером (всё в каталоге goproc/concurent) мы смоделируем ошибочную ситуацию,
которая в параллельной среде выполнения будет создавать состояние гонок:
race.go :
package main
import ("fmt"; "os"; "strconv"; "time")
func main() {
ветви := 3
if len(os.Args) > 1 {
ветви, _ = strconv.Atoi(os.Args[1])
}
fmt.Printf("число ветвей выполнения: %v\n", ветви)
ch := make(chan bool)
var counter int64 = 0
const циклы = 1000000;
t := time.Now()
for i := 0; i < ветви; i++ {
go func() {
defer func() { ch 1.644934 (-3.155929e-08) [159.351051ms]
$ ./bazel0 1e-15
[1.00e-15:31622777] 1.644934 -> 1.644934 (-3.155929e-08) [159.351051ms]

Вот так! Последовательный вариант медленнее в 100 раз! Этого следовало ожидать, но не в такой
степени… Но мы можем рассчитывать, что нас, как в предыдущих задачах ранее, спасёт большое
число параллельных ветвей обработки:
$ ./bazel 2 1e-13
[1.00e-13:2:3162342] 1.644934 -> 1.644934 (-3.162212e-07) [1.917860121s]
$ ./bazel 10 1e-13
[1.00e-13:10:3162322] 1.644934 -> 1.644934 (-3.162232e-07) [2.539155868s]
$ ./bazel 20 1e-13
[1.00e-13:20:3162314] 1.644934 -> 1.644934 (-3.162240e-07) [3.09384969s]
$ ./bazel 40 1e-13
[1.00e-13:40:3162284] 1.644934 -> 1.644934 (-3.162270e-07) [3.068690143s]

Результат на многих процессорах ещё хуже чем на одном! И, более того, чем больше процессоров
— тем медленнее итоговая работа.
И только теперь мы можем попытаться проанализировать то что мы видели, и сделать выводы на
будущее:


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



Для синхронизации используем специальные примитивы синхронизации, обычно
известные как элементы из области IPC (Inter Process Communication): атомарные
переменные, мютексы, условные переменные, семафоры, каналы и др.



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



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



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

278



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



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

Источники информации
[1] Простой пример использования goroutines в языке Go, Владимира Солонин, 3 апреля 2015 —
https://eax.me/go-goroutines/
[2] Использование пакета Flag в Go, January 24, 2020 —
https://www.digitalocean.com/community/tutorials/how-to-use-the-flag-package-in-go-ru
[3] ПЛК — что это такое? — https://habr.com/ru/post/139425/?

279

Заключение
В Go ещё множество интересных «фишек»… Но книга эта не столько про Go, сколько про то, как
в своём программном коде использовать множество процессоров одновременно,
предоставляемых оборудование. И о том, что именно Go изначально «заточен» на такое
использование — его авторы предвосхитили именно такое направление развитие компьютерных
тенденций.
Эра повышения производительности компьютерных систем за счёт скорости (тактовой частоты)
процессора, протяжённостью в 50-60 лет, завершилась. Началась эра повышения
производительности за счёт числа процессоров. И вопрос того как использовать эту
изменившуюся реальность ещё требует осмысления...

280

Об авторе
Цилюрик Олег Иванович — практик-разработчик архитектуры (системотехники) и, главным
образом, программного обеспечения крупных компьютерных проектов со стажем практических
разработок больше 40 лет. Ещё в последние десятилетие существования СССР участвовал в
больших проектах области ВПК СССР, в составе крупнейших инженерно-научных центров:
ВНИИ РТ, КБ ПМ г.Москва.
С того времени непрерывно работал в самых разнообразных разработческих проектах. Как
обобщение этого опыта издал 2 книги в издательствах Москвы и С.-Петербурга, и около 20 книг в
электронном виде, достаточно известных в программистских кругах.
Параллельно работал 5-6 лет научным редактором переводной компьютерной литературы
издательства «Символ-Плюс» — хорошо известная серия компьютерной литературы «со
зверушками на обложке».
Автор известного курса по разработке модулей ядра, драйверов Linux, гуляющего по Интернет в
виде конспекта на протяжении 10 лет, который разрабатывался для проведения учебных
тренингов программистов-разработчиков известной международной софтверной фирмы Global
Logic.
Работу с языком Go начал с 2013 года, по заказу той же компании Global Logic учебный курс,
известный (гуляющий по Интернет) в электронных публикациях как «Go конспект» — адаптация
в Go практикующих программистов C/C++, и хронологически первый в русскоязычных
публикациях обстоятельный курс Go.
Настоящий текст является радикальной переработкой того, что было написано в 2012-2014 годах,
но, более того, он является «уклоном» в другую сторону — использование Go в
многопроцессорных системах для их эффективного использования.

281