Облачный Go. Создание надежных служб в ненадежных окружениях [Мэтью А. Титмус] (pdf) читать онлайн

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


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

Cloud Native Go
Building Reliable Services
in Unreliable Environments

Matthew A. Titmus

Beijing • Boston • Farnham • Sebastopol • Tokyo

Облачный Go
Создание надежных служб
в ненадежных окружениях

Мэтью А. Титмус

Москва, 2022

УДК 004.432
ББК 32.972.1
Э98

Э98

Титмус М. А.
Облачный Go / пер. с англ. А. Н. Киселева. – М.: ДМК Пресс, 2022. – 418 с.:
ил.
ISBN 978-5-97060-965-1
Go – первый язык программирования, спроектированный специально для
разработки облачных приложений. В настоящее время он занял лидирующие
позиции в облачной разработке и используется повсюду: от Docker до Harbour,
от Kubernetes до Consul, от InfluxDB до CockroachDB.
Требования к масштабированию вынуждают разработчиков размещать свои
сервисы на десятках и сотнях серверов – IT-отрасль постепенно становится
«облачной». Но как разрабатывать и поддерживать такой сервис? В этой книге
описывается практическая реализация сложных принципов проектирования облачных вычислений с помощью Go. Издание адресовано опытным разработчикам,
особенно инженерам веб-приложений и инженерам по надежности, которые
решают задачи управления и развертывания облачных приложений.

УДК 004.432
ББК 32.972.1

Authorized Russian translation of the English edition of Cloud Native Go ISBN 9781492076339.
This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. Russian language edition copyright © 2022 by DMK
Press. All rights reserved.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.

ISBN 978-1-492-07633-9 (англ.)
ISBN 978-5-97060-965-1 (рус.)

© Matthew A. Titmus, 2021
© Перевод, оформление, издание,
ДМК Пресс, 2022

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

Отзывы о книге «Облачный Go»
Автор книги проделал большую работу, подробно описав высокоуровневую
концепцию «облачных приложений» и приемы ее реализации с использованием современного языка программирования Go. В результате получилась
захватывающая и вдохновляющая книга.
– Ли Атчисон (Lee Atchison)
Владелец Atchison Technology LLC
Это первая книга, из встречавшихся мне, которая с такой широтой и глубиной освещает современные приемы реализации облачных вычислений.
Представленные здесь шаблоны сопровождаются наглядными примерами
решения реальных задач, с которыми инженеры сталкиваются ежедневно.
– Альваро Атьенза (Alvaro Atienza)
Инженер по надежности, Flatiron Health
На страницах этой книги ясно (и с юмором) отражен богатый опыт Мэтта
в искусстве и науке построения надежных систем в принципиально ненадежном мире. Присоединяйтесь к нему, и он познакомит вас с фундаментальными строительными блоками и приемами конструирования систем,
позволяющими создавать масштабные и надежные системы из эфемерных
и ненадежных компонентов современной облачной инфраструктуры.
– Дэвид Никпонски (David Nicponski)
Главный инженер, Robinhood
За последние несколько лет наметились две важные тенденции: язык Go все
чаще используется для разработки не только серверных компонентов, но
и инф­раструктуры; а инфраструктура перемещается в облако. В этой книге
кратко описывается современное состояние сочетания этих двух факторов.
– Натали Пистунович (Natalie Pistunovich)
Ведущий пропагандист передовых методов разработки, Aerospike
Я начал читать эту книгу, почти ничего не зная о Go, и закончил, чувствуя
себя экспертом. Я бы даже сказал, что, просто прочитав эту книгу, я стал намного более опытным инженером.
– Джеймс Куигли (James Quigley)
Инженер по надежности систем, Bloomberg

Содержание
От издательства. ...................................................................................................17
Об авторе. ................................................................................................................18
Об иллюстрации на обложке. ........................................................................19
Предисловие...........................................................................................................20
Часть I. ОБЛАЧНОЕ ОКРУЖЕНИЕ ............................................................24
Глава 1. Что такое «облачное» приложение?........................................25
История развития до настоящего времени. ...........................................................26
Что значит быть «облачным»?..................................................................................28
Масштабируемость.................................................................................................28
Слабая связанность................................................................................................29
Устойчивость...........................................................................................................30
Управляемость........................................................................................................32
Наблюдаемость.......................................................................................................33
Что особенного в облачном окружении?. ...............................................................34
Итоги.............................................................................................................................35

Глава 2. Почему Go правит облачным миром........................................36
Как появился Go..........................................................................................................36
Особенности облачного мира...................................................................................37
Композиция и структурная типизация...............................................................37
Понятность..............................................................................................................39
Модель взаимодействия последовательных процессов...................................40
Быстрая сборка. ......................................................................................................41
Стабильность языка. ..............................................................................................42
Безопасность памяти.............................................................................................42
Производительность..............................................................................................43
Статическая компоновка.......................................................................................44
Статическая типизация.........................................................................................45
Итоги.............................................................................................................................46

Часть II. ОБЛАЧНЫЕ КОНСТРУКЦИИ В GO ......................................47
Глава 3. Основы языка Go................................................................................48
Базовые типы данных................................................................................................48
Логические значения.............................................................................................49

8  Содержание
Простые числа.........................................................................................................49
Комплексные числа................................................................................................50
Строки......................................................................................................................50
Переменные.................................................................................................................51
Сокращенная форма объявления переменных. ................................................51
Нулевые значения..................................................................................................52
Пустой идентификатор..........................................................................................53
Константы................................................................................................................54
Контейнеры: массивы, срезы и ассоциативные массивы. ...................................54
Массивы...................................................................................................................55
Срезы........................................................................................................................55
Работа со срезами...............................................................................................56
Оператор извлечения среза..............................................................................58
Строки и срезы. ..................................................................................................58
Ассоциативные массивы.......................................................................................59
Проверка наличия в ассоциативном массиве................................................60
Указатели. ....................................................................................................................61
Управляющие структуры...........................................................................................62
Забавный цикл for. .................................................................................................63
Универсальная инструкция for.........................................................................63
Обход в цикле элементов массивов и срезов.................................................64
Обход в цикле элементов ассоциативных массивов. ...................................65
Инструкция if. .........................................................................................................65
Инструкция switch..................................................................................................66
Обработка ошибок......................................................................................................67
Создание ошибки. ..................................................................................................68
Необычные особенности функций: переменное число параметров
и замыкания................................................................................................................69
Функции...................................................................................................................69
Несколько возвращаемых значений...............................................................69
Рекурсия...............................................................................................................70
Отложенные вычисления..................................................................................70
Указатели как параметры.................................................................................72
Функции с переменным числом аргументов.....................................................73
Передача срезов в параметре с переменным числом значений. ...............74
Анонимные функции и замыкания.....................................................................74
Структуры, методы и интерфейсы...........................................................................75
Структуры................................................................................................................76
Методы.....................................................................................................................77
Интерфейсы. ...........................................................................................................78
Проверка типа.....................................................................................................79
Пустой интерфейс..............................................................................................79
Композиция путем встраивания типов. .............................................................80
Встраивание интерфейсов................................................................................80
Встраивание структур........................................................................................81
Продвижение. .....................................................................................................81
Прямой доступ к встроенным полям..............................................................81
Самое интересное: конкуренция. ............................................................................82

Содержание  9

Сопрограммы..........................................................................................................82
Каналы......................................................................................................................83
Блокировка канала.............................................................................................83
Буферизация каналов........................................................................................84
Закрытие каналов. .............................................................................................84
Прием значений из канала в цикле.................................................................85
select. ........................................................................................................................85
Реализация тайм-аутов для каналов...............................................................86
Итоги.............................................................................................................................87

Глава 4. Шаблоны программирования облачных приложений. ....88
Пакет context. ..............................................................................................................89
Что может дать контекст. ......................................................................................90
Создание контекста................................................................................................91
Определение крайних сроков и тайм-аутов контекста....................................91
Определение значений в контексте запроса. ....................................................92
Использование контекста. ....................................................................................92
Структура этой главы.................................................................................................93
Шаблоны стабильности..............................................................................................94
Circuit Breaker (Размыкатель цепи). ....................................................................94
Применимость....................................................................................................94
Реализация..........................................................................................................95
Пример кода........................................................................................................96
Debounce (Антидребезг)........................................................................................97
Применимость....................................................................................................97
Компоненты........................................................................................................98
Реализация..........................................................................................................98
Пример кода........................................................................................................99
Retry (Повтор)........................................................................................................101
Применимость..................................................................................................101
Компоненты......................................................................................................102
Реализация .......................................................................................................102
Пример кода......................................................................................................102
Throttle (Дроссельная заслонка).........................................................................104
Применимость..................................................................................................104
Компоненты......................................................................................................105
Реализация........................................................................................................105
Пример кода......................................................................................................106
Timeout (Тайм-аут)...............................................................................................107
Применимость..................................................................................................107
Компоненты......................................................................................................107
Реализация........................................................................................................108
Пример кода......................................................................................................108
Шаблоны конкуренции............................................................................................110
Fan-In (Мультиплексор).......................................................................................110
Применимость..................................................................................................110
Компоненты......................................................................................................110
Реализация........................................................................................................110

10  Содержание
Пример кода......................................................................................................111
Fan-Out (Демультиплексор)................................................................................112
Применимость..................................................................................................112
Компоненты......................................................................................................112
Реализация........................................................................................................113
Пример кода......................................................................................................113
Future (В будущем). ..............................................................................................114
Применимость..................................................................................................115
Компоненты......................................................................................................116
Реализация........................................................................................................116
Пример кода......................................................................................................116
Sharding (Сегментирование)...............................................................................118
Применимость..................................................................................................118
Компоненты......................................................................................................119
Реализация........................................................................................................119
Пример кода......................................................................................................121
Итоги...........................................................................................................................124

Глава 5. Конструирование облачной службы......................................125
Давайте создадим службу!.......................................................................................125
Что такое хранилище пар ключ/значение?......................................................126
Требования. ...............................................................................................................126
Что такое идемпотентность, и почему это важно?. ........................................126
Конечная цель.......................................................................................................128
Итерация 0: базовая функциональность...............................................................128
Наш суперпростой API.........................................................................................129
Итерация 1: монолит................................................................................................130
Создание HTTP-сервера с использованием net/http. .....................................131
Создание HTTP-сервера с использованием gorilla/mux.................................132
Создание минимальной службы....................................................................133
Инициализация проекта с по­мощью модулей Go.......................................133
Переменные в путях URI.................................................................................134
Множество сопоставлений. ............................................................................135
Создание службы RESTful....................................................................................135
Методы RESTful................................................................................................136
Реализация функции создания......................................................................136
Реализация функции чтения..........................................................................138
Добавление в структуру данных поддержки использования
в конкурентном окружении................................................................................140
Интеграция мьютекса чтения/записи в приложение.................................141
Итерация 2: долговременное хранение ресурса..................................................142
Что такое журнал транзакций?...........................................................................143
Формат журнала транзакций..........................................................................143
Интерфейс регистратора транзакций...........................................................144
Сохранение состояния в журнале транзакций.................................................144
Создание прототипа регистратора транзакций..........................................145
Определение типа события............................................................................146

Содержание  11

Реализация FileTransactionLogger..................................................................148
Создание экземпляра FileTransactionLogger................................................149
Добавление записей в конец журнала транзакций.....................................150
Использование bufio.Scanner для воспроизведения транзакций
из журнала.........................................................................................................151
Интерфейс регистратора транзакций (еще раз)..........................................153
Инициализация FileTransactionLogger в веб-службе..................................153
Интеграция FileTransactionLogger в веб-службу. ........................................155
Будущие улучшения.........................................................................................155
Сохранение состояния во внешней базе данных. ...........................................155
Работа с базами данных в Go..........................................................................156
Импортирование драйвера базы данных.....................................................157
Реализация PostgresTransactionLogger..........................................................157
Создание экземпляра PostgresTransactionLogger........................................158
Выполнение SQL-запроса INSERT с по­мощью db.Exec...............................160
Использование db.Query для воспроизведения транзакций
из журнала.........................................................................................................161
Инициализация PostgresTransactionLogger в веб-службе..........................162
Будущие улучшения.........................................................................................163
Итерация 3: реализация безопасности транспортного уровня.........................163
Transport Layer Security........................................................................................164
Сертификаты, центры сертификации и доверие........................................164
Закрытый ключ и файлы сертификатов. ..........................................................165
Формат Privacy Enhanced Mail (PEM).............................................................165
Защита веб-службы с по­мощью HTTPS.............................................................166
В заключение о транспортном уровне..............................................................167
Контейнеризация хранилища пар ключ/значение. ............................................168
Основы Docker.......................................................................................................169
Dockerfile............................................................................................................169
Сборка образа контейнера..............................................................................170
Запуск образа контейнера. .............................................................................171
Проверка запущенного образа контейнера.................................................172
Отправка запроса в опубликованный порт контейнера............................173
Запуск нескольких контейнеров....................................................................174
Остановка и удаление контейнеров..............................................................174
Сборка контейнера для службы хранилища пар ключ/значение. ................175
Итерация 1: добавление двоичного файла в пустой образ........................176
Итерация 2: многоэтапная сборка.................................................................178
Сохранение данных контейнера вовне.............................................................179
Итоги...........................................................................................................................180

Часть III. ОБЛАЧНЫЕ АТРИБУТЫ . .........................................................182
Глава 6. Все дело в надежности..................................................................183
В чем суть облачных вычислений?. .......................................................................184
Все дело в надежности. ............................................................................................184

12  Содержание
Что такое надежность, и почему она так важна?.................................................185
Надежность обеспечивается не только операторами.....................................187
Достижение надежности..........................................................................................188
Предотвращение неисправностей.....................................................................190
Рекомендуемые практики программирования...........................................190
Особенности языка..........................................................................................190
Масштабируемость. .........................................................................................191
Слабая связанность..........................................................................................191
Отказоустойчивость.............................................................................................192
Устранение неисправностей...............................................................................192
Проверка и тестирование. ..............................................................................193
Управляемость..................................................................................................194
Прогнозирование неисправностей....................................................................194
Непреходящая актуальность методологии «Двенадцать факторов»................194
I. Кодовая база.......................................................................................................195
II. Зависимости. ....................................................................................................196
III. Конфигурация.................................................................................................196
IV. Сторонние службы..........................................................................................198
V. Сборка, выпуск, выполнение..........................................................................199
VI. Процессы..........................................................................................................200
VII. Изоляция данных. .........................................................................................200
VIII. Масштабируемость......................................................................................201
IX. Живучесть........................................................................................................202
X. Сходство окружений разработки/эксплуатации.........................................202
XI. Журналирование.............................................................................................203
XII. Задачи администрирования........................................................................204
Итоги...........................................................................................................................205

Глава 7. Масштабируемость. .........................................................................206
Что такое масштабируемость?................................................................................207
Различные формы масштабирования...............................................................208
Четыре основных узких места................................................................................209
С состоянием и без состояния. ...............................................................................211
Состояние приложения и состояние ресурса...................................................211
Преимущества отсутствия состояния................................................................212
Отложенное масштабирование: эффективность.................................................213
Эффективное кеширование с использованием кеша LRU.............................213
Эффективная синхронизация. ...........................................................................217
Разделяйте память, общаясь..........................................................................217
Уменьшение простоев на блокировках с помощью
буферизованных каналов. ..............................................................................219
Уменьшение простоев на блокировках с помощью сегментирования....221
Утечки памяти могут вызвать... фатальную ошибку исчерпания
памяти во время выполнения............................................................................222
Утечки сопрограмм..........................................................................................222
Вечно тикающие таймеры..............................................................................223
В заключение об эффективности.......................................................................225

Содержание  13

Архитектуры служб...................................................................................................225
Архитектура монолитной системы. ..................................................................226
Архитектура системы микросервисов. .............................................................227
Бессерверные архитектуры.................................................................................229
Достоинства и недостатки бессерверных вычислений..............................229
Бессерверные службы......................................................................................231
Итоги...........................................................................................................................233

Глава 8. Слабая связанность. ........................................................................234
Тесная связанность...................................................................................................235
Множество форм тесной связанности...............................................................236
Хрупкие протоколы обмена............................................................................236
Общие зависимости.........................................................................................237
Общий момент времени. ................................................................................237
Фиксированные адреса...................................................................................238
Взаимодействия между службами. ........................................................................238
Шаблон обмена сообщениями запрос/ответ........................................................239
Распространенные реализации шаблона запрос/ответ. ................................240
Отправка HTTP-запросов с использованием net/http....................................240
Вызов удаленных процедур с использованием gRPC.....................................244
Определение интерфейса с использованием протокола буферов. ..........245
Установка компилятора протокола буферов................................................246
Определение структуры сообщения. ............................................................247
Структура сообщений для взаимодействий с хранилищем пар
ключ/значение..................................................................................................248
Определение методов службы. ......................................................................249
Компиляция протокола буферов. ..................................................................250
Реализация службы gRPC................................................................................251
Реализация клиента gRPC...............................................................................253
Слабое связывание локальных ресурсов с помощью плагинов. .......................255
Подключение плагинов с по­мощью пакета plugin..........................................255
Словарь плагинов.............................................................................................256
Пример плагина. ..............................................................................................257
Интерфейс Sayer...............................................................................................257
Код плагина.......................................................................................................258
Сборка плагинов...............................................................................................258
Использование плагинов Go...........................................................................259
Запуск примера. ...............................................................................................261
Система плагинов HashiCorp для Go, доступных через RPC..........................261
Еще один пример плагина..............................................................................262
Общий код.........................................................................................................263
Реализация плагина.........................................................................................265
Процесс-потребитель. .....................................................................................266
Гексагональная архитектура...................................................................................269
Архитектура. .........................................................................................................269
Реализация гексагональной службы..................................................................270
Реорганизация компонентов. ........................................................................271

14  Содержание
Наш первый разъем.........................................................................................272
Основное приложение.....................................................................................272
Адаптеры TransactionLogger...........................................................................273
Порт FrontEnd. ..................................................................................................274
Все вместе..........................................................................................................276
Итоги...........................................................................................................................277

Глава 9. Устойчивость.......................................................................................279
Почему устойчивость важна. ..................................................................................280
Что подразумевается под сбоем системы?...........................................................281
Обеспечение устойчивости.................................................................................282
Каскадные сбои.........................................................................................................282
Предотвращение перегрузки..............................................................................284
Дросселирование..............................................................................................284
Сброс нагрузки. ................................................................................................288
Постепенное ухудшение качества обслуживания.......................................289
Повтори еще раз: повторные запросы..................................................................289
Алгоритмы увеличения задержки. ....................................................................291
Размыкание цепи.................................................................................................294
Тайм-ауты..............................................................................................................295
Использование контекста Context для реализации тайм-аутов
на стороне службы. ..........................................................................................296
Прерывание ожидания обработки клиентских запросов HTTP/REST. ....298
Прерывание ожидания обработки клиентских запросов gRPC................299
Идемпотентность.................................................................................................301
Как сделать службу идемпотентной?............................................................302
А как насчет скалярных операций?...............................................................303
Избыточность служб.................................................................................................304
Проектирование избыточности.........................................................................305
Автоматическое масштабирование...................................................................307
Проверка работоспособности.................................................................................308
Что подразумевается под «работоспособностью» экземпляра?...................309
Три типа проверок работоспособности. ...........................................................309
Проверка жизнеспособности..........................................................................310
Поверхностная проверка работоспособности.............................................310
Глубокая проверка работоспособности. .......................................................312
Открытие при отказе...........................................................................................313
Итоги...........................................................................................................................314

Глава 10. Управляемость................................................................................315
Что такое управляемость, и почему она важна?..................................................316
Настройка приложения............................................................................................317
Рекомендуемые приемы организации конфигураций...................................318
Настройка с использованием переменных окружения..................................319
Настройка с использованием аргументов командной строки......................320
Стандартный пакет flag...................................................................................320

Содержание  15

Парсер командной строки Cobra....................................................................322
Настройка с использованием файлов. ..............................................................326
Наша структура конфигурационных данных. .............................................326
Формат JSON. ....................................................................................................327
Формат YAML....................................................................................................332
Наблюдение за изменениями в конфигурационных файлах....................335
Viper: швейцарский армейский нож конфигурационных пакетов..............340
Явно устанавливаемые значения в Viper. ....................................................341
Работа с флагами командной строки в Viper...............................................341
Работа с переменными окружения в Viper...................................................342
Работа с конфигурационными файлами в Viper.........................................342
Использование удаленных хранилищ пар ключ/значение в Viper. .........344
Значения по умолчанию в Viper. ...................................................................345
Управление функциональными возможностями с по­мощью флагов..............345
Разработка флага для управления функциональной возможностью...........346
Итерация 0: начальная реализация...................................................................347
Итерация 1: жестко запрограммированный флаг...........................................347
Итерация 2: настраиваемый флаг......................................................................348
Итерация 3: динамический флаг. ......................................................................349
Динамические флаги как функции. ..............................................................350
Реализация функции динамического флага................................................350
Поиск функции флага......................................................................................351
Функция маршрутизации...............................................................................352
Итоги...........................................................................................................................353

Глава 11. Наблюдаемость..............................................................................354
Что такое наблюдаемость?......................................................................................355
Зачем нужна наблюдаемость?............................................................................355
Чем наблюдаемость отличается от «традиционного» мониторинга?..........356
«Три столпа наблюдаемости»..................................................................................357
OpenTelemetry. ..........................................................................................................358
Компоненты OpenTelemetry. ..............................................................................359
Трассировка...............................................................................................................360
Концепции трассировки......................................................................................361
Трассировка с использованием OpenTelemetry...............................................362
Создание экспортеров трассировки..............................................................364
Создание провайдера трассировки...............................................................366
Настройка глобального провайдера трассировки.......................................367
Получение экземпляра трассировщика .......................................................367
Начальная и конечная операции...................................................................367
Установка метаданных операции..................................................................369
Автоматическое инструментирование.........................................................370
Собираем все вместе: трассировка....................................................................373
API-службы вычисления чисел Фибоначчи. ................................................374
Функция-обработчик службы вычисления чисел Фибоначчи..................375
Функция main службы. ....................................................................................376
Запуск служб. ....................................................................................................377

16  Содержание
Вывод консольного экспортера......................................................................377
Просмотр результатов в Jaeger.......................................................................378
Метрики. ....................................................................................................................379
Два способа передачи метрик: принудительная и по запросу. ....................381
Принудительная отправка метрик................................................................382
Передача метрик по запросу..........................................................................382
Какой подход лучше?.......................................................................................383
Метрики в OpenTelemetry. ..................................................................................384
Создание экспортеров метрик.......................................................................385
Установка глобального провайдера метрик.................................................386
Экспортирование конечной точки метрик..................................................386
Получение экземпляра Meter.........................................................................388
Инструменты метрик. .....................................................................................388
Собираем все вместе: метрики. .........................................................................394
Запуск служб. ....................................................................................................394
Вывод конечной точки метрик. .....................................................................395
Просмотр результатов в Prometheus.............................................................396
Журналирование.......................................................................................................397
Рекомендуемые методы журналирования.......................................................397
Интерпретируйте журналы как потоки событий........................................398
Структурируйте события для последующего анализа................................398
Лучше меньше, да лучше.................................................................................400
Динамически фильтруйте журналируемые данные...................................400
Журналирование с использованием стандартного пакета log......................401
Специальные функции журналирования.....................................................402
Журналирование в нестандартный объект записи.....................................402
Флаги журналирования...................................................................................403
Пакет журналирования Zap. ...............................................................................403
Создание регистратора Zap............................................................................405
Журналирование с использованием Zap......................................................405
Динамическая фильтрация журналируемых данных в Zap. .....................407
Итоги...........................................................................................................................409

Предметный указатель. ..................................................................................410

От издательства
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы ду­маете
об этой книге – что понравилось или, может быть, не понравилось. Отзывы
важны для нас, чтобы выпускать книги, которые будут для вас максимально
полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на
страницу книги и оставив комментарий в разделе «Отзывы и рецензии».
Также можно послать письмо главному редактору по адресу dmkpress@gmail.
com; при этом укажите название книги в теме письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы в написании новой книги, заполните форму на нашем сайте по адресу http://
dmkpress.com/authors/publish_book/ или напишите в издательство по адресу
dmkpress@gmail.com.

Скачивание исходного кода примеров
Скачать файлы с дополнительной информацией для книг издательства «ДМК
Пресс» можно на сайте www.dmkpress.com на странице с описанием соответствующей книги.

Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить высокое качество наших текстов, ошибки все равно случаются. Если вы найдете
ошибку в одной из наших книг, мы будем очень благодарны, если вы сообщите о ней главному редактору по адресу dmkpress@gmail.com. Сделав это,
вы избавите других читателей от недопонимания и поможете нам улучшить
последующие издания этой книги.

Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой. Издательства «ДМК Пресс» и O’Reilly очень серьезно относятся к вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной
публикацией какой-либо из наших книг, пожалуйста, пришлите нам ссылку на
интернет-ресурс, чтобы мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу элект­
ронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря
которой мы можем предоставлять вам качественные материалы.

Об авторе
Мэтью А. Титмус – ветеран индустрии разработки программного обеспечения. Научившись создавать виртуальные миры в LPC, он получил удивительно востребованное образование в области молекулярной биологии,
создал инструменты анализа терабайтных наборов данных для лаборатории
физики высоких энергий, с нуля написал фреймворк для разработки вебприложений, применил методы распределенных вычислений для анализа
ракового генома, а также в числе первых разрабатывал методы машинного
обучения на связанных данных.
Он был одним из первых сторонников облачных технологий в целом и языка Go в частности. Последние четыре года специализируется на переносе монолитных приложений в контейнерный облачный мир, помогая компаниям
осваивать новые способы разработки, развертывания и управления своими
службами. Он увлечен решением задач повышения качества промышленных
систем и потратил много времени на обдумывание и реализацию стратегий
наблюдения за распределенными системами и управления ими.
Мэтью живет на Лонг-Айленде с самой терпеливой женщиной в мире, на
которой ему посчастливилось жениться, и самым очаровательным мальчиком в мире, от которого ему посчастливилось услышать «папа».

Об иллюстрации
на обложке
На обложке «Облачный Go» изображено животное из семейства туко-туко
(Ctenomyidae). Эти неотропические грызуны обитают в южной части Южной
Америки.
Название «туко-туко» относится к широкому кругу видов. Эти грызуны
имеют плотное тело с мощными короткими лапами и хорошо развитыми
когтями. У них большая голова, но маленькие уши, и хотя до 90 % времени
они проводят под землей, они имеют относительно большие глаза, по сравнению с другими норными грызунами. Цвет и текстура шерсти туко-туко
варьируются в зависимости от вида, но в целом шерсть довольно густая.
Хвост короткий и почти без шерсти.
Туко-туко роют системы туннелей, часто весьма обширные и сложные,
в песчаной и/или суглинистой почве. В этих системах туннелей имеются отдельные камеры для гнездования и хранения пищи. В ходе эволюции тукотуко претерпели различные морфологические изменения, которые помогают им прекрасно чувствовать себя под землей, и развили хорошее обоняние,
помогающее им ориентироваться в туннелях. При рытье нор используют как
когти, так и рыло.
Рацион туко-туко состоит в основном из корней, стеблей и трав. В настоящее время считаются сельскохозяйственными вредителями, но во времена
до появления европейцев в Южной Америке они были важным источником
пищи для коренных народов, особенно на Огненной Земле. Современный
охран­ный статус туко-туко зависит от вида и географического региона. Одним видам присвоена категория «вызывающий наименьшие опасения», тогда как другие считаются «находящимися под угрозой исчезновения». Многие
животные, изображенные на обложках книг издательства O’Reilly, находятся
под угрозой вымирания; все они очень важны для биосферы.
Иллюстрацию для обложки нарисовал Карен Монтгомери (Karen Montgomery) на основе черно-белой гравюры из энциклопедии «English Cyclopedia Natural History». Текст на обложке набран шрифтами Gilroy Semibold
и Guardian Sans. Текст книги набран шрифтом Adobe Minion Pro; текст заголовков – шрифтом Adobe Myriad Condensed; а фрагменты программного
кода – шрифтом Ubuntu Mono, созданным Далтоном Маагом (Dalton Maag).

Предисловие
Это волшебное время для инженеров
У нас есть Docker для создания контейнеров и Kubernetes для управления
ими. Prometheus помогает нам следить за ними. Consul позволяет обнаруживать их. Jaeger дает возможность организовать взаимодействия между ними.
Это лишь несколько примеров, в действительности круг возможностей гораздо шире, и все эти возможности поддерживают новое поколение технологий:
все они «облачные», и все они написаны на Go.
Термин «облачный» кажется двусмысленным и отдает рекламной шумихой, но на самом деле он имеет довольно конкретное определение. Согласно
Cloud Native Computing Foundation, подразделению известного фонда Linux
Foundation, облачное приложение – это приложение, способное масштабироваться синхронно с изменением нагрузки, устойчивое к неопределенности
окружения и управляемое в условиях нестабильности и постоянно меняющихся требований. Иначе говоря, облачные приложения создаются для
работы в жесткой и неопределенной вселенной.
На основе опыта, накопленного за годы разработки облачного программного обеспечения, около десяти лет назад был создан Go – первый язык программирования, спроектированный специально для разработки облачных
приложений. Во многом его появление было обусловлено тем, что типичные
серверные языки, использовавшиеся в то время, просто не подходили для
создания распределенных приложений, которые производит Google.
С тех пор Go занял лидирующие позиции в облачной разработке и используется повсюду: от Docker до Harbour, от Kubernetes до Consul, от InfluxDB
до CockroachDB. Десять из пятнадцати сертифицированных проектов Cloud
Native Computing Foundation и 42 из 621 его проектов в целом написаны в основном или полностью на Go. И с каждым днем их становится все больше.

Кому адресована эта книга
Эта книга адресована опытным разработчикам, особенно инженерам вебприложений и инженерам по надежности. Многие из них уже использовали
Go для создания веб-сервисов, но не знали некоторых тонкостей разработки
в облачных окружениях или не имели четкого представления о том,что такое «облачные приложения», и впоследствии обнаруживали, что их сервисы
сложно развертывать, ими сложно управлять или наблюдать за ними. Таким
1

Включая проекты CNCF из категорий Sandbox, Incubating и Graduated, по состоянию на февраль 2021 года.

Предисловие  21

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

Почему я написал эту книгу
Способы проектирования, конструирования и развертывания приложений
меняются со временем. Требования к масштабированию вынуждают разработчиков размещать свои сервисы на десятках и сотнях серверов: отрасль
постепенно становится «облачной». Но при этом возникает множество новых проблем: как разрабатывать, развертывать или управлять сервисом,
действующим на десятках, сотнях или даже тысячах серверов? К сожалению,
существующие книги об облачных вычислениях сосредоточены на абстрактных принципах проектирования и содержат лишь элементарные примеры,
если вообще содержат. Эта книга призвана удовлетворить потребность в демонстрации практической реализации сложных принципов проектирования
облачных вычислений.

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

22  Предисловие
Так выделяются советы и предложения.
Так обозначаются примечания общего характера.
Так обозначаются предупреждения и предостережения.

Использование программного кода примеров
Вспомогательные материалы (примеры кода, упражнения и т. д.) доступны
для загрузки по адресу: https://github.com/cloud-native-go/examples.
Данная книга призвана оказать вам помощь в решении ваших задач. В общем случае все примеры кода из этой книги вы можете использовать в своих
программах и в документации. Вам не нужно обращаться в издательство за
разрешением, если вы не собираетесь воспроизводить существенные части
программного кода. Например, если вы разрабатываете программу и используете в ней несколько отрывков программного кода из книги, вам не
нужно обращаться за разрешением. Однако в случае продажи или распространения примеров из этой книги вам необходимо получить разрешение от
издательства O’Reilly. Если вы отвечаете на вопросы, цитируя данную книгу
или примеры из нее, получение разрешения не требуется. Но при включении
существенных объемов программного кода примеров из этой книги в вашу
документацию вам необходимо будет получить разрешение издательства.
Мы приветствуем, но не требуем добавлять ссылку на первоисточник при
цитировании. Под ссылкой на первоисточник мы подразумеваем указание
авторов, издательства и ISBN. Например: «Мэтью А. Титмус. Облачный Go.
М.: ДМК Пресс, 2021. 978-5-97060-965-1» или «Cloud Native Go by Matthew A.
Titmus (O’Reilly). Copyright 2021 Matthew A. Titmus, 978-1-492-07633-9».
За получением разрешения на использование значительных объемов программного кода примеров из этой книги обращайтесь по адресу permissions@
oreilly.com.

Благодарности
В первую очередь я хочу поблагодарить жену и сына. Вы – главная причина
всех моих успехов, достигнутых после того, как вы вошли в мою жизнь. Вы –
мои путеводные звезды, которые помогают мне не сбиться с пути и заставляют смотреть в небо.
Моему отцу, которого мы недавно потеряли. Ты был ближе всех к людям
эпохи Возрождения и при этом умудрялся быть самым добрым и скромным
человеком, которого я когда-либо знал. Я все еще хочу быть похожим на тебя,
когда вырасту.
Мэри. Ты чувствуешь его отсутствие острее других. Мы – одна семья, и мы
всегда будем семьей, даже если я буду звонить тебе не так часто, как следовало бы. Папа гордился бы твоей силой и грацией.

Предисловие  23

Саре. Меня всегда поражала твоя сила духа и стойкость. Твой острый ум
сделал тебя моим самым верным союзником и самым жестким противником
с тех самых пор, как только научились говорить. Не говори Натану, но ты моя
любимая сестра.
Натану. Если каждый из нас унаследовал что-то одно от отца, то ты,
безуслов­но, получил его сердце. Я нечасто говорю это, но я очень горжусь
тобою и твоими достижениями. Не говори Саре, но ты мой любимый брат.
Маме. Ты сильная, умная, яркая и необычная. Спасибо, что научила меня
всегда делать то, что действительно нужно делать, независимо от того, что
думают люди. Оставайся необычной и не забывай кормить цыплят.
Альберту. У тебя огромное сердце и бездонное терпение. Спасибо, что присоединился к нашей семье; мы все выиграли от этого.
Всем другим членам нашей семьи. Я не могу видеться с вами так часто, как
хотелось бы, и я очень скучаю по вам, но я всегда ощущаю вас рядом, когда
вы мне нужны. Спасибо, что праздновали со мной победы и поддерживали
меня в поражениях.
Уолту и Альваро, с которыми я не смогу расстаться, даже поменяв работу.
Спасибо за вашу восторженную поддержку в моих начинаниях и за ваш абсолютный реализм. Вы оба делаете меня лучше. Кроме того, спасибо за то,
что познакомили меня с серией книг «Gradle» Уилла Уайта (Will Wight) и за
последовавшую за этим пагубную зависимость.
Моим друзьям «Jeff Classic», «New Jeff», Алексу (Alex), Маркану (Markan),
Приянке (Priyanka), Сэму (Sam), Оуэну (Owen), Мэтту М., Мариусу (Matt M.,
Marius), Питеру (Peter), Рохиту (Rohit) и коллегам из Flatiron Health. Спасибо,
что позволили мне отвлечься на эту книгу, и за поддержку, что выступили
в качестве советчиков, первых читателей рукописи и критиков, а также за
то, что воодушевили меня и были моими помощниками.
Всем моим друзьям из CoffeeOps в Нью-Йорке и во всем мире. Вы любезно
позволили мне отразить ваши мысли и бросить вам вызов, а вы приняли этот
вызов. Эта книга определенно выиграла от вашего участия.
Лиз Фон-Джонс (Liz Fong-Jones), известному эксперту в области наблюдений и оракулу. Ваши указания, замечания и образцы кода были неоценимы,
и без вашей щедрости написать эту книгу было бы намного труднее, а результат был бы намного хуже.
Моим техническим обозревателям Ли Атчисону (Lee Atchison), Альваро
Атьензе (Alvaro Atienza), Дэвиду Никпонски (David Nicponski), Натали Пис­
тунович (Natalie Pistunovich) и Джеймсу Куигли (James Quigley). Спасибо за
терпение, которое вы проявили, чтобы прочитать каждое написанное мной
слово (даже сноски). Эта книга получилась намного лучше благодаря вашей
зоркости и упорному труду.
Наконец, спасибо всей команде редакторов и художников O’Reilly Media,
с которыми мне посчастливилось работать, особенно Амелии Блевинс (Amelia Blevins), Дэнни Эльфанбаум (Danny Elfanbaum) и Зану Маккуэйду (Zan
McQuade). 2020 год был сложным, но ваши доброта, терпение и поддержка
помогли мне пройти через него.

Часть

I
ОБЛАЧНОЕ
ОКРУЖЕНИЕ

Глава

1

Что такое
«облачное» приложение?
Самая опасная фраза в этом языке звучит так: «Мы всегда так поступали»1.
– Грейс Хоппер (Grace Hopper), Computerworld (январь 1976)

Если вы читаете эту книгу, значит, вы, по крайней мере, слышали раньше
термин облачный. Вероятно, вы даже читали некоторые из множества статей, написанных восторженными авторами, соблазнившимися хрустом купюр. Если ваше знание термина ограничивается только этим опытом, то вас
можно простить за то, что сочли его неоднозначным, модным и просто еще
одним из ряда рекламных выражений, которые могли начинаться как что-то
полезное, но затем были использованы людьми, пытающимися что-то вам
продать, как, например, «гибкая разработка» или «DevOps».
По схожим причинам поиск в интернете по запросу «определение термина
облачный» может привести вас к мысли, что любое приложение, предназначенное для работы в облаке, должно быть написано на «правильном» языке2
или с использованием «правильного» фреймворка или «правильной» технологии. Конечно, выбор языка может значительно упростить или усложнить
вашу жизнь, но этот выбор не является ни необходимым, ни достаточным
для создания облачного приложения.
Облачность зависит лишь от того, где запускается приложение. Термин
облачный явно предполагает это. Все, что от вас требуется, – это «залить»
свое старое, сляпанное кое-как приложение в контейнер и запустить его под
управлением Kubernetes, после чего оно автоматически станет облачным,
верно? Нет, потому что в этом случае вы лишь усложнили развертывание
и управление приложением3.
Итак, что такое облачное приложение? В этой главе мы ответим на этот
вопрос. Для начала мы познакомимся с историей парадигм вычислительных
служб до (и особенно) настоящего времени и обсудим, как неумолимое тре1

2
3

Surden, Esther. «Privacy Laws May Usher in Defensive DP: Hopper». Computerworld, 26
Jan. 1976, p. 9.
Это Go. Не поймите меня неправильно, но, в конце концов, эта книга о Go.
Вы когда-нибудь задумывались, почему так много миграций в Kubernetes терпят
неудачу?

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

История развития до настоящего времени
История сетевых приложений – это история все усиливающегося требования
масштабируемости.
В конце 1950-х появилась большая ЭВМ – мейнфрейм. В то время каждая
программа и каждый фрагмент данных хранились в одной гигантской машине, к которой пользователи могли обращаться с по­мощью простых терминалов, не имевших собственных вычислительных возможностей. Вся логика
и все данные жили вместе как одна большая счастливая семья. Это было
простое время.
Все изменилось в 1980-х с появлением недорогих персональных компьютеров, подключаемых к сети. В отличие от простых терминалов, персональные
компьютеры (ПК) могли выполнять некоторые вычисления самостоятельно,
что позволяло переносить на них часть логики приложения. Эта новая многоуровневая архитектура, разделяющая логику представления, бизнес-логику
и данные (рис. 1.1), сделала возможным изменение или замену компонентов
сетевого приложения независимо друг от друга.
Уровень представления

Уровень бизнес-логики

Клиент
Интернет

Веб-сервер + сервер
приложений

Уровень управления
данными

Сервер
баз данных

Клиент

Рис. 1.1  Традиционная трехуровневая архитектура,
в которой четко разделялись представление, бизнес-логика и данные

В 1990-х популяризация Всемирной паутины и последовавшая за ней «золотая лихорадка доткомов» породили технологию «программное обеспечение как услуга» (Software as a Service, SaaS). Целые отрасли были основаны на
модели SaaS, что привело к созданию более сложных и ресурсоемких приложений, которые, в свою очередь, было труднее разрабатывать, поддерживать
и развертывать. Внезапно классической многоуровневой архитектуры стало
недостаточно. В результате бизнес-логика начала дробиться на подкомпоненты, которые можно было разрабатывать, поддерживать и развертывать
независимо, и наступила эра микросервисов.

История развития до настоящего времени  27

В 2006 году Amazon запустила облачную платформу Amazon Web Services
(AWS), включавшую службу Elastic Compute Cloud (EC2). AWS была не первым
предложением инфраструктуры как услуги (Infrastructure as a Service, IaaS),
но именно она произвела революцию в отношении доступности хранилищ
данных и вычислительных ресурсов, сделав облачные вычисления – и возможность быстрого масштабирования – доступными для масс, что ускорило
массовую миграцию ресурсов в «облако».
К сожалению, организации вскоре поняли, что масштабирование – непростая задача. Проблемы неизбежны, и когда вы работаете с сотнями или
тысячами ресурсов, проблемы возникают очень часто. Трафик может резко
увеличиваться или уменьшаться, основное оборудование может выходить из
строя, вышестоящие зависимости могут внезапно и необъяснимо стать недоступными. Но даже притом, что что-то может пойти не так, вам все равно
придется развернуть все эти ресурсы и управлять ими. При таких масштабах
люди не могут (или, по крайней мере, могут с большими затруднениями)
справляться со всеми этими проблемами вручную.

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

Вышестоящая

Нижестоящая
Зависит от

В этом сценарии служба A посылает запросы службе B (и, следовательно, зависит от
нее), которая, в свою очередь, зависит от службы C.
Поскольку служба B зависит от службы C, можно сказать, что служба C является нижестоящей зависимостью службы B. В более широком смысле, поскольку служба A
зависит от службы B, которая зависит от службы C, служба C также является транзитивной1 нижестоящей зависимостью службы A.
И наоборот, поскольку служба C используется службой B, можно сказать, что служба
B является вышестоящей зависимостью службы C, а служба A – транзитивной вышестоящей зависимостью службы C.

1

То есть непрямой, косвенной. – Прим. перев.

28  Что такое «облачное» приложение?

Что значит быть «облачным»?
Вообще говоря, по-настоящему облачное приложение включает в себя все,
что мы узнали о масштабировании сетевых приложений за последние 60 лет.
Они масштабируются в условиях резко меняющейся нагрузки, устойчивы
в условиях неопределенности окружения и управляемы в условиях постоянно меняющихся требований. Иначе говоря, облачное приложение создано
для жизни в жесткой и неопределенной вселенной.
Но как определить термин облачный? К счастью для всех нас1, в этом нет
необходимости. Cloud Native Computing Foundation (https://oreil.ly/621yd) –
подразделение известного фонда Linux Foundation и в некотором роде признанный авторитет в этой области – уже сделал это за нас:
Облачные технологии позволяют организациям создавать и запускать масштабируемые приложения в современных динамических окружениях, таких как
общедоступные, частные и гибридные облака…
Они делают слабосвязанные системы устойчивыми, управляемыми и наблюдаемыми. В сочетании с надежной автоматизацией они позволяют инженерам
часто и предсказуемо вносить важные изменения с минимальными усилиями2.
– Cloud Native Computing Foundation,

Согласно этому определению, облачные приложения – больше, чем просто
приложения, которые действуют в облаке. Они также масштабируемы, слабо
связаны, устойчивы, управляемы и доступны для наблюдения. Можно сказать,
что наличие этих «облачных атрибутов» позволяет называть системы облачными.
Как оказалось, каждый из этих атрибутов имеет довольно специфическое
значение, поэтому рассмотрим их поближе.

Масштабируемость
В контексте облачных вычислений масштабируемость можно определить
как способность показывать ожидаемое поведение в условиях значительных
колебаний спроса вверх и вниз. Систему можно считать масштабируемой,
если ее не нужно реорганизовывать для решения намеченной задачи во время или после резкого увеличения спроса.
Немасштабируемые службы могут отлично функционировать в начальных условиях, поэтому масштабируемость не всегда является первоочередной задачей при проектировании. Однако службы, не способные масштабироваться за пределы первоначальных ожиданий, имеют ограниченный срок
жизни. Более того, реорганизовать службу для поддержки масштабируемости зачастую чрезвычайно сложно, поэтому проектирование с прицелом на
1
2

И особенно для меня, пишущего эту классную книгу.
Cloud Native Computing Foundation. «CNCF Cloud Native Definition v1.0», GitHub,
7 Dec. 2020. https://oreil.ly/KJuTr.

Что значит быть «облачным»  29

такую поддержку может сэкономить время и деньги в долгосрочной перспективе.
Существует два способа масштабирования, каждый из которых имеет свои
плюсы и минусы.
Вертикальное масштабирование
Под вертикальным масштабированием подразумевается увеличение (или
уменьшение) аппаратных ресурсов, доступных системе. Например, можно
выделить дополнительную память или процессоры базе данных, которая
работает на выделенном экземпляре. Преимущество вертикального масштабирования – в технической простоте реализации, но ресурсы любого
конкретного экземпляра невозможно наращивать до бесконечности.
Горизонтальное масштабирование
Под горизонтальным масштабированием подразумевается увеличение
(или уменьшение) количества действующих экземпляров службы. Например, можно увеличить количество узлов за балансировщиком нагрузки
или контейнеров в Kubernetes либо в другой системе управления контейнерами. Эта стратегия имеет свои преимущества, включая избыточность
и свободу от ограничений размеров экземпляров. Однако чем больше экземпляров, тем сложнее проектирование и управление, а кроме того, не
все службы можно масштабировать по горизонтали.
Итак, у нас есть два способа масштабирования – по вертикали и по горизонтали. Но если служба поддерживает вертикальное масштабирование
аппаратных ресурсов (и способна извлечь выгоду из этого), то можно ли назвать ее «масштабируемой»? И насколько она масштабируема? Вертикальное
масштабирование по своей природе ограничено объемом доступных вычислительных ресурсов, поэтому служба, которую можно масштабировать
только по вертикали, вообще не очень масштабируема. Если может понадобиться масштабирование в десять, сто или тысячу раз, то служба должна
поддерживать горизонтальное масштабирование.
Так в чем же разница между службами, поддерживающими и не поддерживающими горизонтальное масштабирование? Все сводится к одному: состоя­
ние. Службу, которая не поддерживает состояния или была спроектирована
для распределения своего состояния между экземплярами, будет довольно
легко масштабировать. Для любого другого приложения это будет сложно.
Более подробно идеи масштабируемости, состояния и избыточности будут
рассмотрены в главе 7.

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

30  Что такое «облачное» приложение?
Например, веб-серверы и веб-браузеры можно считать слабо связанными:
серверы можно обновлять и даже полностью заменять, не опасаясь влияния
на наши браузеры. Это возможно потому, что стандартные веб-серверы обмениваются данными с использованием набора стандартных протоколов1.
Иначе говоря, они предоставляют контракт на обслуживание. Представьте,
какой был бы хаос, если бы все веб-браузеры в мире приходилось обновлять
после выхода новой версии NGINX или httpd2!
Можно сказать, что «слабая связанность» – это один из базовых принципов архитектуры микросервисов: разделение компонентов таким образом,
чтобы изменения в одном не влияли на другой. Однако этим принципом
часто пренебрегают, и его стоит повторить. Преимущества слабой связанности – и последствия пренебрежения ею – нельзя недооценивать. Очень
легко создать систему, «худшую из возможных», которая сочетает в себе накладные расходы на управление и сложность, связанные с наличием нескольких служб, с зависимостями и связями монолитной системы: жуткий
распределенный монолит.
К сожалению, не существует волшебной технологии или протокола, которые могли бы предотвратить тесную связанность ваших служб. Любой
формат обмена данными может использоваться неправильно. Однако есть
несколько приемов, помогающих добиться желаемого и – в сочетании с такими практиками, как декларативные API и методы управления версиями, –
создавать слабо связанные службы, которые могут изменяться независимо
друг от друга.
Эти приемы и практики будут подробно обсуждаться и демонстрироваться
в главе 8.

Устойчивость
Устойчивость (близкий синоним к отказоустойчивости) – это мера способности системы восстанавливаться после ошибок и сбоев. Систему можно
считать устойчивой, если она может продолжать работать правильно – возможно, с меньшей эффективностью – вместо полного отказа после выхода
из строя какой-либо ее части.
При обсуждении устойчивости (а также других «облачных атрибутов», но
особенно при обсуждении устойчивости) мы довольно часто используем
слово «система». Термин система, в зависимости от контекста, может относиться к чему угодно, от сложной сети взаимосвязанных служб (например,
целого распределенного приложения) до набора тесно связанных компонентов (таких как реплики одной функции или экземпляра службы), и даже
к единст­венному процессу, выполняющемуся на единственной машине. Всякая система состоит из нескольких подсистем, которые, в свою очередь, состоят из более мелких подсистем, которые сами состоят из еще более мелких
подсистем. И так до самого конца.
1
2

Те, кто помнит браузерные войны 1990-х годов, знают, что так было не всегда.
Или если бы для каждого веб-сайта нужно было использовать другой браузер. Это
было бы крайне неудобно, не так ли?

Что значит быть «облачным»  31

Выражаясь языком системотехники, любая система может содержать
дефекты, или нарушения, которые мы, программисты, любовно называем
жучками (bugs). Мы все слишком хорошо знаем, что при определенных условиях любая неисправность может привести к ошибке – так мы называем
любое несоответствие между предполагаемым и фактическим поведением
системы. Ошибки могут привести к тому, что система не сможет выполнить
свою функцию, то есть откажет. Но это еще не все: отказ в подсистеме или
компоненте приводит к нарушению работы более крупной системы; любое
нарушение, не устраненное должным образом, может вызывать нарушения
во все более вышестоящих подсистемах и, в конце концов, привести к полному отказу системы.
В идеальном мире каждая система должна тщательно проектироваться,
чтобы предотвратить появление нарушений, но в реальной жизни это невозможно. Невозможно предотвратить все возможные нарушения – пытаться сделать это бессмысленно и непродуктивно. Однако если предположить,
что все компоненты системы могут выходить из строя – а это так и есть, –
и спроектировать их так, чтобы они правильно реагировали на возможные
нарушения и ограничивали распространение их последствий, то можно создать систему, которая продолжает исправно функционировать, даже если
некоторые из ее компонентов выйдут из строя.
Существует множество подходов к проектированию отказоустойчивых
систем. Наиболее распространенным из них является, пожалуй, развертывание избыточных компонентов, но при этом также предполагается, что нарушение не повлияет на другие компоненты того же типа. Можно добавить
автоматическое размыкание цепи и логику повтора, чтобы предотвратить
распространение нарушений между компонентами. Неисправные компоненты можно даже вывести из строя намеренно, чтобы принести пользу
всей системе.
Мы обсудим все эти подходы (и многие другие) в главе 9.

Устойчивость – это не безотказность
Термины устойчивость и безотказность описывают похожие понятия, которые
час­то путают. Но, как мы обсудим в главе 9, это не совсем одно и то же1:
• устойчивость системы – это степень, в которой она может продолжать правильно
функционировать, столкнувшись с ошибками и неполадками. Устойчивость, наряду с другими четырьмя облачными свойствами, является лишь одним из факторов, влияющих на надежность;
• безотказность системы – это ее способность сохранять ожидаемое поведение
в течение заданного интервала времени. Безотказность в сочетании с такими
атрибутами, как доступность и ремонтопригодность, способствует общей надежности системы.

1

Если вам интересно узнать академическую трактовку, то я настоятельно рекомендую книгу Кишора С. Триведи (Kishor S. Trivedi) и Андреа Боббио (Andrea Bobbio)
«Reliability and Availability Engineering» (https://oreil.ly/80wGT).

32  Что такое «облачное» приложение?

Управляемость
Управляемость системы – это простота (или ее отсутствие), с которой можно
изменить поведение системы для обеспечения безопасности, бесперебойной работы и соответствия меняющимся требованиям. Систему можно считать управляемой, если она позволяет изменить ее поведение без изменения
кода.
Управляемость как свойство системы привлекает гораздо меньше внимания, чем другие атрибуты, такие как масштабируемость или наблюдаемость.
Однако она играет не менее важную роль, особенно в сложных распределенных системах.
Например, представьте гипотетическую систему, которая включает службу и базу данных, и служба обращается к базе данных по URL. Что, если вам
потребуется изменить эту службу так, чтобы она обращалась к другой базе
данных? Если URL жестко запрограммирован, то вам может понадобиться изменить код и повторно развернуть систему, что иногда может быть неудобно
по некоторым причинам. Конечно, можно обновить запись в системе DNS,
чтобы она возвращала другой IP-адрес для заданного URL, но как быть, если
вам потребуется повторно развернуть разрабатываемую версию службы,
которая будет ссылаться на базу данных в среде разработки?
Управляемая система может, например, извлекать требуемый URL из легко
изменяемой переменной окружения; если служба, которая ее использует,
развернута в Kubernetes, то для корректировки ее поведения достаточно
будет обновить значение в конфигурации. Более сложная система может
даже предоставлять декларативный API, с по­мощью которого разработчик
сможет сообщить системе, какого поведения он ожидает. Нет единственного
правильного ответа1.
Управляемость не ограничивается поддержкой изменений в конфигурации. Она охватывает все возможные аспекты поведения системы, будь то
активация функций или ротация учетных данных и сертификатов TLS, или
даже (и, возможно, особенно) развертывание либо обновление компонентов
системы.
Управляемые системы предполагают адаптируемость и могут легко приспосабливаться к изменяющимся функциональным требованиям, а также
к требованиям окружения или безопасности. С другой стороны, неуправляемые системы, как правило, более хрупкие, часто требуют специальных
изменений, нередко выполняемых вручную. Накладные расходы, связанные
с управлением такими системами, вводят фундаментальные ограничения на
их масштабируемость, доступность и надежность.
Идея управляемости и некоторые предпочтительные практики ее реализации в Go будут обсуждаться в главе 10.

1

И есть много неправильных.

Что значит быть «облачным»  33

Управляемость – это не удобство сопровождения
Можно сказать, что управляемость и удобство сопровождения (ремонтопригодность) в чем-то «перекликаются», потому что оба понятия связаны с простотой изменения системы1, но на самом деле они совершенно разные:
• управляемость описывает простоту изменения поведения работающей системы,
вплоть до развертывания (и повторного развертывания) ее компонентов. Управляемость – это простота внесения изменений извне;
• удобство сопровождения описывает простоту изменения базовых функций системы, чаще всего ее кода. Удобство сопровождения – это простота внесения изменений изнутри.

Наблюдаемость
Наблюдаемость системы – это мера простоты определения ее внутреннего
состояния по наблюдаемым результатам. Система считается наблюдаемой,
если можно быстро и последовательно получать ответы на все новые вопросы о ней с минимальными предварительными знаниями, без необходимости
внедряться в существующий код или писать новый.
На первый взгляд в этом нет ничего сложного: достаточно добавить журналирование, включить пару панелей мониторинга (дашбордов) – и система
станет наблюдаемой, не так ли? Почти наверняка нет, особенно в современных сложных системах, где почти любая проблема так или иначе связана
с сетью, в которой одновременно может обнаружиться несколько проблем.
Эпоха стека LAMP закончилась; сейчас дело обстоит намного сложнее.
Это не означает, что метрики, журналы и трассировка стали неважны.
Напротив, они так и остались основными строительными блоками наблюдаемости. Но одного их существования недостаточно: данные – это не информация. Их важно правильно интерпретировать. Они должны иметься
в достаточном количестве. Они должны подсказывать ответы на вопросы,
о которых вы даже не думали раньше.
Способность обнаруживать и отлаживать проблемы является фундаментальным требованием для сопровождения и развития надежной системы.
Но в распределенной системе бывает сложно выяснить причины проблемы.
Сложные системы слишком... сложны. Количество возможных состояний отказа в любой системе пропорционально произведению количества возможных состояний частичного и полного отказа каждого из ее компонентов,
и невозможно предсказать их все заранее. Традиционного подхода, заключающегося в фокусировке внимания на вещах, которые по нашему мнению
могут потерпеть сбой, недостаточно.
Появление новых методов наблюдения можно рассматривать как процесс эволюции мониторинга. Многолетний опыт проектирования, создания
и сопровождения сложных систем научил нас, что традиционные методы
1

К тому же оба термина начинаются на «У». Очень запутанно.

34  Что такое «облачное» приложение?
инструментирования, включая, помимо всего прочего, панели мониторинга,
журналы или оповещения о различных «известных неизвестных», просто не
справляются с проблемами, создаваемыми современными распределенными системами.
Наблюдаемость – сложная и тонкая тема, но, по сути, она сводится к следующему: подготовить систему к реальным условиям настолько хорошо,
чтобы в будущем можно было отвечать на вопросы, о которых вы пока не
задумывались.
Идея наблюдаемости и некоторые предложения по ее реализации будут
обсуждаться в главе 11.

Что особенного в облачном окружении?
Миграция в облако является примером архитектурной и технической адаптации, движимой давлением окружающей среды. Это эволюция – выживание
сильнейшего. Имейте в виду, что по образованию я – биолог.
В стародавние времена, когда все только начиналось1, приложения создавались и развертывались (обычно вручную) на одном или нескольких
серверах, где обслуживались и поддерживались со всем тщанием. Если они
заболевали, то их заботливо лечили. Если служба выходила из строя, то ее
исправляли простым перезапуском. Наблюдаемость заключалась во входе
в командную оболочку сервера и просмотре журналов. В те времена все было
проще.
В 1997 году только 11 % людей в промышленно развитых странах и 2 %
во всем мире пользовались интернетом. Однако в последующие годы наблюдался экспоненциальный рост подключений к интернету, и к 2017 году
это число выросло до 81 % в промышленно развитых странах и 48 % во всем
мире2 и продолжает расти.
Все эти пользователи – и их деньги – создавали давление на службы, требуя
масштабирования. Более того, по мере роста сложности требований пользователей и их зависимости от веб-сервисов росли и ожидания, что их любимые
веб-приложения будут многофункциональными и всегда доступными.
Результатом стало значительное эволюционное давление в сторону масштабирования, сложности и надежности. Однако эти три атрибута плохо сочетаются друг с другом, и традиционные подходы просто не могли и не могут
угнаться за ними. Пришлось изобретать новые приемы и методы.
К счастью, с появлением общедоступных облаков и IaaS возможность масштабирования инфраструктуры существенно упростилась. Недостатки надежности часто можно было компенсировать количеством. Но это создало
новые проблемы. Как обслуживать сотни, тысячи или даже десятки тысяч
1
2

Это было в 1990-х.
International Telecommunication Union (ITU). «Internet users per 100 inhabitants 1997
to 2007» и «Internet users per 100 inhabitants 2005 to 2017». ICT Data and Statistics
(IDS).

Итоги  35

серверов? Как установить на них свое приложение или обновлять его? Как
отлаживать возникающие в нем проблемы? Как вообще узнать – здорово ли
оно? Проблемы в небольшом масштабе, вызывающие лишь легкое раздражение, становятся очень сложными с увеличением масштаба.
Облачные технологии важны, потому что масштабирование является причиной (и решением) всех наших проблем. Это не волшебство. Здесь нет ничего особенного. Если отбросить причудливую терминологию, то облачные
методы и технологии существуют только для того, чтобы дать возможность
использовать преимущества «облака» (количество) и компенсировать его
недостатки (отсутствие надежности).

Итоги
В этой главе мы познакомились с историей компьютерных вычислений
и узна­ли, что то, что теперь мы называем «облачным окружением», не является чем-то новым – это неизбежный результат цикла технологического
развития, стимулирующего инновации.
Однако все эти причудливые термины, с которыми мы познакомились,
сводятся к одному: современные приложения должны надежно служить
большому количеству людей. Методы и технологии, которые мы называем
«облачными», – это лучшие современные практики создания масштабируемых, адаптируемых и достаточно устойчивых служб.
Но какое отношение все это имеет к языку Go? Как оказывается, для облачного окружения нужны свои облачные инструменты. В главе 2 мы начнем
говорить о том, что это означает.

Глава

2
Почему Go правит
облачным миром

Каждый смышленый дурак сможет сделать любой предмет больше, сложнее
и мощнее. Развитие в другом направлении требует присутствия гениальности и огромного мужества.
– Э. Ф. Шумахер (E. F. Schumacher), Small Is Beautiful1 (август 1973 г.)

Как появился Go
Идея создания языка Go возникла в Google в сентябре 2007 года, и это был
неизбежный результат того, что нескольких умных парней заперли в одной
комнате и чертовски расстроили их.
Речь идет о Роберте Гриземере (Robert Griesemer), Робе Пайке (Rob Pike)
и Кене Томпсоне (Ken Thompson); все они уже имели богатый опыт разработки других языков. Источником их расстройства стал тот факт, что ни один
из языков программирования, доступных в то время, просто не подходил
для описания видов распределенных, масштабируемых, устойчивых служб,
создававшихся в Google2.
По сути, все основные языки программирования того времени были разработаны в другую эпоху, еще до того, как многопроцессорные системы стали
обычным явлением, а сети – повсеместными. Поддержка многопроцессорной обработки и сетей – основных строительных блоков современных «облачных» служб3 – была ограничена или требовала чрезвычайных усилий для
использования. Проще говоря, языки программирования не соответствовали
потребностям разработки современного программного обеспечения.
1

2
3

Шумахер Э. Ф. Малое прекрасно. Экономика, в которой люди имеют значение. М.:
Изд. дом Высшей школы экономики, 2012. ISBN 978-5-7598-0822-0. – Прим. перев.
Это были «облачные» службы, созданные еще до появления термина «облачные».
Конечно, в то время они не назывались «облачными»; для Google они были просто
«службами».

Особенности облачного мира  37

Особенности облачного мира
Разочаровывающих факторов было много, но все они сводились к чрезмерной сложности языков, с которыми они работали, затруднявшей создание
серверного программного обеспечения, в том числе1:
Замысловатость программ
Код было трудно читать. Излишние проверки и повторяющиеся фрагменты усугублялись функционально перекрывающимися особенностями, которые помогали оттачивать ум, но не придавали ясности.
Медленная сборка
Конструкция языка и многие годы постепенного развития привели к тому,
что сборка приложений могла длиться часами, даже на больших кластерах.
Неэффективность
Многие программисты отреагировали на вышеупомянутые проблемы,
взяв на вооружение более гибкие и динамичные языки, фактически поменяв эффективность и безопасность типов на выразительность.
Высокая стоимость обновлений
Несовместимость даже между младшими версиями языка, а также любые
зависимости, которые он может иметь (и транзитивные зависимости!),
часто вызывали большие сложности при обновлении.
За прошедшие годы было предложено множество – часто довольно остроумных – решений для преодоления некоторых из этих проблем, обычно вносящих дополнительные сложности в процесс. Ясно, что их нельзя исправить
с по­мощью нового API или особенности языка. Поэтому проектировщики
Go представили новый современный язык, созданный специально для облачных вычислений, который поддерживает сетевые и многопроцессорные
вычисления и обладает большой выразительностью, но при этом понятный
и позволяющий пользователям сосредоточиться на решении своих задач,
а не на борьбе с языком.
В результате получился язык Go, отличающийся явными и неявными особенностями. Некоторые из этих особенностей (явные и неявные) и их мотивация обсуждаются в следующих разделах.

Композиция и структурная типизация
Объектно-ориентированное программирование, основанное на понятии
«объектов» различных «типов», обладающих различными атрибутами, существует с 1960-х годов, но по-настоящему вошло в моду в начале-середине
1990-х с появлением Java и добавлением объектно-ориентированных воз1

Pike, Rob. «Go at Google: Language Design in the Service of Software Engineering».
Google, Inc., 2012. https://oreil.ly/6V9T1.

38  Почему Go правит облачным миром
можностей в C++. С тех пор этот стиль превратился в доминирующую парадигму программирования и остается таковой по сей день.
Перспективы объектно-ориентированного программирования соблазнительны, а теория, лежащая в основе, даже имеет определенный интуитивный
смысл. Данные и поведение могут быть связаны с типами вещей и наследоваться подтипами этих вещей. Экземпляры этих типов можно представить
как материальные объекты со свойствами и поведением – компоненты более
крупной системы, моделирующей конкретные понятия реального мира.
Однако на практике объектно-ориентированное программирование
с наследованием часто требует тщательной проработки отношений между
типами, а также точного соблюдения определенных шаблонов и практик
проектирования. Таким образом, как показано на рис. 2.1, в объектно-ориентированном программировании наблюдается тенденция к смещению фокуса
от разработки алгоритмов к разработке и поддержке классификации и объектных моделей.

Рис. 2.1  Со временем объектно-ориентированное программирование
стало все больше тяготеть к классификации

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

Особенности облачного мира  39

В частности, там, где наследование крутится вокруг расширения отношения «является» между классами (например, автомобиль «является» транспортным средством с мотором), композиция позволяет создавать типы с использованием отношений «имеет», определяющих, что тот или иной тип
может делать (например, автомобиль «имеет» мотор). На практике это обес­
печивает большую гибкость проектирования, позволяя создавать код, менее
подверженный проблемам из-за причуд «членов семьи».
В более широком смысле: в Go есть интерфейсы для описания поведенческих контрактов, но в нем нет понятия «является», поэтому эквивалентность экземпляров определяется путем изучения определения типа, а не его
происхождения. Например, пусть есть интерфейс Shape, который определяет
метод Area, тогда любой тип с методом Area будет неявно удовлетворять интерфейсу Shape и вам не потребуется явно объявлять его как Shape:
type Shape interface {
Area() float64
}

// Любой экземпляр Shape
// должен иметь метод Area

type Rectangle struct {
width, height float64
}

// Rectangle не заявляет явно
// о поддержке Shape

func (Rectangle r) Area() float64 { // Rectangle имеет метод Area
return r.width * r.height
// и удовлетворяет интерфейсу Shape
}

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

Понятность
Такие языки, как C++ и Java, часто критикуют за неуклюжесть, неудобство
и излишнюю многословность. Они требуют большого количества шаблонного кода и тщательной проверки результатов, обременяя проекты избыточным кодом, который мешает программистам, отвлекая их внимание от
решаемой задачи и ограничивая масштабируемость проектов под тяжестью
итоговой сложности.
Go проектировался с прицелом на разработку больших проектов большим
количеством программистов. Его минималистский дизайн (всего 25 ключевых
слов и 1 вид циклов) и бескомпромиссный компилятор решительно отдают
1

В языках, использующих утиную типизацию, тип объекта менее важен, чем методы, которые он определяет. То есть «если он ходит как утка и крякает как утка,
значит, это утка».

40  Почему Go правит облачным миром
предпочтение ясности перед остроумием1. Это, в свою очередь, способствует
простоте кода и продуктивности программистов. Получившийся код легко
читать, проверять иподдерживать, и в нем гораздо сложнее допустить ошибку.

Модель взаимодействия последовательных
процессов
Большинство основных языков поддерживают возможность одновременного запуска нескольких конкурирующих процессов, позволяя составлять
программы из процессов, выполняемых независимо. При правильном использовании конкуренция может быть невероятно полезным инструментом,
но она также создает ряд проблем, особенно это касается упорядочивания
событий, взаимодействий между процессами и координации доступа к общим ресурсам.
Программист неизбежно сталкивается с этими проблемами, позволяя процессам совместно использовать некоторую область памяти и затем с по­
мощью блокировок и мьютексов упорядочивать доступ к ней, чтобы в каждый момент времени работать с этой областью мог только один процесс.
Но даже при правильной реализации эта стратегия может привести к значительным накладным расходам на выполнение проверок. Также легко забыть заблокировать или разблокировать общую память, что может привести
к появлению состояния гонки, взаимоблокировки или одновременному изменению одного и того же значения разными процессами. Этот класс ошибок
чертовски труден для отладки.
Go, напротив, отдает предпочтение другой стратегии, основанной на формальном языке под названием «модель взаимодействия последовательных
процессов» (Communicating Sequential Processes, CSP), впервые описанном
Тони Хоаром (Tony Hoare) в статье с тем же названием2, где иллюстрируются
шаблоны взаимодействий в конкурентных системах с использованием каналов для передачи сообщений.
Получившаяся модель конкуренции, реализованная в Go в виде языковых
примитивов, таких как сопрограммы и каналы, делает Go уникальным3 и способным элегантно структурировать конкурентные вычисления вообще без
использования блокировок. Это побуждает разработчиков ограничивать совместное использование памяти и разрешать процессам взаимодействовать
друг с другом исключительно путем передачи сообщений. Эту идею часто
выражают в виде афоризма:
Не общайтесь, разделяя память. Разделяйте память, общаясь.
– Афоризм Go
1

2

3

Cheney, Dave. «Clear Is Better than Clever». The Acme of Foolishness, 19 July 2019. https://
oreil.ly/vJs0X.
Hoare, C. A. R. «Communicating Sequential Processes». Communications of the ACM,
vol. 21, no. 8, Aug. 1978, pp. 666–77. https://oreil.ly/CHiLt.
По крайней мере, среди «основных» языков, что бы это ни значило.

Особенности облачного мира  41

Конкуренция – это не параллелизм
Конкуренцию и параллелизм часто путают, что вполне понятно, учитывая, что оба
понятия описывают выполнение нескольких процессов в течение некоторого пе­
риода времени. Однако это определенно не одно и то же1:
• параллелизм описывает одновременное выполнение нескольких независимых
процессов;
• конкуренция описывает комплекс независимо выполняющихся процессов, но ничего не говорит о том, когда эти процессы будут выполняться.

Быстрая сборка
Одним из основных мотивов для создания языка Go было безумно долгое
время сборки программ на некоторых языках того времени2, на которую даже
в больших кластерах Google часто уходило от нескольких минут до нескольких часов. Это отнимает время у разработчика и снижает его продуктивность.
Учитывая, что основная цель Go заключалась в повышении продуктивности,
а не в снижении, долгие сборки должны были уйти в прошлое.
Обсуждение особенностей компилятора Go выходит за рамки этой книги
(и моей компетенции). Тем не менее вкратце отмечу, что язык Go проектировался с прицелом предоставить модель сборки программного обес­
печения, свободную от сложных взаимосвязей, что значительно упрощает
анализ зависимостей и устраняет необходимость в подключаемых файлах
и библиотеках в стиле языка C, а также устраняет накладные расходы, связанные с ними. В результате сборка подавляющего большинства программного обеспечения на Go завершается за секунды, редко за минуты, даже на
относительно скромном оборудовании. Например, для компиляции всех
1,8 миллиона строк3 реализации Go в Kubernetes v1.20.2 на MacBook Pro
с 8-ядерным процессором Intel i9 2,4 ГГц и 32 Гбайт ОЗУ потребовалось всего
около 45 секунд:
mtitmus:~/workspace/kubernetes[MASTER]$ time make
real
user
sys

0m45.309s
1m39.609s
0m43.559s

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

2
3

Gerrand, Andrew. «Concurrency Is Not Parallelism». The Go Blog, 16 Jan. 2016. https://
oreil.ly/WXf4g.
C++. Я говорю о C++.
Не считая комментариев; Openhub.net. «Kubernetes». Open Hub, Black Duck Software,
Inc., 18 Jan. 2021. https://oreil.ly/y5Rty.

42  Почему Go правит облачным миром

Стабильность языка
Go 1 был выпущен в марте 2012-го, включая спецификацию языка и спецификацию набора основных библиотек. Естественным следствием этого явилось
явное обещание команды разработчиков Go пользователям Go, что программы, написанные на Go 1, будут продолжать компилироваться и работать правильно, без изменений, в течение всего срока действия спецификации Go 1. То
есть можно было ожидать, что программы на Go, работающие сегодня, будут
продолжать работать и в будущих выпусках Go 1 (Go 1.1, Go 1.2 и т. д.)1.
Это резко контрастирует со многими другими языками, в которые иногда
добавляются новые возможности, усложняющие сам язык и все, что на нем
написано, и приводит к тому, что некогда элегантный язык превращается
в обширную среду, богатую возможностями, которую зачастую чрезвычайно
трудно освоить2.
Команда разработчиков Go считает этот исключительный уровень стабильности языка жизненно важной особенностью; она позволяет пользователям доверять Go и полагаться на него, использовать имеющиеся и создавать новые библиотеки с минимальными усилиями и значительно снижает
стоимость обновлений, особенно крупных проектов. Важно отметить, что
высокая стабильность позволяет также сообществу учить Go и использовать
его; писать на языке, а не его.
Это не означает, что Go не будет развиваться: и сам язык, и его библиотеки
безусловно будут расширяться новыми пакетами и возможностями3, и к настоящему времени накопилось уже много предложений по расширению4, но
без нарушения работоспособности существующего кода на Go 1.
При этом вполне возможно5, что Go 2 никогда не появится. Скорее всего,
совместимость с Go 1 будет сохраняться бесконечно; и в том маловероятном
случае, если будет внесено критическое изменение, разработчики Go создадут утилиту преобразования, подобную команде go fix, которая использовалась при переходе на Go 1.

Безопасность памяти
Разработчики Go приложили огромные усилия, чтобы избавиться от различных ошибок и уязвимостей, не говоря уже о необходимости писать шаб­
лонный код, связанных с прямым доступом к памяти. Указатели строго ти1

2

3

4
5

Разработчики Go. «Go 1 and the Future of Go Programs». The Go Documentation. https://
oreil.ly/Mqn0I.
Кто-нибудь помнит Java 1.1? Я помню. Да, у нас не было ни дженериков, ни автоматического преобразования элементарных типов, ни расширенных циклов, но
мы были счастливы. Счастливы, это я вам ответственно заявляю.
Я вхожу в команду, занимающуюся разработкой дженериков. Но Go против параметрического полиморфизма!
Разработчики Go. «Proposing Changes to Go». GitHub, 7 Aug. 2019. https://oreil.ly/folYF.
Pike, Rob. «Sydney Golang Meetup – Rob Pike – Go 2 Draft Specifications» (видео).
YouTube, 13 Nov. 2018. https://oreil.ly/YmMAd.

Особенности облачного мира  43

пизированы и всегда инициализируются некоторым значением (даже если
это значение nil), а арифметика указателей явно запрещена. Встроенные
ссылочные типы, такие как ассоциативные массивы и каналы, которые внут­
ренне представлены указателями на изменяемые структуры, инициализируются функцией make. Проще говоря, Go не требует и не допускает ручного
управления памятью и манипуляций, которых поддерживают и требуют
низкоуровневые языки, такие как C и C++, и полученный в результате выигрыш в отношении сложности и безопасности памяти невозможно переоценить.
Тот факт, что Go – это язык со сборкой мусора, избавляет программиста от
необходимости тщательно следить за своевременным освобождением каждого выделенного байта и снимает с его плеч значительное бремя рутины.
Жизнь без malloc дает свободу.
Более того, отказавшись от управления памятью вручную – даже от арифметики указателей, – разработчики Go сделали его практически не восприимчивым к целому классу ошибок, возникающих при работе с памятью
и проблемам безопасности, которые они могут привнести. Никаких утечек
памяти, никаких переполнений буфера, никакой рандомизации адресного
пространства. Ничего.
Конечно, такая простота невозможна без определенных компромиссов,
и, несмотря на невероятную интеллектуальность, сборщик мусора в Go все
же вносит некоторые накладные расходы. По этой причине Go не может
конкурировать с такими языками, как C++ и Rust, по скорости выполнения
в чистом виде. Тем не менее, как будет показано в следующем разделе, Go
занимает далеко не последнее место в этом состязании.

Производительность
Столкнувшись с необходимостью писать шаблонный код и медленной сборкой в статически типизированных компилируемых языках, таких как C++
и Java, многие программисты перешли на более динамичные и гибкие языки,
такие как Python. Эти языки превосходно справляются со многими задачами,
но они очень неэффективны по сравнению с компилирующими языками,
такими как Go, C++ и Java.
Это можно заметить по результатам тестирования в табл. 2.1. Конечно, не
следует слепо доверять тестам, но некоторые результаты особенно порази­
тельны.
На первый взгляд, результаты можно сгруппировать в три категории, соответствующие типам языков:
  компилирующие, строго типизированные языки с ручным управлением памятью (C++, Rust);
  компилирующие, строго типизированные языки со сборкой мусора
(Go, Java);
  интерпретирующие языки с динамической типизацией (Python, Ruby).
Эти результаты показывают, что языки со сборкой мусора, как правило,
немного уступают в производительности языкам с ручным управлением

44  Почему Go правит облачным миром
памятью, но различия не выглядят значительными, за исключением самых
строгих требований.
Таблица 2.1. Тесты быстродействия некоторых распространенных языков
программирования (время в секундах)1
1
Fannkuch-Redux
FASTA
K-Nucleotide
Mandlebrot
N-Body
Spectral norm

C++
8.08
0.78
1.95
0.84
4.09
0.72

8.28
1.20
8.29
3.75
6.38
1.43

Go

Java
11.00
1.20
5.00
4.11
6.75
4.09

NodeJS
11.89
2.02
15.48
4.03
8.36
1.84

Python3
367.49
39.10
46.37
172.58
586.17
118.40

Ruby
1255.50
31.29
72.19
259.25
253.50
113.92

Rust
7.28
0.74
2.76
0.93
3.31
0.71

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

Статическая компоновка
По умолчанию программы на Go компилируются непосредственно в статически скомпонованные выполняемые файлы, куда копируются все необходимые библиотеки и сама среда выполнения. По этой причине файлы
получаются довольно большими (для простейшей программы «привет мир»
создается выполняемый файл с размером порядка 2 Мбайт), зато они не
требуют установки внешней среды выполнения2 или внешних библиотек,
не подвержены конфликтам с имеющимся окружением 3 и могут распространяться между пользователями или развертываться на хосте без риска
повредить или создать конфликты в существующем окружении.
Это особенно удобно при работе с контейнерами. Поскольку двоичные
файлы Go не требуют внешней среды выполнения или даже дистрибутива,
они могут встраиваться в «рабочие» образы, у которых нет родительских
образов. В результате получается очень маленький (единицы мегабайт) образ с минимальным временем развертывания и небольшими накладными
расходами на передачу данных. Это очень полезные черты в такой системе
оркестровки, как Kubernetes, которой может потребоваться регулярно извлекать образы.
1

2
3

Gouy, Isaac. «The Computer Language Benchmarks Game». 18 Jan. 2021. https://oreil.ly/
bQFjc.
Как этого не хватает в Java.
Как этого не хватает в Python.

Особенности облачного мира  45

Статическая типизация
Еще на заре разработки Go его авторам пришлось сделать выбор: будет ли он
статически типизированным, как C++ или Java, и требовать явного определения переменных перед использованием, или динамически типизированным, как Python, что позволит программистам присваивать значения переменным без их определения и, следовательно, писать программы быстрее.
Это было не очень сложное решение; на его принятие ушло совсем немного
времени. Статическая типизация была очевидным выбором, но этот выбор
не был произвольным или основанным на личных предпочтениях1.
Во-первых, корректность типов в статически типизированных языках
можно оценить во время компиляции, что делает их гораздо более производительными (см. табл. 2.1).
Разработчики Go понимали, что время, затрачиваемое на разработку, составляет лишь часть общего жизненного цикла проекта, и любой выигрыш
в скорости программирования в динамически типизированных языках
с лихвой компенсируется возрастающими трудностями при отладке и поддержке такого кода. В конце концов, какой программист на Python не допус­
кал ошибку, пытаясь использовать строку как целое число?
Возьмем, к примеру, следующий фрагмент кода на Python:
my_variable = 0
while my_variable < 10:
my_varaible = my_variable + 1 # Опечатка, порождающая бесконечный цикл!

Заметили? Прочитайте внимательно этот код, если нет. Это может занять
несколько секунд.
Любой программист может допустить такую малозаметную опечатку, которая приводит к созданию совершенно корректного выполняемого кода
на Python. Это всего лишь два тривиальных примера целого класса ошибок,
которые Go будет обнаруживать во время компиляции, а не (не дай бог) после
запуска в производство, и, как правило, по времени ближе к моменту их появления. В конце концов, всем понятно, что чем раньше в цикле разработки
обнаружится ошибка, тем проще (читайте: дешевле) ее исправить.
Наконец, я даже выражу несколько спорное мнение: типизированные языки более читабельны. Python считается особенно удобочитаемым за его снисходительный характер и синтаксис, похожий на синтаксис английского языка2, но что бы вы подумали, увидев следующую сигнатуру функции на Python?
def send(message, recipient):

Что такое message – это строка? И является ли recipient экземпляром какого-либо класса, описанного где-то еще? Да, этот код можно улучшить,
1

2

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

46  Почему Go правит облачным миром
добавив комментарии и разумные значения по умолчанию, но многим из
нас достаточно долго приходилось поддерживать код, чтобы знать, что все
это – далекая звезда, которую можно только желать. Явное определение типов может направлять разработку, облегчать умственную нагрузку за счет
автоматической проверки информации, которую программист в противном
случае должен был бы проверять сам, и служить документацией как для программиста, так и для всех, кому придется поддерживать этот код.

Итоги
В главе 1 основное внимание уделялось атрибутам, отличающим облачные
системы, а в этой главе основное внимание уделяется характеристикам языка, в частности Go, делающим его пригодным для создания облачных служб.
Облачная система должна быть масштабируемой, слабо связанной, устойчивой, управляемой и наблюдаемой, а язык для облачной эпохи должен уметь
делать больше, чем просто создавать системы с этими атрибутами. В конце концов, приложив немного усилий, облачные системы можно создавать
практически на любом языке. Так что же делает язык Go таким особенным?
Можно утверждать, что все особенности, представленные в этой главе, прямо или косвенно влияют на облачные атрибуты, перечисленные в предыдущей главе. Что поддержка конкуренции и безопасность памяти способствуют масштабируемости служб, а структурная типизация обеспечивает слабую
связанность. Go – единственный известный мне распространенный язык,
который сочетает в себе все эти особенности, но настолько ли они новы?
Наиболее заметной из особенностей Go являются, пожалуй, встроенная,
а не «прикрученная» поддержка конкуренции, позволяющая программисту
безопасно и полностью использовать возможности современного сетевого и многопроцессорного оборудования. Сопрограммы и каналы, конечно,
удивительны и значительно упрощают создание устойчивых сетевых служб,
но технически они не уникальны, если вспомнить о некоторых менее распространенных языках, таких как Clojure или Crystal.
Я бы добавил, что главная сила Go состоит в его неуклонном следовании
принципу ясности над умом, которое исходит из понимания, что исходный
код пишется людьми для других людей1. А тот факт, что исходный код компилируется в машинный, почти не так важен.
Go проектировался с учетом возможности совместной работы людей в командах, состав которых иногда меняется, участники которых также могут работать над другими проектами. В этой ситуации критически важны ясность
кода, минимизация «племенных знаний» и возможность быстрого выполнения итераций. Простоту Go часто неправильно понимают и недооценивают,
а между тем он позволяет программистам сосредоточиться на решении задач, а не на борьбе с языком.
В главе 3 мы рассмотрим многие особенности языка Go и познакомимся
с его простотой поближе.
1

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

Часть

II

ОБЛАЧНЫЕ
КОНСТРУКЦИИ В GO

Глава

3
Основы языка Go

Не стоит изучать язык, который не меняет вашего представления о программировании1.
– Алан Перлис (Alan Perlis), ACM SIGPLAN Notices (сентябрь 1982)

Ни одна книга по программированию не может считаться полной без хотя бы
краткого обзора избранного языка, поэтому далее я немного расскажу о Go!
Эта глава будет чуть отличаться от типичных вводных глав, однако я предполагаю, что вы знакомы хотя бы с общими парадигмами программирования,
но, возможно, немного подзабыли тонкости синтаксиса Go. Соответственно,
основное внимание в данной главе будет уделено нюансам и тонкостям Go,
а также его основам. Для более полного знакомства с основами я рекомендую
книгу Introducing Go2 (O’Reilly) Калеба Докси (Caleb Doxsey) или The Go Programming Language3 (Addison-Wesley Professional) Алана А. А. Донована (Alan
A. A. Donovan) и Брайана У. Кернигана (Brian W. Kernighan).
Если вы только начинаете осваивать этот язык, то вам обязательно стоит
прочитать эту главу. Даже если вы достаточно уверенно программируете на
Go, все равно просмотрите ее: здесь вы наверняка подметите пару новых
интересных приемов. Если вы опытный программист на Go, то можете пропустить эту главу (или прочитать ее с ироничной улыбкой и подколоть меня).

Базовые типы данных
Базовые типы данных в Go, фундаментальные строительные блоки, из которых конструируются более сложные типы, можно разделить на три категории:
  логические значения, несущие только один бит информации, – true
или false, – представляющий некоторое логическое заключение или
состояние;
1
2

3

Perlis, Alan. ACM SIGPLAN Notices 17(9), September 1982, pp. 7–13.
Существует перевод на русский язык, выполненный сообществом: http://golangbook.ru. – Прим. перев.
Донован Алан А. А., Керниган Брайан У. Язык программирования Go. М.: Вильямс,
2018. ISBN: 978-5-907114-21-0. – Прим. перев.

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

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

Логические значения
Логический тип, описывающий состояние истинности, существует в той или
иной форме1 во всех языках программирования. В Go он называется bool
и является особым 1-битным целочисленным типом и имеет два возможных
значения:
  true;
  false.
Go поддерживает все обычные логические операции:
and := true && false
fmt.Println(and)

// "false"

or := true || false
fmt.Println(or)

// "true"

not := !true
fmt.Println(not)

// "false"

Интересно отметить, что в Go нет встроенного логического оператора XOR (ИСКЛЮЧАЮЩЕЕ ИЛИ). В нем есть оператор ^, но он выполняет поразрядную операцию XOR.

Простые числа
В Go есть небольшой набор числовых типов с именами, укладывающимися
в определенную систему, которые представляют числа с плавающей запятой
и целые со знаком и без знака:
Целые со знаком
int8, int16, int32, int64
Целые без знака
uint8, uint16, uint32, uint64
С плавающей запятой
float32, float64

1

В ранних версиях C, C++ и Python отсутствовал истинный логический тип, а для
представления логических значений использовались целые числа – 0 представлял
false и 1 – true. Некоторые языки, такие как Perl, Lua и Tcl, по-прежнему используют
эту стратегию.

50  Основы языка Go
Использование определенной системы для именования – это хорошо, но
код пишут люди, которые думают по-разному, поэтому дизайнеры Go преду­
смотрели два дополнительных удобства.
Во-первых, существует два «машинно зависимых» типа с простыми именами – int и uint, – размер которых определяется доступным аппаратным
окружением. Это удобно, если конкретная размерность чисел не играет большой роли. К сожалению, машинно зависимых чисел с плавающей запятой
не существует.
Во-вторых, два целочисленных типа имеют мнемонические псевдонимы:
byte – псевдоним для uint8; и rune – псевдоним для int32.
В большинстве случаев имеет смысл использовать просто int и float64.

Комплексные числа
Go предлагает два типа комплексных чисел, для работы с которыми нужно
немного воображения1: complex64 и complex128. Их можно выразить в виде воображаемого литерала – числа с плавающей запятой, за которым следует i:
var x complex64 = 3.1415i
fmt.Println(x)

// "(0+3.1415i)"

Комплексные числа очень удобны, но редко используются на практике, поэтому я не буду подробно описывать их. Если вам интересно узнать больше,
то полное представление этих чисел, какого они заслуживают, вы найдете
в книге The Go Programming Language Донована и Кернигана.

Строки
Строка – это последовательность кодовых пунктов Юникода. Строки в Go неизменяемы: содержимое строки нельзя изменить после ее создания.
Go поддерживает два стиля строковых литералов: в двойных кавычках
(интерпретируемые литералы) и в обратных апострофах (низкоуровневые
строковые литералы). Например, следующие два строковых литерала эквивалентны:
// Интерпретируемая форма
"Hello\nworld!\n"
// Низкоуровневая форма
`Hello
world!`

В этом интерпретируемом строковом литерале каждая пара символов \n
будет преобразована в один символ перевода строки, а каждая пара символов
\" – в один символ двойной кавычки.
1

Вы меня понимаете?

Переменные  51

В действительности строковый тип является лишь тонкой оберткой вокруг
срезов значений byte в кодировке UTF-8, поэтому любая операция, применимая к срезам и массивам, также может применяться к строкам. Если вы
пока незнакомы со срезами, то можете воспользоваться удобным моментом
и прочитать раздел «Срезы» ниже.

Переменные
Переменные можно объявлять с по­мощью ключевого слова var, чтобы связать
имя с некоторым типизированным значением, и изменять в любой момент.
Типовой синтаксис объявления переменной:
var имя тип = выражение

Однако объявление переменной обладает довольно большой гибкостью:
  с инициализацией: var foo int = 42;
  нескольких переменных: var foo, bar int = 42, 1302;
  с автоматическим определением типа: var foo = 42;
  нескольких переменных разных типов: var b, f, s = true, 2.3, "four";
  без инициализации (см. раздел «Нулевые значения» ниже): var s string.
Go очень строго относится к беспорядку: он его ненавидит. Если вы объявите переменную в функции, но не будете ее использовать, то программа просто не будет компилироваться.

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

С его помощью можно объявить и одну, и сразу несколько переменных:
  с инициализацией: percent := rand.Float64() * 100.0;
  сразу несколько переменных: x, y := 0, 2.
На практике сокращенная форма является наиболее распространенным
способом объявления и инициализации переменных; ключевое слово var
обычно используется либо для объявления локальных переменных, когда
требуется явно указать тип, либо для объявления переменных, которым значения будут присвоены позже.
Запомните, что := – это объявление, а = – присваивание. Попытка повторно использовать оператор := для присваивания нового значения существующей переменной
завершится ошибкой во время компиляции.

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

Нулевые значения
Когда переменная объявляется без явного значения, ей присваивается нулевое значение соответствующего типа:
  целые числа: 0;
  числа с плавающей запятой: 0.0;
  логические значения: false;
  строки: "" (пустая строка).
Для иллюстрации определим четыре переменные разных типов без явной
инициализации:
var
var
var
var

i
f
b
s

int
float64
bool
string

Если теперь обратиться к этим переменным, то можно обнаружить, что
они инициализированы нулевыми значениями:
fmt.Printf("integer: %d\n", i)
fmt.Printf("float: %f\n", f)
fmt.Printf("boolean: %t\n", b)
fmt.Printf("string: %q\n", s)

//
//
//
//

integer: 0
float: 0.000000
boolean: false
string: ""

Обратите внимание, что в этом примере используется функция fmt.Printf,
которая позволяет управлять форматом вывода. Если вы незнакомы с этой
функцией или строками формата в Go, то прочитайте следующую врезку.

Форматирование ввода/вывода в Go
Пакет fmt для Go реализует несколько функций форматированного ввода/вывода.
Чаще других (как мне кажется) используются fmt.Printf и fmt.Scanf, осуществляющие запись в стандартный вывод и чтение из стандартного ввода соответственно:
func Printf(format string, a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
Обратите внимание, что обе имеют параметр format. Это строка формата, то есть
строка, содержащая один или несколько спецификаторов (или глаголов в терминологии Go), определяющих, как следует интерпретировать остальные параметры.
Для функций вывода, таких как fmt.Printf, спецификаторы определяют формат вывода значений аргументов.
У каждой функции также есть параметр a. Оператор ... (произвольное число аргументов) указывает, что функция принимает ноль или более аргументов в этом месте;
interface{} сообщает, что тип параметра не указан. Функции с переменным числом

Переменные  53
аргументов будут рассматриваться в разделе «Функции с переменным числом аргументов», а тип interface{} – в разделе «Интерфейсы».
Вот некоторые спецификаторы, наиболее часто используемые в строках формата:
%v
%T
%%
%t
%b
%d
%f
%s
%q

Значение в формате по умолчанию
Представление типа значения
Сам знак процента; не потребляет ни одного аргумента
Для логических значений: выводит слово true или false
Для целых чисел: выводит значение в двоичном виде
Для целых чисел: выводит значение в десятичном виде
Для чисел с плавающей запятой: выводит в формате с запятой
без экспоненты, например 123.456
Для строк: выводит байты из строки или среза без дополнительной интерпретации
Для строк: интерпретирует как строку в двойных кавычках
(экранированную с использованием синтаксиса Go)

Знакомые с языком C могут распознать эти спецификаторы как несколько упрощенные версии спецификаторов функций printf и scanf. Более полный список можно найти в документации с описанием пакета fmt (https://oreil.ly/Qajzp).

Пустой идентификатор
Пустой идентификатор, представленный оператором _ (подчеркивание),
действует как анонимный заполнитель. Его можно использовать как любой
другой идентификатор в объявлении, с той лишь разницей, что с ним не
будет связано никакое значение.
Чаще всего он используется для выборочного игнорирования ненужных
значений в операциях присваивания, что особенно полезно в языке, поддерживающем возврат нескольких значений и не терпящем неиспользуемых
переменных. Например, вот как можно поступить, чтобы обработать любые
потенциальные ошибки, возвращаемые fmt.Printf, не заботясь о количестве
записанных байтов1:
str := "world"
_, err := fmt.Printf("Hello %s\n", str)
if err != nil {
// обработать ошибку
}

Пустой идентификатор также можно использовать для импортирования
пакета исключительно ради побочного эффекта:
import _ "github.com/lib/pq"

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

А какой интерес может представлять это количество?

54  Основы языка Go

Константы
Константы очень похожи на переменные: ключевое слово const связывает идентификатор с некоторым типизированным значением. Однако константы – это не переменные. Во-первых, и это наиболее очевидно, попытка
изменить константу приведет к ошибке во время компиляции. Во-вторых,
константы должны получать значения при объявлении: они не имеют нулевого значения.
Ключевые слова var и const можно использовать как на уровне пакета, так
и на уровне функции:
const language string = "Go"
var favorite bool = true
func main() {
const text = "Does %s rule? %t!"
var output = fmt.Sprintf(text, language, favorite)
fmt.Println(output) // "Does Go rule? true!"
}

Для демонстрации их поведенческого сходства предыдущий пример намеренно смешивает явные определения типов с автоматическим определением
типа констант и переменных.
Наконец, выбор функции fmt.Sprintf здесь не играет особой роли, но если
вам неясно назначение спецификаторов в строке формата, то вернитесь
к врезке «Форматирование ввода/вывода в Go».

Контейнеры: массивы, срезы
и ассоциативные массивы
Go поддерживает стандартные типы контейнеров, в которых можно хранить
коллекции значений:
Массив
Последовательность фиксированной длины элементов конкретного типа.
Срез
Абстрактная обертка для массивов, размеры которых можно изменять во
время выполнения.
Ассоциативный массив
Структура ассоциативных данных, которая позволяет произвольно сопоставлять ключи со значениями (то есть отображать ключи в значения).
Все эти типы контейнеров имеют свойство length, возвращающее количест­
во элементов в контейнере. Также для определения длины любого массива,

Контейнеры: массивы, срезы и ассоциативные массивы  55

среза (включая строки) или ассоциативного массива можно использовать
встроенную функцию len.

Массивы
В Go, как и в большинстве других основных языков, массив представляет
последовательность фиксированной длины из нуля или более элементов
определенного типа.
Массивы объявляются включением длины в объявление. Нулевое значение для массива – это массив указанной длины, содержащий элементы
с нулевыми значениями. Элементы массива доступны по индексам от 0 до
N-1, которые указываются знакомым способом с использованием квадратных скобок:
var a [3]int
fmt.Println(a)
fmt.Println(a[1])

// Массив типа [3]int, заполненный нулевыми значениями
// "[0 0 0]"
// "0"

a[1] = 42
fmt.Println(a)
fmt.Println(a[1])

// Изменить второй элемент
// "[0 42 0]"
// "42"

i := a[1]
fmt.Println(i)

// "42"

Массивы можно инициализировать с по­мощью литералов:
b := [3]int{2, 4, 6}

Также можно доверить компилятору самому подсчитать количество элементов в объявлении массива:
b := [...]int{2, 4, 6}

В обоих случаях будет создан массив b с типом [3]int.
Для определения длины любого представителя контейнерного типа можно
использовать встроенную функцию len:
fmt.Println(len(b))
fmt.Println(b[len(b)-1])

// "3"
// "6"

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

Срезы
Срезы (slices) – это тип данных в Go, который обеспечивает мощную абстракцию для традиционных массивов. Операции со срезами выглядят для программиста очень похожими на операции с массивами. Как и массивы, срезы

56  Основы языка Go
обеспечивают доступ к последовательностям элементов определенного типа
через знакомую форму записи индексов от 0 до N-1 в квадратных скобках.
Однако, в отличие от массивов, которые имеют фиксированную длину, размеры срезов можно изменять во время выполнения.
Как показано на рис. 3.1, в действительности срез является облегченной
структурой данных с тремя компонентами:
  указатель на некоторый элемент массива, лежащего в основе среза,
представляющий первый элемент среза (не обязательно первый элемент массива);
  длина, представляющая количество элементов в срезе;
  емкость, представляющая максимально возможное значение длины.

Рис. 3.1  Два среза, основанных на одном и том же массиве

Если не указано иное, значение емкости равно количеству элементов от
начала среза до конца массива, лежащего в основе. Получить длину и емкость
среза можно с по­мощью встроенных функций len и cap соответственно.

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

Контейнеры: массивы, срезы и ассоциативные массивы  57
n := make([]int, 3)

// Создать срез с 3 элементами типа int

fmt.Println(n)
fmt.Println(len(n))

// "[0 0 0]"
// "3"; len можно применять и к срезам, и к массивам

n[0] = 8
n[1] = 16
n[2] = 32
fmt.Println(n)

// "[8 16 32]"

Как видите, работа со срезами во многом напоминает работу с массивами. Так же как массивы, нулевое значение среза – это срез указанной длины
с нулевыми значениями в элементах, а элементы в срезе доступны по их
индексам точно так же, как в массиве.
Литерал среза объявляется так же, как литерал массива, за исключением
того, что количество элементов не указывается:
m := []int{1}
fmt.Println(m)

// Литерал объявления среза []int
// "[1]"

Длину среза можно увеличить с по­мощью встроенной функции append, которая возвращает срез увеличенной длины с дополнительными элементами
в конце:
m = append(m, 2)
fmt.Println(m)

// Добавит 2 в конец m
// "[1 2]"

Встроенная функция append может принимать произвольное количество
аргументов, помимо среза. Подробнее о функциях с переменным числом
аргументов рассказывается в разделе «Функции с переменным числом аргументов»:
m = append(m, 2)
fmt.Println(m)

// Добавит 2 в m из предыдущего примера
// "[1 2]"

m = append(m, 3, 4)
fmt.Println(m)

// "[1 2 3 4]"

m = append(m, m...)
fmt.Println(m)

// Добавит в конец m элементы из самого себя
// "[1 2 3 4 1 2 3 4]"

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

58  Основы языка Go

Оператор извлечения среза
Массивы и срезы (включая строки) поддерживают оператор извлечения среза,
имеющий синтаксис s[i: j], где значения i и j должны находиться в диапазоне 0 ≤ i ≤ j ≤ cap (s).
Например:
s0 := []int{0, 1, 2, 3, 4, 5, 6} // Литерал среза
fmt.Println(s0)
// "[0 1 2 3 4 5 6]"

В предыдущем примере определяется литерал среза. Он очень похож на
литерал массива, с той лишь разницей, что в литералах среза не указывается
размер.
Если значение i или j опущено, то по умолчанию они принимаются равными 0 и len(s) соответственно:
s1 := s0[:4]
fmt.Println(s1)

// "[0 1 2 3]"

s2 := s0[3:]
fmt.Println(s2)

// "[3 4 5 6]"

Оператор извлечения среза создает новый срез, основанный на том же
массиве, с длиной j - i. Изменения, произведенные в этом срезе, отразятся
на содержимом базового массива и, соответственно, на всех срезах, базирующихся на том же самом массиве:
s0[3] = 42
fmt.Println(s0)
fmt.Println(s1)
fmt.Println(s2)

//
//
//
//

Это изменение отразится на всех 3 срезах
"[0 1 2 42 4 5 6]"
"[0 1 2 42]"
"[42 4 5 6]"

Более наглядно подобный эффект иллюстрирует рис. 3.1.

Строки и срезы
Внутренняя реализация строк в Go немного сложнее, чем вы могли бы ожидать, и опирается на множество тонкостей, таких как различия между байтами, символами и рунами (тип rune), кодировка UTF-8 Юникода и различия
между строками и строковыми литералами.
На данный момент вам достаточно знать, что строки в Go – это просто срезы байтов, доступные только для чтения, которые обычно (но не обязательно) содержат последовательность символов UTF-8, представляющих кодовые
пункты Юникода, называемые рунами. Go даже позволяет преобразовывать
строки в массивы байтов или рун:
s := "foö"
r := []rune(s)
b := []byte(s)

// Юникод: f=0x66 o=0x6F ö=0xC3B6

Преобразовав строку s таким способом, можно раскрыть ее идентичность
как среза байтов или рун. Это легко проиллюстрировать с по­мощью fmt.

Контейнеры: массивы, срезы и ассоциативные массивы  59

Printf и спецификаторами %T (тип) и %v (значение), представленными во
врезке «Форматирование ввода/вывода в Go» выше:
fmt.Printf("%7T %v\n", s, s)
fmt.Printf("%7T %v\n", r, r)
fmt.Printf("%7T %v\n", b, b)

// "string foö"
// "[]int32 [102 111 246]"
// "[]uint8 [102 111 195 182]"

Обратите внимание, что значение строкового литерала – foö – содержит
смесь символов, часть которых может быть представлена одним байтом (f и o
представлены кодами 102 и 111 соответственно), и один символ может быть
представлен только двумя байтами (ö представлен парой кодов 195 182).
Напомню еще раз, что типы byte и rune – это мнемонические псевдонимы для uint8
и int32 соответственно.

Все три вызова fmt.Printf выводят тип и значение указанной переменной.
Как и ожидалось, строковое значение foö выводится буквально. Однако следующие два вызова выглядят интереснее. Срез типа uint8 содержит четыре
байта, представляющих символы в кодировке UTF-8 (два однобайтовых кодовых пункта и один двухбайтовый). Срез типа int32 содержит три значения,
представляющих кодовые пункты отдельных символов.
В Go поддерживаются и другие способы кодирования строк, но у нас не так
много места для их обсуждения. Чтобы узнать больше, загляните в статью
Роба Пайка (Rob Pike) «Strings, Bytes, Runes and Characters in Go» в блоге Go
(https://oreil.ly/mgku7).

Ассоциативные массивы
Ассоциативный массив в Go реализован в виде хеш-таблицы – невероятно полезной ассоциативной структуры данных, позволяющей «отображать» ключи
в значения, то есть формировать пары ключ/значение. Эта структура данных
имеется во всех основных современных языках программирования: если
у вас есть опыт программирования на одном из них, то, вероятно, вы уже
использовали их, например, в форме dict в Python, Hash в Ruby или HashMap
в Java.
Ассоциативные массивы в Go объявляются как map[K]V, где K и V – это типы
ключей и значений соответственно. В качестве типа ключа можно использовать любой тип, поддерживающий сравнение с использованием оператора
==, при этом не требуется, чтобы K и V были одним и тем же типом. Например,
строковые ключи могут отображаться в значения типа float32.
Ассоциативный массив можно инициализировать с по­мощью встроенной
функции make, а ссылаться на значения в нем – с использованием привычного
синтаксиса name[key]. Уже знакомая нам функция len вернет для ассоциативного массива количество пар ключ/значение; удалять пары ключ/значение
можно с по­мощью встроенной функции delete:
freezing := make(map[string]float32) // пустой ассоциативный массив,
// отображающий string в float32

60  Основы языка Go
freezing["celsius"] = 0.0
freezing["fahrenheit"] = 32.0
freezing["kelvin"] = 273.2
fmt.Println(freezing["kelvin"])
fmt.Println(len(freezing))

// "273.2"
// "3"

delete(freezing, "kelvin")
fmt.Println(len(freezing))

// Удалить "kelvin"
// "2"

Инициализировать и заполнять ассоциативные массивы можно также
с применением литералов:
freezing := map[string]float32{
"celsius": 0.0,
"fahrenheit": 32.0,
"kelvin": 273.2,
}

// Запятая в конце обязательна!

Обратите внимание на запятую в конце последней строки. Она обязательна: компилятор откажется компилировать код, если опустить эту запятую.

Проверка наличия в ассоциативном массиве
Попытка обратиться к ключу, отсутствующему в ассоциативном массиве, не
вызовет исключения (в любом случае в языке Go их нет) или возврата какого-либо пустого (null) значения. Вместо этого будет возвращено нулевое
значение для типа значения:
foo := freezing["no-such-key"] // Получить значение отсутствующего ключа
fmt.Println(foo)
// "0" (нулевое значение для float32)

Это очень полезная особенность, сокращающая количество шаблонных
проверок наличия при работе с ассоциативными массивами, но иногда она
может вызывать сложности, когда ассоциативный массив действительно содержит нулевые значения. К счастью, операция обращения к ассоциативному массиву возвращает также второе необязательное логическое значение –
признак присутствия ключа в ассоциативном массиве:
newton, ok := freezing["newton"]
fmt.Println(newton)
fmt.Println(ok)

// Существует ли шкала Ньютона?
// "0"
// "false"

В этом примере переменная newton получит значение 0.0. Но как узнать,
это действительное значение1 или просто в массиве не оказалось искомого
ключа? Благодаря значению false в ok мы можем смело утверждать, что верно
последнее.

1

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

Указатели  61

Указатели
Итак, указатели. Проклятие и погибель студентов во всем мире. Если вы
используете язык с динамической типизацией, то идеяуказателя может показаться вам чуждой. Мы не будем слишком вдаваться в подробности этого
предмета, но тем не менее постараемся охватить его достаточно полно, чтобы у вас сложилось более или менее ясное представление о нем.
Вернемся к первому принципу: «переменная» – это область в памяти, где
хранится какое-то значение. Обычно, когда вы ссылаетесь на переменную
по ее имени (foo = 10) или используете ее в выражении (s[i] = "foo"), то фактически читаете или изменяете ее значение.
Указатель хранит адрес переменной: адрес места в памяти, где хранится
значение. Каждая переменная имеет адрес, и использование указателей позволяет читать или изменять значения переменных косвенным способом
(как показано на рис. 3.2):
Получение адреса переменной
Адрес именованной переменной можно получить с по­мощью оператора &.
Например, выражение p := &a получит адрес переменной a и присвоит его
переменной-указателю p.
Типы указателей
Переменная p, про которую можно сказать, что она «указывает на», имеет
тип *int, где * сообщает, что это тип указателя на значение типа int.
Разыменование указателя
Чтобы получить значение a с по­мощью p, указатель нужно разыменовать,
добавив * перед именем переменной-указателя. Это позволит косвенно
читать или изменять a.
Переменная

Адрес

Память

Обычная переменная

Указывает на

Указатель

Рис. 3.2  Выражение p := &a извлекает адрес переменной a
и присваивает его переменной p

62  Основы языка Go
Теперь соберем все вместе:
var a int = 10
var p *int = &a
fmt.Println(p)
fmt.Println(*p)

// p типа *int указывает на a
// "0x0001"
// "10"

*p = 20
fmt.Println(a)

// косвенно изменит a
// "20"

Как и любые другие переменные, указатели можно объявлять с нулевым
значением nil, если их не требуется инициализировать заранее. Также их
можно сравнивать: если указатели равны, значит, они содержат один и тот
же адрес (то есть указывают на одну и ту же переменную или оба содержат
nil):
var n *int
var x, y int
fmt.Println(n)
// ""
fmt.Println(n == nil) // "true" (n содержит nil)
fmt.Println(x == y)
fmt.Println(&x == &x)
fmt.Println(&x == &y)
fmt.Println(&x == nil)

//
//
//
//

"true" (x и y обе содержат ноль)
"true" (адрес x равен самому себе)
"false" (разные адреса переменных)
"false" (адрес x не равен nil)

Поскольку в этом примере переменная n нигде не инициализируется, она
всегда имеет значение nil, поэтому сравнение ее с nil возвращает true. Переменные x и y хранят значение 0, поэтому сравнение их значений дает true,
но они являются разными переменными, поэтому сравнение их адресов дает
false.

Управляющие структуры
Любой программист, пришедший в Go из другого языка, обнаружит, что набор управляющих структур, имеющихся в Go, в целом знаком и даже привычен (поначалу), особенно тем, кто имеет опыт программирования на языках, близких к языку C. Однако их реализации имеют некоторые довольно
су­щественные отличия, которые первое время могут казаться странными.
Например, инструкции управляющих структур не требуют большого количества скобок. И это хорошо – чем меньше отвлекающих деталей синтаксиса,
тем лучше.
Также существует только один тип циклов. В Go нет циклов while – только
for. Серьезно! Хотя на самом деле это очень круто. Читайте дальше, и вы
поймете, о чем я.

Управляющие структуры  63

Забавный цикл for
Инструкция for – это единственный цикл в Go, но, несмотря на отсутствие
явного цикла while, его можно реализовать с по­мощью for, эффективно унифицируя все способы управления входом в цикл, к которым вы привыкли.
Go не имеет эквивалента do-while.

Универсальная инструкция for
В общем случае инструкция цикла for в Go имеет почти такой же синтаксис,
как и в других языках семейства C, она включает: оператор инициализации,
условие продолжения и оператор, выполняемый в конце каждой итерации;
все операторы традиционно разделяются точкой с запятой. Любые переменные, объявленные в операторе инициализации, будут доступны только
в теле цикла for:
sum := 0
for i := 0; i < 10; i++ {
sum += 1
}
fmt.Println(sum)

// "10"

В этом примере переменная i инициализируется значением 0. В конце
каждой итерации i увеличивается на 1, и пока ее значение остается меньше
10, процесс повторяется.
В отличие от большинства C-подобных языков, инструкция for в Go не требует использования круглых скобок вокруг предложений в заголовке цикла, а фигурные
скобки, ограничивающие тело цикла, являются обязательными.

В отличие от традиционных C-подобных языков, оператор инициализации
и оператор, выполняемый в конце каждой итерации, в языке Go не являются
обязательными. Как показано в следующем примере, это делает цикл for
значительно более гибким:
sum, i := 0, 0
for i < 10 {
sum += i
i++
}

// Эквивалентно: for ; i < 10;

fmt.Println(i, sum)

// "10 45"

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

64  Основы языка Go
Наконец, исключение всех трех операторов из заголовка инструкции for
создает блок, выполняющийся в цикле бесконечно, подобно традиционному
while (true):
fmt.Println("For ever...")
for {
fmt.Println("...and ever")
}

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

Обход в цикле элементов массивов и срезов
В Go имеется полезное ключевое слово range, упрощающее обход различных
коллекций в цикле.
Например, range можно использовать вместе с инструкцией for для перечисления индексов и извлечения значений элементов:
s := []int{2, 4, 8, 16, 32} // Срез значений типа int
for i, v := range s {
// range возвращает каждый индекс/значение
fmt.Println(i, "->", v) // Выведет индекс и соответствующее ему значение
}

В предыдущем примере значения i и v будут обновляться в каждой итерации и содержать индекс и значение каждого следующего элемента в срезе s.
То есть результат будет выглядеть примерно так:
0
1
2
3
4

->
->
->
->
->

2
4
8
16
32

Но что, если вам не нужны оба этих значения? В конце концов, компилятор Go требует использовать объявленные переменные. К счастью, как
и повсюду в Go, ненужные значения можно отбросить, используя «пустой
идентификатор»:
a := []int{0, 2, 4, 6, 8}
sum := 0
for _, v := range a {
sum += v
}
fmt.Println(sum)

// "20"

Как и в предыдущем примере, значение v будет обновляться в каждой
итерации и содержать значение очередного элемента среза a. Однако на этот

Управляющие структуры  65

раз значение индекса игнорируется и отбрасывается и сохраняется только
значение элемента.

Обход в цикле элементов ассоциативных массивов
Ключевое слово range также может использоваться с оператором for для
перебора элементов ассоциативного массива, при этом в каждой итерации
будет возвращаться следующий ключ и значение:
m := map[int]string{
1: "January",
2: "February",
3: "March",
4: "April",
}
for k, v := range m {
fmt.Println(k, "->", v)
}

Обратите внимание, что ассоциативные массивы в Go не гарантируют
какой-то определенный порядок следования ключей, поэтому вывод выглядит неупорядоченным:
3
4
1
2

->
->
->
->

March
April
January
February

Инструкция if
Инструкция if в Go используется точно так же, как в других C-подобных языках, за исключением отсутствия круглых скобок вокруг условного выражения
и обязательности фигурных скобок:
if 7 % 2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
В отличие от большинства C-подобных языков, инструкция if в Go не требует заключать условное выражение в круглые скобки, а фигурные скобки являются обязательными.

Интересно отметить, что Go позволяет вставлять инструкцию инициализации перед выражением проверки условия в операторе if. Например:
if _, err := os.Open("foo.ext"); err != nil {
fmt.Println(err)

66  Основы языка Go
} else {
fmt.Println("All is fine.")
}

Обратите внимание, как инициализируется переменная err перед проверкой, делая этот код примерно похожим на следующий:
_, err := os.Open("foo.go")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("All is fine.")
}

Однако эти две конструкции не являются точно эквивалентными: в первом
примере переменная err видима только внутри инструкции if; во втором
примере она доступна во всей области видимости, окружающей инструкцию.

Инструкция switch
Так же как во многих других языках, в Go есть инструкция switch, помогающая более кратко выразить серию условных выражений if-then-else. Однако
она имеет множество отличий от привычных реализаций, что делает ее значительно более гибкой.
Наиболее очевидное отличие для тех, кто пришел из C-подобных языков,
заключается в отсутствии «проваливания» из одного варианта в другой; такое
проваливание можно явно добавить с по­мощью ключевого слова fallthrough:
i := 0
switch i % 3 {
case 0:
fmt.Println("Zero")
fallthrough
case 1:
fmt.Println("One")
case 2:
fmt.Println("Two")
default:
fmt.Println("Huh?")
}

В этом примере значение i % 3 равно 0, что соответствует первому варианту, и в результате выводится слово Zero. В языке Go варианты не проваливаются друг в друга по умолчанию, и наличие явного оператора fallthrough
означает, что следующий вариант тоже выполнится и выведет One. Наконец,
отсутствие fallthrough в этом случае приводит к завершению инструкции
switch. Таким образом, этот пример выведет:
Zero
One

Обработка ошибок  67

Инструкция switch в Go имеет два интересных свойства. Во-первых, выражения в case не обязательно должны быть целыми числами или даже константами: операторы case будут последовательно оцениваться сверху вниз,
и выполнится первый из них, значение выражения при котором совпадет со
значением выражения при switch. Во-вторых, если выражение при операторе
switch оставить пус­тым, то оно будет интерпретировано как true и совпадет
с первым вариантом case, условие в котором будет оценено как истинное.
Оба этих свойства демонстрируются в следующем примере:
hour := time.Now().Hour()
switch {
case hour >= 5 && hour < 9:
fmt.Println("I'm writing")
case hour >= 9 && hour < 18:
fmt.Println("I'm working")
default:
fmt.Println("I'm sleeping")
}

Здесь при инструкции switch нет выражения, поэтому она интерпретируется как switch true. В результате для выполнения будет выбран первый
вариант case, условие которого также будет оценено как true. В этом случае
значение hour равно 23, поэтому этот код выведет «I’m sleeping»1.
Наконец, так же как в случае с if, условному выражению в switch может
предшествовать выражение инициализации, и в этом случае любые объявленные переменные будут видны только в теле инструкции switch. Например,
предыдущий пример можно переписать так:
switch hour := time.Now().Hour(); { // Пустое выражение эквивалентно true
case hour >= 5 && hour < 9:
fmt.Println("I'm writing")
case hour >= 9 && hour < 18:
fmt.Println("I'm working")
default:
fmt.Println("I'm sleeping")
}

Обратите внимание на точку с запятой в конце: это пустое выражение,
подразумевающее true, то есть это выражение эквивалентно switch hour: =
time.Now().Hour(); true и соответствует первому варианту case с истинным
условием.

Обработка ошибок
Ошибки в Go интерпретируются как еще одно значение, представленное
встроенным типом error. Это упрощает обработку ошибок: идиоматические
1

Понятно, что этот код нужно откалибровать.

68  Основы языка Go
функции Go могут включать в свой список возвращаемых значений значение
с типом error, которое, если не равно nil, указывает на состояние ошибки,
которое можно обработать в основном пути выполнения. Например, функция os.Open возвращает значение ошибки, отличное от nil, когда не может
открыть файл:
file, err := os.Open("somefile.ext")
if err != nil {
log.Fatal(err)
return err
}

Фактическая реализация типа error невероятно проста: это просто универсальный интерфейс, объявляющий единственный метод:
type error interface {
Error() string
}

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

Создание ошибки
Есть два простых способа создания значений ошибок и один сложный.
К прос­тым относятся вызовы функций errors.New и fmt.Errorf; последняя из
которых удобнее, потому что дополнительно поддерживает форматирование
строк:
e1 := errors.New("error 42")
e2 := fmt.Errorf("error %d", 42)

Тот факт, что error – это интерфейс, позволяет при необходимости реализовать собственные типы ошибок. Например, вот типичный шаблон создания
вложенных ошибок:
type NestedError struct {
Message string
Err
error
}
func (e *NestedError) Error() string {
return fmt.Sprintf("%s\n contains: %s", e.Message, e.Err.Error())
}

Дополнительную информацию об ошибках и полезные советы по их обработке в Go вы найдете в статье Эндрю Герранда (Andrew Gerrand) «Error
Handling and Go» в блоге The Go Blog (https://oreil.ly/YQ6if).

Необычные особенности функций: переменное число параметров и замыкания  69

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

Функции
Объявление функции в Go аналогично объявлению функции в большинстве
других языков: у них есть имя, список типизированных параметров, необязательный список типов возвращаемых значений и тело. Однако объявление
функции в Go несколько отличается от объявлений в других C-подобных
языках: объявление начинается со специального ключевого слова func; тип
каждого параметра следует за его именем; а типы возвращаемых значений
помещаются в конец заголовка определения функции и могут быть полностью опущены (в Go нет типа void).
Функция со списком типов возвращаемых значений должна заканчиваться
оператором return, кроме случаев, когда выполнение не может достичь конца
функции из-за наличия бесконечного цикла или когда функция завершается
оператором panic:
func add(x int, y int) int {
return x + y
}
func main() {
sum := add(10, 5)
fmt.Println(sum) // "15"
}

Кроме того, дополнительный синтаксический сахар позволяет записывать
тип для последовательности однотипных параметров или возвращаемых
значений только один раз. Например, следующие определения func foo эквивалентны:
func foo(i int, j int, a string, b string) { /* ... */ }
func foo(i, j int, a, b string)
{ /* ... */ }

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

70  Основы языка Go
func swap(x, y string) (string, string) {
return y, x
}

Чтобы принять несколько значений, возвращаемых функцией, можно использовать множественное присваивание:
a, b := swap("foo", "bar")

В данном примере переменная a получит значение "bar", а переменная
b – значение "foo".

Рекурсия
Go поддерживает рекурсивные вызовы функций, когда функции вызывают
сами себя. При правильном использовании рекурсия может быть очень мощным инструментом, позволяющим решать самые разные задачи. Канонический пример – вычисление факториала положительного целого числа, то
есть произведения всех положительных целых чисел, меньших или равных n:
func factorial(n int) int {
if n < 1 {
return 1
}
return n * factorial(n-1)
}
func main() {
fmt.Println(factorial(11)) // "39916800"
}

Для любого целого n больше единицы factorial будет вызывать саму себя
с параметром n - 1. Такие рекурсивные вызовы могут накапливаться очень
быстро!

Отложенные вычисления
В Go имеется ключевое слово defer, которое можно использовать для планирования выполнения некоторых действий непосредственно перед возвратом
из функции. Обычно эта возможность используется для высвобождения ресурсов некоторым способом.
Например, чтобы отложить вывод текста "cruel world" до конца вызова
функции, можно вставить ключевое слово defer непосредственно перед выводом:
func main() {
defer fmt.Println("cruel world")
fmt.Println("goodbye")
}

Необычные особенности функций: переменное число параметров и замыкания  71

Если вызвать такую функцию, она выведет:
goodbye
cruel world

Чтобы продемонстрировать более сложный пример, создадим пустой файл
и попытаемся записать в него. Функция closeFile предназначена для закрытия файла по окончании работы с ним. Однако если просто вызвать ее
в конце функции main, то в случае ошибки closeFile никогда не будет вызвана,
и файл останется открытым. Поэтому мы используем defer, чтобы гарантировать вызов функции closeFile перед возвратом из функции main:
func main() {
file, err := os.Create("/tmp/foo.txt") // Создать пустой файл
defer closeFile(file)
// Гарантировать вызов closeFile(file)
if err != nil {
return
}
_, err = fmt.Fprintln(file, "Your mother was a hamster")
if err != nil {
return
}
fmt.Println("File written to successfully")
}
func closeFile(f *os.File) {
if err := f.Close(); err != nil {
fmt.Println("Error closing file:", err.Error())
} else {
fmt.Println("File closed successfully")
}
}

Эта функция main выведет:
File written to successfully
File closed successfully

Если в функции используется несколько ключевых слов defer, они помещаются в стек и по завершении вмещающей функции выполняются в обратном
порядке, по принципу «последним пришел – первым ушел». Например:
func main() {
defer fmt.Println("world")
defer fmt.Println("cruel")
defer fmt.Println("goodbye")
}

72  Основы языка Go
Эта функция выведет:
goodbye
cruel
world

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

Указатели как параметры
Широта возможностей указателей становится особенно очевидной в сочетании с функциями. Как правило, параметры передаются в функции по значению: в таких случаях функция получает копию каждого параметра и изменения, внесенные функцией в копии, не влияют на вызывающую сторону.
Однако указатели содержат ссылку на значение, а не само значение, и могут
использоваться вызываемой функцией для косвенного изменения значения,
переданного в функцию, чтобы повлиять на вызывающую функцию.
Следующая функция демонстрирует оба сценария:
func main() {
x := 5
zeroByValue(x)
fmt.Println(x)

// "5"

zeroByReference(&x)
fmt.Println(x)

// "0"

}
func zeroByValue(x int) {
x = 0
}
func zeroByReference(x *int) {
*x = 0
// Разыменование x и запись 0 по указателю
}

Такое поведение не является уникальным для указателей. В действительности некоторые типы данных являются ссылками на ячейки памяти, включая срезы, ассоциативные массивы, функции и каналы. Изменения, внесенные функциями в значения таких ссылочных типов, могут повлиять на
вызывающую программу, при этом вызываемым функциям не требуется
явно разыменовывать их:
func update(m map[string]int) {
m["c"] = 2
}
func main() {
m := map[string]int{ "a" : 0, "b" : 1}
fmt.Println(m)

// "map[a:0 b:1]"

Необычные особенности функций: переменное число параметров и замыкания  73
update(m)
fmt.Println(m)

// "map[a:0 b:1 c:2]"

}

В этом примере в функцию update передается ассоциативный массив m
с длиной 2. Затем update добавляет в него пару {"c": 2}. Поскольку m является
ссылочным типом, он передается в update как ссылка на структуру данных,
а не как копия, поэтому main увидит в m новую пару после возврата из функции update.

Функции с переменным числом аргументов
Функции с переменным числом аргументов могут принимать ноль и более
аргументов. Типичным примером могут служить функции из семейства fmt.
Printf, которые принимают строку формата и произвольное количество дополнительных аргументов.
Вот сигнатура стандартной функции fmt.Printf:
func Printf(format string, a ...interface{}) (n int, err error)

Обратите внимание, что она принимает строку и ноль или более значений
типа interface{}. Если вы незнакомы с синтаксисом interface{}, то не волнуйтесь, мы рассмотрим его в разделе «Интерфейсы» ниже, а пока можете рассматривать interface{} как «нечто произвольное». Однако самое интересное
здесь то, что последний аргумент содержит многоточие (...). Это оператор
переменного числа аргументов – variadic, который сообщает, что функцию
можно вызвать с любым количеством аргументов этого типа. Например,
fmt.Printf можно вызвать со строкой формата и двумя аргументами разных
типов:
const name, age = "Kim", 22
fmt.Printf("%s is %d years old.\n", name, age)

В вызов такой функции список дополнительных аргументов передается
как срез. В следующем примере параметр factor метода product имеет тип []
int и может использоваться в соответствии с типом:
func product(factors ...int) int {
p := 1
for _, n := range factors {
p *= n
}
return p
}
func main() {
fmt.Println(product(2, 2, 2)) // "8"
}

74  Основы языка Go
В этом примере функция main вызывает product с тремя аргументами (хотя
в принципе она может передать любое количество аргументов, сколько потребуется). В функции product они доступны как срез типа []int с элементами
{2, 2, 2}, которые последовательно перемножаются для получения возвращаемого значения 8.

Передача срезов в параметре с переменным числом
значений
Что, если необходимые аргументы уже находятся в некотором срезе и было
бы желательно передать его в функцию с переменным числом аргументов?
Неужели придется разобрать такой срез на отдельные значения? Конечно,
нет!
В этом случае можно добавить оператор переменного числа аргументов
после имени переменной в вызове функции:
m := []int{3, 3, 3}
fmt.Println(product(m...)) // "27"

Здесь у нас есть переменная m с типом []int, которую нужно передать в вызов функции product. Использование оператора ... при вызове product(m...)
делает это возможным.

Анонимные функции и замыкания
В языке Go функции считаются самыми обычными (или, как говорят, первоклассными) значениями, с которыми можно работать так же, как с любыми
другими объектами: они имеют типы, их можно присваивать переменным
и даже передавать и возвращать другим функциям.
Нулевое значение типа функции – nil; вызов функции nil вызовет крах
(панику):
func sum(x, y int) int
{ return x + y }
func product(x, y int) int { return x * y }
func main() {
var f func(int, int) int // Переменная-функция имеет тип
f = sum
fmt.Println(f(3, 5))

// "8"

f = product
fmt.Println(f(3, 5))

// Допустимо: product имеет тот же тип, что и sum
// "15"

}

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

Структуры, методы и интерфейсы  75

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

Возьмем, например, следующую функцию incrementor. Она имеет состояние в виде переменной i и возвращает анонимную функцию, которая увеличивает это значение перед его возвратом. Можно сказать, что возвращаемая
функция замкнута на переменной i, что делает ее истинным (в простейшем
случае) замыканием:
func incrementer() func() int {
i := 0
return func() int { // Возвращает анонимную функцию,
i++
// "замкнутую на" переменной i в родительской функции
return i
}
}

Вызов incrementor создаст свою локальную копию i и вернет новую анонимную функцию, увеличивающую значение этой копии. Последующие вызовы incrementor будут создавать новые копии i. Вот как это работает:
func main() {
increment := incrementer()
fmt.Println(increment())
fmt.Println(increment())
fmt.Println(increment())

// "1"
// "2"
// "3"

newIncrement := incrementer()
fmt.Println(newIncrement())
// "1"
}

Как видите, incrementer возвращает новую функцию, которая сохраняется
в переменной increment; каждый вызов increment увеличивает внутренний
счетчик на единицу. Однако когда incrementer вызывается снова, он создает
и возвращает совершенно новую функцию со своим собственным счетчиком.
Ни одна из этих функций не может повлиять на другую.

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

76  Основы языка Go
Например, в строгом объектно-ориентированном языке может быть объявлен класс Car, расширяющий (наследующий) абстрактный класс Vehicle;
возможно, класс Car добавляет свойства Wheels и Engine. В теории выглядит
неплохо, но подобные отношения между классами быстро могут стать очень
запутанными и сложными.
Композиционный подход в Go, напротив, позволяет «соединять» компоненты без необходимости определять их отношения. Продолжая предыдущий пример, в Go можно объявить структуру Car и добавить в нее различные
части, такие как Wheels и Engine. Более того, методы в Go можно определить
для любого типа данных – не только для структур.

Структуры
Структура в языке Go – это просто некоторый агрегат с несколькими полями, представляющий единую сущность, каждое поле которой является именованным значением произвольного типа. Структуры определяются с по­
мощью следующего синтаксиса: тип Имя struct. Структура никогда не может
иметь значение nil: нулевое значение структуры – это комплекс нулевых
значений всех ее полей:
type Vertex struct {
X, Y float64
}
func main() {
var v Vertex
fmt.Println(v)

// Структура не может иметь значение nil
// "{0 0}"

v = Vertex{}
fmt.Println(v)

// Явное определение пустой структуры
// "{0 0}"

v = Vertex{1.0, 2.0}
fmt.Println(v)

// Определение значений полей по порядку
// "{1 2}"

v = Vertex{Y:2.5}
fmt.Println(v)

// Определение значений отдельных полей по их именам
// "{0 2.5}"

}

Доступ к полям структур производится с использованием стандартной
точечной нотации:
func main() {
v := Vertex{X: 1.0, Y: 3.0}
fmt.Println(v)
// "{1 3}"
v.X *= 1.5
v.Y *= 2.5
fmt.Println(v)
}

// "{1.5 7.5}"

Структуры, методы и интерфейсы  77

Операции со структурами выполняются с по­мощью ссылок, поэтому Go
предоставляет немного синтаксического сахара: к членам структур можно
обратиться по указателю на структуру с использованием точечной нотации;
в таких случаях указатели разыменовываются автоматически:
func main() {
var v *Vertex = &Vertex{1, 3}
fmt.Println(v)
// &{1 3}
v.X, v.Y = v.Y, v.X
fmt.Println(v)

// &{3 1}

}

Здесь v – это указатель на структуру Vertex, и нам требуется поменять
местами значения ее членов X и Y. Если бы требовалось разыменовывать
указатель, то вам пришлось бы написать примерно такой код: (*v).X, (*v).Y
= (*v).Y, (*v).X, который выглядит довольно жутко. Автоматическое разыменование ссылок на структуры позволяет записать то же самое так: v.X, v.Y =
v.Y, v.X, что выглядит намного проще.

Методы
Методы в Go – это функции, прикрепленные к типам, включая и структуры. Синтаксис объявления метода очень похож на синтаксис объявления
функции, за исключением дополнительного аргумента получателя перед
именем функции, указывающего на тип, к которому прикрепляется метод. В вызове метода экземпляр типа будет доступен по имени аргумента
получателя.
Возьмем для примера наш тип Vertex и добавим к нему метод Square, указав
получателя с именем v и типом *Vertex:
func (v *Vertex) Square() { // Присоединить метод к типу *Vertex
v.X *= v.X
v.Y *= v.Y
}
func main() {
vert := &Vertex{3, 4}
fmt.Println(vert)
// "&{3 4}"
vert.Square()
fmt.Println(vert)

// "&{9 16}"

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

Помимо структур можно также создавать свои версии стандартных составных типов – структур, срезов или ассоциативных массивов – и присоединять к ним свои методы. Для примера объявим новый тип MyMap, версию

78  Основы языка Go
стандартного ассоциативного массива map[string]int, и присоединим к нему
метод Length:
type MyMap map[string]int
func (m MyMap) Length() int {
return len(m)
}
func main() {
mm := MyMap{"A":1, "B": 2}
fmt.Println(mm)
fmt.Println(mm["A"])
fmt.Println(mm.Length())

// "map[A:1 B:2]"
// "1"
// "2"

}

Результатом является новый тип MyMap, отображающий строки в целые
числа подобно map[string]int, но имеющий дополнительный метод Length,
который возвращает количество элементов в массиве.

Интерфейсы
Интерфейс в языке Go – это просто набор сигнатур методов. Как и в других
языках, поддерживающих идею интерфейсов, они используются для обобщенного описания поведения других типов без привязки к деталям реализации. То есть интерфейс можно рассматривать как контракт, которого будет
придерживаться тип, открывающий двери для мощных методов абстракции.
Например, можно определить интерфейс Shape, включающий сигнатуру
метода Area. Любой тип, который должен удовлетворять требованиям интерфейса Shape, обязан иметь метод Area, возвращающий значение типа float64:
type Shape interface {
Area() float64
}

Теперь определим две фигуры, Circle и Rectangle, удовлетворяющие интерфейсу Shape, добавив в каждую из них метод Area. Обратите внимание: нам
не нужно явно объявлять, что они реализуют интерфейс: если тип обладает
всеми методами, определяемыми интерфейсом, он будет неявно соответствовать этому интерфейсу. Это позволяет определять типы, соответствующие интерфейсам, которыми вы не владеете или не управляете:
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

Структуры, методы и интерфейсы  79
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}

Поскольку обе структуры, Circle и Rectangle, неявно удовлетворяют интерфейсу Shape, мы можем передавать их в любые функции, принимающие
параметр типа Shape:
func PrintArea(s Shape) {
fmt.Printf("%T's area is %0.2f\n", s, s.Area())
}
func main() {
r := Rectangle{Width:5, Height:10}
PrintArea(r)
// "main.Rectangle's area is 50.00"
c := Circle{Radius:5}
PrintArea(c)

// "main.Circle's area is 78.54"

}

Проверка типа
Проверку типа можно применить к экземпляру интерфейса, чтобы «подтвердить» его принадлежность к конкретному типу. Синтаксис имеет вид: x.(T),
где x – это экземпляр интерфейса, а T – проверяемый тип.
Вот пример для интерфейса Shape и структуры Circle, которые мы использовали выше:
var s Shape
s = Circle{}
// s -- это экземпляр типа Shape
c := s.(Circle)
// Проверить, является ли s экземпляром Circle
fmt.Printf("%T\n", c) // "main.Circle"

Пустой интерфейс
Одна из любопытных конструкций – пустой интерфейс: interface{}. Пустой
интерфейс не определяет никаких методов. Он не несет никакой информации и ничего не говорит о типе1.
Переменная типа interface{} может содержать значение любого типа, что
очень полезно, когда код должен обрабатывать значения любого типа. Отличным примером функции, использующей такую стратегию, может служить
метод fmt.Println.
Однако у этого подхода есть свои недостатки. Работа с пустым интерфейсом требует проверки определенных предположений во время выполнения,
в результате чего код становится более хрупким и менее эффективным.
1

Pike, Rob. «Go Proverbs». YouTube. 1 Dec. 2015. https://oreil.ly/g8Rid.

80  Основы языка Go

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

Встраивание интерфейсов
Популярный пример встраивания интерфейсов можно найти в пакете io.
В частности, в виде широко используемых интерфейсов io.Reader и io.Writer,
которые определены следующим образом:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}

Но что, если вам понадобится интерфейс с обоими методами, io.Reader
и io.Writer? В таком случае можно реализовать третий интерфейс, содержащий методы из обоих интерфейсов, но тогда вам придется согласовать их все.
Это не только добавляет ненужные накладные расходы на обслуживание, но
также является отличным способом случайно добавить ошибки.
Вместо простого копирования сигнатур методов Go позволяет встроить
два существующих интерфейса в третий, который возьмет на себя функции
обоих. Синтаксически это делается путем добавления встроенных интерфейсов в качестве анонимных полей, как демонстрирует стандартный интерфейс
io.ReadWriter:
type ReadWriter interface {
Reader
Writer
}

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

Структуры, методы и интерфейсы  81

Встраивание структур
Поддержка встраивания не ограничивается интерфейсами: структуры тоже
можно встраивать друг в друга.
Примером может служить структура из пакета bufio, эквивалентная примерам io.Reader и io.Writer в предыдущем разделе. В частности, она имеет
два поля – bufio.Reader (которое реализует io.Reader) и bufio.Writer (которое
реализует io.Writer). Кроме того, в bufio определена реализация io.ReadWriter,
которая является простой композицией существующих типов bufio.Reader
и bufio.Writer:
type ReadWriter struct {
*Reader
*Writer
}

Как видите, синтаксис встраивания структур идентичен синтаксису встраи­
вания интерфейсов: существующие типы встраиваются в виде безымянных
полей. В предыдущем случае bufio.ReadWriter встраивает bufio.Reader и bufio.
Writer как типы указателей.
Так же как любые указатели, встроенные указатели на структуры имеют нулевое значение nil и должны инициализироваться ссылками на допустимые структуры перед
использованием.

Продвижение
Итак, почему композиция выгоднее простого добавления полей структуры?
Ответ прост: при встраивании типа его свойства и методы продвигаются
в комбинированный тип, что позволяет вызывать их непосредственно. Например, метод Read интерфейса bufio.Reader доступен непосредственно из
экземпляра bufio.ReadWriter:
var rw *bufio.ReadWriter = GetReadWriter()
var bytes []byte = make([]byte, 1024)
n, err := rw.Read(bytes) {
// некоторые операции
}

Вам не нужно знать или беспокоиться о том, что метод Read в действительности принадлежит встроенному типу *bufio.Reader. Но при этом важно
помнить, что получателем при вызове продвинутого метода все еще является
встроенный тип, поэтому получателем rw.Read будет поле Reader, а не сам
экземпляр ReadWriter.

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

82  Основы языка Go
(несколько надуманном) примере функции UseReader нужно передать *bufio.
Reader, но у вас есть только экземпляр *bufio.ReadWriter:
func UseReader(r *bufio.Reader) {
fmt.Printf("We got a %T\n", r)
}

// "We got a *bufio.Reader"

func main() {
var rw *bufio.ReadWriter = GetReadWriter()
UseReader(rw.Reader)
}

Как видите, здесь используется имя типа поля (Reader), чтобы получить доступ к полю rw.Reader в экземпляре rw типа *bufio.Reader. Этот прием можно
использовать для инициализации:
rw := &bufio.ReadWriter{Reader: &bufio.Reader{}, Writer: &bufio.Writer{}}

Если бы мы создали rw как &bufio.ReadWriter{}, его встроенные поля получили бы значение nil, а этот фрагмент создает *bufio.ReadWriter с полностью
инициализированными полями *bufio.Reader и *bufio.Writer. Этот прием
редко используется на практике, но он может пригодиться для создания
фиктивных объектов в тестах.

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

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

Самое интересное: конкуренция  83

пока функция завершит работу. За кулисами такая функция выполняется
как легковесный параллельный процесс, который называют сопрограммой.
Синтаксис поразительно прост: функция foo, выполняющаяся последовательно, если ее вызвать как foo(), может выполняться конкурентно, как
сопрограмма, достаточно лишь добавить в инструкцию вызова ключевое
слово go:
foo()
// Вызвать foo() и ждать ее завершения
go foo() // Запустить новую сопрограмму, которая вызовет foo() конкурентно

В сопрограммах можно запускать литералы функций:
func Log(w io.Writer, message string) {
go func() {
fmt.Fprintln(w, message)
}() // Не забудьте добавить круглые скобки в конце!
}

Каналы
Каналы в языке Go – это типизированные примитивы, поддерживающие
возможность взаимодействий двух сопрограмм. Они действуют как трубопроводы, через которые сопрограммы могут пересылать значения друг другу.
Каналы можно создавать с по­мощью функции make. Каждый канал может
передавать значения только одного определенного типа, который называют
типом элементов. Типы каналов записываются с использованием ключевого
слова chan, за которым следует тип элемента. Вот пример объявления и создания канала типа int:
var ch chan int = make(chan int)

Каналы поддерживают две основные операции: отправка и прием. Обе
записываются с использованием оператора