КулЛиб - Классная библиотека! Скачать книги бесплатно 

100 ошибок Go и как их избежать [Харшани Тейва] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
100 ошибок Go
И КАК ИХ ИЗБЕЖАТЬ

ТЕЙВА ХАРШАНИ

2024

ББК 32.973.2-018.1
УДК 004.43
Х22

Харшани Тейва
Х22 100 ошибок Go и как их избежать. — СПб.: Питер, 2024. — 480 с.: ил. — (Серия «Для профессионалов»).
ISBN 978-5-4461-2058-1
Лучший способ улучшить код — понять и исправить ошибки, сделанные при его
написании. В этой уникальной книге проанализированы 100 типичных ошибок и неэффективных приемов в Go-приложениях.
Вы научитесь писать идиоматичный и выразительный код на Go, разберете десятки
интересных примеров и сценариев и поймете, как обнаружить ошибки и потенциальные
ошибки в своих приложениях. Чтобы вам было удобнее работать с книгой, автор разделил методы предотвращения ошибок на несколько категорий, начиная от типов данных и
работы со строками и заканчивая конкурентным программированием и тестированием.
Для опытных Go-разработчиков, хорошо знакомых с синтаксисом языка.

16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.)
ББК 32.973.2-018.1
УДК 004.43

Права на издание получены по соглашению с Manning Publications. Все права защищены. Никакая часть
данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения
владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как
надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не
может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за
возможные ошибки, связанные с использованием книги. В книге возможны упоминания организаций, деятельность которых запрещена на территории Российской Федерации, таких как Meta Platforms Inc., Facebook,
Instagram и др. Издательство не несет ответственности за доступность материалов, ссылки на которые вы
можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были
действующими.

ISBN 978-1617299599 англ.
ISBN 978-5-4461-2058-1

© 2022 Manning Publications
© Перевод на русский язык ООО «Прогресс книга», 2023
© Издание на русском языке, оформление ООО «Прогресс книга»,
2023

Краткое содержание

https://t.me/it_boooks/2

Предисловие.......................................................................................................................... 18
Благодарности....................................................................................................................... 20
Об этой книге......................................................................................................................... 22
Об авторе................................................................................................................................. 25
Иллюстрация на обложке................................................................................................. 26
От издательства.................................................................................................................... 27
Глава 1. Go: просто научиться, но сложно освоить............................................. 28
Глава 2. Организация кода и проекта........................................................................ 36
Глава 3. Типы данных......................................................................................................... 98
Глава 4. Управляющие структуры..............................................................................148
Глава 5. Строки...................................................................................................................171
Глава 6. Функции и методы...........................................................................................187

6  Краткое содержание
Глава 7. Обработка ошибок..........................................................................................208
Глава 8. Конкурентность: основы...............................................................................231
Глава 9. Конкурентность: практика...........................................................................270
Глава 10. Стандартная библиотека...........................................................................321
Глава 11. Тестирование..................................................................................................355
Глава 12. Оптимизация...................................................................................................402

Оглавление

Предисловие............................................................................................... 18
Благодарности............................................................................................ 20
Об этой книге............................................................................................... 22
Для кого эта книга.................................................................................................. 22
Структура книги...................................................................................................... 22
О коде в книге.......................................................................................................... 23
Форум liveBook........................................................................................................ 24

Об авторе..................................................................................................... 25
Иллюстрация на обложке.......................................................................... 26
От издательства.......................................................................................... 27
Глава 1. Go: просто научиться, но сложно освоить............................... 28
1.1. Go: основные моменты........................................................................................... 29
1.2. Просто не означает легко....................................................................................... 30

8  Оглавление
1.3. 100 ошибок в Go....................................................................................................... 31
1.3.1. Баги................................................................................................................... 32
1.3.2. Излишняя сложность................................................................................. 33
1.3.3. Плохая читаемость...................................................................................... 33
1.3.4. Неоптимальная или неидиоматическая организация..................... 34
1.3.5. Отсутствие удобства в API....................................................................... 34
1.3.6. Неоптимизированный код........................................................................ 34
1.3.7. Недостаточная производительность..................................................... 35
Итоги.................................................................................................................................... 35

Глава 2. Организация кода и проекта..................................................... 36
2.1. Ошибка #1: непреднамеренно затенять переменные.................................. 36
2.2. Ошибка #2: лишний вложенный код............................................................... 39
2.3. Ошибка #3: неправильно использовать функцию инициализации...... 42
2.3.1. Концепция...................................................................................................... 42
2.3.2. Когда использовать функции init........................................................... 46
2.4. Ошибка #4: злоупотреблять геттерами и сеттерами................................... 49
2.5. Ошибка #5: загрязнять интерфейсы ............................................................... 50
2.5.1. Концепции...................................................................................................... 50
2.5.2. Когда использовать интерфейсы............................................................ 53
2.5.3. Загрязнение интерфейса........................................................................... 57
2.6. Ошибка #6: интерфейсы на стороне производителя.................................. 58
2.7. Ошибка #7: возврат интерфейсов...................................................................... 61
2.8. Ошибка #8: any не говорит ни о чем................................................................. 64
2.9. Ошибка #9: путаница в использовании дженериков.................................. 66
2.9.1. Концепция...................................................................................................... 67
2.9.2. Общие случаи использования и злоупотребления.......................... 71

Оглавление  9

2.10. Ошибка #10: не знать о возможных проблемах
со встраиванием типов................................................................................................... 73
2.11. Ошибка #11: не использовать паттерн функциональных опций......... 77
2.11.1. Структура Config....................................................................................... 79
2.11.2. Паттерн Строитель.................................................................................... 80
2.11.3. Паттерн функциональных опций......................................................... 82
2.12. Ошибка #12: неорганизованность проекта.................................................. 85
2.12.1. Структура проекта..................................................................................... 85
2.12.2. Организация пакета.................................................................................. 86
2.13. Ошибка #13: создавать пакеты утилит.......................................................... 88
2.14. Ошибка #14: игнорировать коллизии имен пакетов................................ 90
2.15. Ошибка #15: не писать документацию по коду.......................................... 91
2.16. Ошибка #16: не использовать линтеры......................................................... 94
Итоги.................................................................................................................................... 95

Глава 3. Типы данных................................................................................. 98
3.1. Ошибка #17: путаница с восьмеричными литералами.............................. 98
3.2. Ошибка #18: игнорировать целочисленные переполнения................... 100
3.2.1. Концепции.................................................................................................... 100
3.2.2. Обнаружение целочисленного переполнения
при инкрементировании..................................................................................... 102
3.2.3. Обнаружение целочисленного переполнения
при сложении.......................................................................................................... 103
3.2.4. Обнаружение целочисленного переполнения
при умножении...................................................................................................... 103
3.3. Ошибка #19: не понимать проблем, связанных с плавающей
точкой................................................................................................................................ 104
3.4. Ошибка #20: не понимать особенностей, связанных с длиной
среза и его емкостью..................................................................................................... 109

10  Оглавление
3.5. Ошибка #21: неэффективная инициализация среза................................. 114
3.6. Ошибка #22: путать пустые и нулевые срезы............................................. 118
3.7. Ошибка #23: неправильно проверять пустоту среза................................ 122
3.8. Ошибка #24: неправильно создавать копии срезов.................................. 124
3.9. Ошибка #25: неожиданные побочные эффекты
при использовании append в операциях со срезами.......................................... 125
3.10. Ошибка #26: срезы и утечки памяти............................................................ 129
3.10.1. Утечки емкости......................................................................................... 129
3.10.2. Срез и указатели...................................................................................... 131
3.11. Ошибка #27: неэффективно инициализировать карты......................... 135
3.11.1. Концепции.................................................................................................. 135
3.11.2. Инициализация........................................................................................ 137
3.12. Ошибка #28: карты и утечки памяти........................................................... 139
3.13. Ошибка #29: некорректное сравнение значений..................................... 142
Итоги.................................................................................................................................. 146

Глава 4. Управляющие структуры.......................................................... 148
4.1. Ошибка #30: игнорировать то, что элементы в цикле range
копируются...................................................................................................................... 148
4.1.1. Концепция.................................................................................................... 149
4.1.2. Копия значения.......................................................................................... 150
4.2. Ошибка #31: игнорировать то, как в циклах range вычисляются
аргументы......................................................................................................................... 152
4.2.1. Каналы........................................................................................................... 154
4.2.2. Массив........................................................................................................... 155
4.3. Ошибка #32: игнорировать влияние, которое оказывает
использование элементов указателя в циклах range......................................... 157
4.4. Ошибка #33: делать неверные допущения во время итераций
карты.................................................................................................................................. 161

Оглавление  11

4.4.1. Упорядочивание......................................................................................... 161
4.4.2. Вставка карты во время итераций........................................................ 163
4.5. Ошибка #34: игнорировать особенности работы оператора break...... 165
4.6. Ошибка #35: использовать defer внутри циклов........................................ 167
Итоги.................................................................................................................................. 169

Глава 5. Строки.......................................................................................... 171
5.1. Ошибка #36: не понимать концепции рун.................................................... 172
5.2. Ошибка #37: неточная итерация строк.......................................................... 174
5.3. Ошибка #38: неправильно использовать функции обрезки.................. 177
5.4. Ошибка #39: недостаточная степень оптимизации
при конкатенации строк.............................................................................................. 179
5.5. Ошибка #40: бесполезные преобразования строк..................................... 182
5.6. Ошибка #41: подстроки и утечки памяти..................................................... 183
Итоги.................................................................................................................................. 186

Глава 6. Функции и методы..................................................................... 187
6.1. Ошибка #42: не знать, какой тип получателя использовать.................. 188
6.2. Ошибка #43: не использовать именованные параметры результата....191
6.3. Ошибка #44: побочные эффекты от именованных параметров
результата......................................................................................................................... 194
6.4. Ошибка #45: возврат получателя nil.............................................................. 196
6.5. Ошибка #46: использовать имя файла в качестве входных
данных функции............................................................................................................ 200
6.6. Ошибка #47: игнорировать то, как вычисляются аргументы
и получатели оператора defer.................................................................................... 202
6.6.1. Вычисление аргументов.......................................................................... 203
6.6.2. Получатели значений или указателей................................................ 205
Итоги.................................................................................................................................. 207

12  Оглавление
Глава 7. Обработка ошибок..................................................................... 208
7.1. Ошибка #48: паника............................................................................................. 209
7.2. Ошибка #49: игнорировать оборачивание ошибки................................... 211
7.3. Ошибка #50: неточная проверка типа ошибки........................................... 215
7.4. Ошибка #51: неточная проверка значения ошибки.................................. 219
7.5. Ошибка #52: двойная обработка ошибки..................................................... 221
7.6. Ошибка #53: не выполнять обработку ошибки.......................................... 224
7.7. Ошибка #54: не выполнять обработку ошибки оператора defer........... 226
Итоги.................................................................................................................................. 229

Глава 8. Конкурентность: основы.......................................................... 231
8.1. Ошибка #55: путать конкурентность и параллелизм............................... 232
8.2. Ошибка #56: полагать, что конкурентность быстрее................................ 236
8.2.1. Планирование в Go.................................................................................... 236
8.2.2. Параллельная сортировка слиянием.................................................. 239
8.3. Ошибка #57: путаться в том, когда использовать каналы,
а когда мьютексы............................................................................................................ 244
8.4. Ошибка #58: не понимать проблем гонки.................................................... 246
8.4.1. Гонка данных и состояние гонки.......................................................... 246
8.4.2. Модель памяти Go..................................................................................... 252
8.5. Ошибка #59: не понимать влияние типа рабочей нагрузки
на конкурентность......................................................................................................... 255
8.6. Ошибка #60: неверно понимать контексты Go........................................... 261
8.6.1. Крайний срок............................................................................................... 262
8.6.2. Сигналы отмены......................................................................................... 263
8.6.3. Значения контекстов................................................................................ 264
8.6.4. Перехват отмены контекста.................................................................... 266
Итоги.................................................................................................................................. 268

Оглавление  13

Глава 9. Конкурентность: практика....................................................... 270
9.1. Ошибка #61: передавать неподходящий контекст.................................... 270
9.2. Ошибка #62: запускать горутину и не знать, когда ее остановить....... 273
9.3. Ошибка #63: неосторожно обращаться с горутинами
и переменными цикла.................................................................................................. 276
9.4. Ошибка #64: ожидать детерминированное поведение
при использовании select и каналов........................................................................ 278
9.5. Ошибка #65: не использовать каналы уведомлений................................ 283
9.6. Ошибка #66: не использовать нулевые каналы.......................................... 285
9.7. Ошибка #67: гадать насчет размера канала.................................................. 291
9.8. Ошибка #68: забывать о возможных побочных эффектах
при форматировании строк........................................................................................ 294
9.8.1. Гонка данных в etcd................................................................................... 295
9.8.2. Взаимоблокировка..................................................................................... 296
9.9. Ошибка #69: создавать ситуацию гонки данных
из-за оператора append................................................................................................. 299
9.10. Ошибка #70: неверно использовать мьютексы со срезами
и картами.......................................................................................................................... 301
9.11. Ошибка #71: неправильно использовать sync.WaitGroup.................... 304
9.12. Ошибка #72: забывать о sync.Cond............................................................... 307
9.13. Ошибка #73: не использовать errgroup....................................................... 313
9.14. Ошибка #74: копировать тип sync................................................................ 317
Итоги.................................................................................................................................. 319

Глава 10. Стандартная библиотека........................................................ 321
10.1. Ошибка #75: неправильно задавать промежуток времени................... 322
10.2. Ошибка #76: time.After и утечки памяти.................................................... 323
10.3. Ошибка #77: типичные ошибки при обработке JSON........................... 326
10.3.1. Неожиданное поведение из-за встраивания типов...................... 326

14  Оглавление
10.3.2. JSON и монотонные часы..................................................................... 329
10.3.3. Карта типа any........................................................................................... 332
10.4. Ошибка #78: типичные ошибки, связанные с SQL................................. 333
10.4.1. Не знать, что sql.Open не всегда устанавливает
соединение с базой данных................................................................................ 333
10.4.2. Забывать о пуле соединений................................................................ 334
10.4.3. Не использовать подготовленные операторы................................ 336
10.4.4. Неправильная обработка нулевых значений................................. 337
10.4.5. Не обрабатывать ошибки итерации строк...................................... 339
10.5. Ошибка #79: не закрывать временные ресурсы....................................... 340
10.5.1. Тело HTTP................................................................................................. 340
10.5.2. sql.Rows........................................................................................................ 343
10.5.3. os.File............................................................................................................ 344
10.6. Ошибка #80: забывать об операторе return после ответа
на HTTP-запрос............................................................................................................. 346
10.7. Ошибка #81: использовать стандартные HTTP-клиент
и сервер.............................................................................................................................. 348
10.7.1. HTTP-клиент............................................................................................ 348
10.7.2. HTTP-сервер............................................................................................. 351
Итоги.................................................................................................................................. 353

Глава 11. Тестирование............................................................................ 355
11.1. Ошибка #82: не распределять тесты по категориям............................... 356
11.1.1. Теги сборки................................................................................................. 356
11.1.2. Переменные среды.................................................................................. 358
11.1.3. Короткий режим....................................................................................... 359
11.2. Ошибка #83: не включать флаг -race............................................................ 360

Оглавление  15

11.3. Ошибка #84: не использовать режимы выполнения тестов................ 363
11.3.1. Флаг parallel............................................................................................... 363
11.3.2. Флаг -shuffle............................................................................................... 365
11.4. Ошибка #85: не использовать табличные тесты...................................... 366
11.5. Ошибка #86: задержки в юнит-тестах......................................................... 371
11.6. Ошибка #87: неэффективная работа с API времени.............................. 374
11.7. Ошибка #88: не использовать пакеты утилит для тестирования....... 379
11.7.1. Пакет httptest............................................................................................ 379
11.7.2. Пакет iotest................................................................................................. 381
11.8. Ошибка #89: писать неточные бенчмарки................................................. 384
11.8.1. Не сбрасывать или не ставить на паузу таймер............................ 385
11.8.2. Делать неверные предположения о микробенчмарках.............. 386
11.8.3. Небрежное отношение к оптимизациям компилятора.............. 389
11.8.4. Эффект наблюдателя.............................................................................. 391
11.9. Ошибка #90: не изучать все возможности тестирования в Go........... 395
11.9.1. Покрытие тестами................................................................................... 395
11.9.2. Тестирование из другого пакета......................................................... 396
11.9.3. Вспомогательные функции.................................................................. 397
11.9.4. Настройка и демонтаж........................................................................... 398
Итоги.................................................................................................................................. 399

Глава 12. Оптимизация............................................................................ 402
12.1. Ошибка #91: не понимать устройство кэша CPU................................... 403
12.1.1. Архитектура CPU.................................................................................... 403
12.1.2. Кэш-линия.................................................................................................. 405
12.1.3. Срез структур и структура срезов...................................................... 408

16  Оглавление
12.1.4. Предсказуемость...................................................................................... 410
12.1.5. Стратегия размещения кэша .............................................................. 412
12.2. Ошибка #92: писать конкурентный код, который приводит
к ложному совместному использованию............................................................... 418
12.3. Ошибка #93: не учитывать параллелизм на уровне инструкций....... 423
12.4. Ошибка #94: не знать о выравнивании данных........................................ 430
12.5. Ошибка #95: не понимать различий между стеком и кучей................ 435
12.5.1. Стек и куча................................................................................................. 435
12.5.2. Эскейп-анализ........................................................................................... 440
12.6. Ошибка #96: не знать, как сократить число выделений памяти........ 443
12.6.1. Изменения API......................................................................................... 443
12.6.2. Приемы оптимизации компилятора................................................. 444
12.6.3. sync.Pool...................................................................................................... 445
12.7. Ошибка #97: не полагаться на встраивание.............................................. 448
12.8. Ошибка #98: не использовать диагностический
инструментарий Go....................................................................................................... 451
12.8.1. Профилирование..................................................................................... 451
12.8.2. Трассировщик выполнения.................................................................. 460
12.9. Ошибка #99: не понимать, как работает сборщик мусора.................... 465
12.9.1. Концепции.................................................................................................. 465
12.9.2. Примеры..................................................................................................... 467
12.10. Ошибка #100: не понимать особенностей запуска Go
внутри Docker и Kubernetes....................................................................................... 471
Итоги.................................................................................................................................. 474
Заключение...................................................................................................................... 475

Дэйву Харшани: продолжай оставаться тем, кто ты есть, братик.
Твой потолок — звезды.
Милой Мелиссе.

Предисловие

В 2019 году я во второй раз начал профессионально заниматься работой на Go
в качестве основного языка программирования. Тогда я заметил некоторые
закономерности, связанные с ошибками написания кода на Go. Я подумал,
что обобщение информации о таких частых ошибках было бы полезно для разработчиков.
В своем блоге я сделал пост «10 самых распространенных ошибок, с которыми
я сталкивался в проектах на Go» («The Top 10 Most Common Mistakes I’ve Seen
in Go Projects»). Пост стал популярным: его прочитали более 100 000 человек,
он был выбран новостным бюллетенем Golang Weekly как один из лучших за
2019 год. Мне льстили положительные отзывы, которые я получал от сообщества Go.
Я понял, что обсуждение типичных ошибок — это мощный инструмент разработки. Сопровождаемый конкретными примерами, он поможет им эффективно
осваивать новые навыки, облегчать запоминание как контекста, в котором эти
ошибки встречаются, так и способов, позволяющих их избегать.
Около года я собирал примеры типичных ошибок: из профессиональных проектов других разработчиков, из репозиториев опенсорсных программ, из книг,
блогов, исследований и обсуждений в сообществе Go. Могу сказать, что я и сам
был «достойным источником информации» в плане подобных ошибок.
К концу 2020 года размер моей коллекции ошибок достиг 100 штук, и это показалось мне подходящим, чтобы предложить идею публикации какому-либо
издательству. В результате я связался с Manning, которое считал высококлассным

Предисловие  19

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

Благодарности

Хочу выразить свою признательность многим людям. Моим родителям — за то,
что поддержали меня в тот момент, когда во время учебы я ощутил себя так, как
будто нахожусь в ситуации полного провала. Моему дяде Жан-Полю Демону
(Jean-Paul Demont) за то, что помог увидеть свет в конце туннеля. Пьеру Готье
(Pierre Gautier) за то, что был замечательным вдохновителем и помог мне поверить в себя. Дэмиену Шамбону (Damien Chambon) за то, что заставлял меня
постоянно поднимать планку и подталкивал меня к лучшему. Лорану Бернару
(Laurent Bernard) за то, что был образцом для подражания и привел меня
к осознанию того, что навыки социального общения очень важны. Валентину
Делепласу (Valentin Deleplace) за последовательность и логичность его исключительно полезных отзывов. Дугу Раддеру (Doug Rudder) за то, что обучил меня
тонкому искусству передачи идей в письменной форме. Тиффани Тейлор (Tiffany
Taylor) и Кэти Теннант (Katie Tennant) за высококачественное редактирование
и корректуру текста, а также Тиму ван Дерзену (Tim van Deurzen) за глубину
и качество профессионального рецензирования.
Хочу также поблагодарить Клару Шамбон (Clara Chambon) — мою любимую
маленькую крестницу, Виржини Шамбон (Virginie Chambon) — милейшего человека на свете, всю семью Харшани, Афродити Катику (Afroditi Katika), Серхио
Гарсеза (Sergio Garcez) и Каспера Бентсена (Kasper Bentsen) — замечательных
инженеров-разработчиков, а также все сообщество Go.
Наконец, я хотел бы поблагодарить своих рецензентов: Адама Ванадамайкена (Adam Wanadamaiken), Алессандро Кампейса (Alessandro Campeis), Аллена Гуча (Allen Gooch), Андреса Сакко (Andres Sacco), Анупама Сенгупту
(Anupam Sengupta), Борко Джурковича (Borko Djurkovic), Брэда Хоррокса
(Brad Horrocks), Камала Какара (Camal Cakar), Чарльза М. Шелтона (Charles

Благодарности  21

M. Shelton), Криса Аллана (Chris Allan), Клиффорда Тербера (Clifford Thurber),
Козимо Дамиано Прете (Cosimo Damiano Prete), Дэвида Кронкайта (David
Cronkite), Дэвида Джейкобса (David Jacobs), Дэвида Моравека (David Moravec),
Фрэнсиса Сеташа (Francis Setash), Джанлуиджи Спаньоло (Gianluigi Spagnuolo),
Джузеппе Максиа (Giuseppe Maxia), Хироюки Мушу (Hiroyuki Musha), Джеймса Бишопа (James Bishop), Джерома Майера (Jerome Meyer), Джоэля Холмса
(Joel Holmes), Джонатана Р. Чоута (Jonathan R. Choate), Йорта Роденбурга
(Jort Rodenburg), Кита Кима (Keith Kim), Кевина Ляо (Kevin Liao), Лева Вайде
(Lev Veyde), Мартина Денерта (Martin Dehnert), Мэтта Велке (Matt Welke),
Нираджа Шаха (Neeraj Shah), Оскара Утбулта (Oscar Utbult), Пейти Ли (Peiti
Li), Филиппа Джанертка (Philipp Janertq), Роберта Веннера (Robert Wenner),
Райана Барроуска (Ryan Burrowsq), Райана Хубера (Ryan Huber), Санкета Найка
(Sanket Naik), Сатадру Ройя (Satadru Roy), Шона Д. Вика (Shon D. Vick), Тада
Майера (Thad Meyer) и Вадима Туркова. Все ваши предложения и замечания
помогли сделать эту книгу лучше.

Об этой книге

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

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

Структура книги
Книга состоит из 12 глав:
Глава 1 «Go: просто научиться, но сложно освоить» объясняет, почему, несмотря
на то что Go считается простым языком, его нелегко освоить досконально. В ней
также приведены типы ошибок, которые мы рассмотрим в книге.
Глава 2 «Организация кода и проекта» содержит описание распространенных
ошибок, которые могут помешать организовать программный код чистым, идио­
матичным, удобным для дальнейшей обработки и поддержки образом.

Об этой книге  23

В главе 3 «Типы данных» обсуждаются ошибки, связанные с основными типами,
срезами и картами.
В главе 4 «Управляющие структуры» исследуются распространенные ошибки,
связанные с циклами и другими управляющими структурами.
В главе 5 «Строки» рассматривается принцип представления строк и связанные
с ним распространенные ошибки, приводящие к неточности или неэффективности кода.
В главе 6 «Функции и методы» обсуждаются распространенные проблемы,
связанные с функциями и методами, такие как выбор типа получателя и предотвращение распространенных ошибок отложенного выполнения (defer).
В главе 7 «Обработка ошибок» рассматривается идиоматическая и точная обработка ошибок в Go.
В главе 8 «Конкурентность: основы» представлены основные концепции конкурентности. Мы разберем, почему конкурентность не всегда быстрее, в чем
различия между конкурентностью и параллелизмом, а также обсудим типы
рабочей нагрузки.
В главе 9 «Конкурентность: практика» рассмотрены примеры ошибок, связанных
с конкурентностью при использовании каналов, горутин и других примитивов
Go.
Глава 10 «Стандартная библиотека» содержит описание распространенных
ошибок, допускаемых при использовании стандартной библиотеки с HTTP,
JSON или (например) time API.
В главе 11 «Тестирование» обсуждаются ошибки, которые делают тестирование
и бенчмаркинг менее универсальными, эффективными и точными.
Глава 12 «Оптимизация» завершает книгу. В ней исследуются способы того,
как оптимизировать приложение для повышения его производительности, — от
понимания основ функционирования центрального процессора до конкретных
тем, связанных с Go.

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

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

дыдущими шагами, — например, при добавлении новой функциональности
в существующую строку кода.
Во многих случаях оригинальная версия исходного кода переформатируется;
добавляются разрывы строк и измененные отступы, чтобы код помещался на
странице. Иногда даже этого оказывается недостаточно и в листинги включаются
маркеры продолжения строк (➥). Также из исходного кода часто удаляются
комментарии, если код описывается в тексте.
Исполняемые фрагменты кода можно загрузить из версии liveBook (электронной) по адресу https://livebook.manning.com/book/100-go-mistakes-how-to-avoid-them.
Полный код примеров книги доступен для загрузки на сайте Manning по адресу
https://www.manning.com/books/100-go-mistakes-how-to-avoid-them и GitHub https://
github.com/teivah/100-go-mistakes.

Форум liveBook
Приобретая книгу «100 ошибок Go и как их избежать», вы получаете бесплатный
доступ к закрытому веб-форуму издательства Manning (на английском языке), на
котором можно оставлять комментарии о книге, задавать технические вопросы
и получать помощь от автора и других пользователей. Чтобы получить доступ
к форуму, откройте страницу https://livebook.manning.com/book/100-go-mistakes-howto-avoid-them/discussion. Информацию о форумах Manning и правилах поведения
на них см. на https://livebook.manning.com/#!/discussion.
В рамках своих обязательств перед читателями издательство Manning предоставляет ресурс для содержательного общения читателей и авторов. Эти обязательства не подразумевают конкретную степень участия автора, которое остается
добровольным (и неоплачиваемым). Задавайте автору хорошие вопросы, чтобы
он не терял интереса к происходящему! Форум и архивы обсуждений доступны
на веб-сайте издательства, пока книга продолжает издаваться.

Об авторе

ТЕЙВА ХАРШАНИ — старший инженер-программист в Docker. Работал в области страхования, транспорта и в отраслях, где критически важна безопасность,
например в управлении воздушным движением. Увлечен языком Go и тем, как
разрабатывать и реализовывать на нем надежные приложения.

Иллюстрация на обложке

На обложке книги — рисунок под названием «Femme de Buccari en Croatie»
(«Женщина из Бакара, Хорватия»).
Иллюстрация взята из вышедшего в 1797 году каталога национальных костюмов, составленного Жаком Грассе де Сен-Савьером. Каждая иллюстрация этого
каталога тщательно прорисована и раскрашена от руки. В прежние времена
по одежде человека можно было легко определить, где он живет и какова его
профессия или положение. Manning отдает дань изобретательности и инициативности компьютерных технологий, используя для своих изданий обложки,
демонстрирующие богатое вековое разнообразие региональных культур, оживающее на изображениях из собраний, подобных этому.

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com
(издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию
о наших книгах.

1

Go: просто научиться,
но сложно освоить

https://t.me/it_boooks/2

В этой главе:
33 Что делает Go эффективным, масштабируемым
и производительным языком

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

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

1.1. Go: основные моменты  29

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

1.1. GO: ОСНОВНЫЕ МОМЕНТЫ
Если вы читаете нашу книгу, то, скорее всего, уже «подсели» на Go. Поэтому
в этом разделе будет только краткий обзор, призванный напомнить, что делает
Go таким мощным языком.
Отрасль разработки программного обеспечения (ПО) за последние десятилетия
значительно изменилась. Большинство современных систем больше не создается
одним человеком. Все они — результат работы команд, состоящих из многих программистов, а иногда даже из сотен, если не тысяч. Написанный программный код
должен быть читабельным, выразительным, удобным в сопровождении, чтобы
обеспечивать надежную работу системы на протяжении многих лет. С другой
стороны, в нашем быстро меняющемся мире максимальное повышение гибкости
и сокращение времени выхода на рынок очень важны для большинства компаний.
Программирование тоже должно следовать этой тенденции, поэтому компании
стремятся к тому, чтобы программисты работали максимально продуктивно при
чтении, написании и сопровождении кода.
В ответ на эти вызовы и требования в 2007 году компания Google создала
язык Go. С тех пор многие организации приняли его для использования в различных областях программирования: в API, автоматизации, базах данных,
интерфейсах командной строки и т. д. Сегодня многие считают Go одним из
основных языков для разработки облачных систем.
Что касается функциональности, то в Go нет наследования типов, исключений,
макросов, частичных функций, поддержки ленивых вычислений или неизменяемости, перегрузки операторов, сопоставления шаблонов и т. д. Почему? Вот
что об этом говорит официальный FAQ по Go (https://go.dev/doc/faq):
Почему в Go нет какой-то функции X? Ваша любимая функция может отсутствовать, поскольку не вписывается в логику или структуру языка, влияет
на скорость компиляции или ясность дизайна кода либо просто потому, что
сделала бы фундаментальную модель системы слишком сложной.
Оценка качества языка программирования на основании количества функций
в нем, вероятно, некорректна. По крайней мере для Go эта метрика не главная.

30  Глава 1. Go: просто научиться, но сложно освоить
При оценке адекватности использования языка в масштабе какой-то организации
используют несколько важных характеристик. К ним относятся:
Стабильность. Несмотря на то что в Go вносятся частые изменения (направленные на улучшение самого языка и устранение уязвимостей с точки
зрения безопасности), он остается достаточно стабильным языком. Некоторые считают это качество одной из лучших особенностей языка.
Выразительность. Мы можем определить выразительность языка по тому,
насколько написание и чтение кода отвечает представлениям о естественности и интуитивной понятности. Уменьшенное количество ключевых слов
и ограниченные способы решения общих проблем делают Go выразительным
языком для больших кодовых баз.
Компиляция. Что может быть более раздражающим для разработчиков, чем
долгое ожидание сборки для тестирования приложения? Стремление к быстрой компиляции всегда было сознательной целью разработчиков языка.
А это основа высокой производительности.
Безопасность. Go — надежный язык со статической типизацией. Следовательно, у него есть строгие правила времени компиляции, которые в большинстве
случаев обеспечивают безопасность типов.
Go был создан с нуля с очень полезными функциями: с примитивами конкурентности, горутинами и каналами. Ему особо не нужно полагаться на внешние
библиотеки для создания эффективных конкурентных приложений. Наблюдение
за тем, насколько важна конкурентность в наши дни, также показывает, почему
Go сейчас самый подходящий язык и будет оставаться им в обозримом будущем.
Некоторые считают Go простым языком, и отчасти это правда. Например, новичок может разобраться с его основными возможностями менее чем за один
день. Возникает вопрос: зачем же изучать книгу, посвященную систематизации
ошибок в Go, если он так прост?

1.2. ПРОСТО НЕ ОЗНАЧАЕТ ЛЕГКО
Между понятиями «просто» и «легко» есть тонкая разница. «Простой» применительно к технологии означает несложный для изучения или понимания.
«Легкость» означает возможность добиваться чего угодно без особых усилий.
Go прост в изучении, но не всегда легок в освоении.
Возьмем, к примеру, конкурентность. В 2019 году было опубликовано исследование, посвященное ошибкам конкурентности: «Понимание реальных ошибок

1.3. 100 ошибок в Go  31

конкурентности в Go»1. Это исследование было первым систематическим анализом ошибок конкурентности. Оно опиралось на данные нескольких популярных
репозиториев Go — Docker, gRPC и Kubernetes. Один из самых важных выводов
заключается в том, что большинство блокирующих ошибок вызвано неточным
использованием парадигмы передачи сообщений (message passing) по каналам,
несмотря на убеждение, что передача сообщений легче обрабатывается и менее
подвержена ошибкам, чем разделяемая память.
Какой должна быть реакция на такой вывод? Должны ли мы считать, что
разработчики языка ошибались насчет передачи сообщений? Должны ли мы
пересмотреть использование конкурентности в нашем проекте? Конечно нет.
Это не вопрос противопоставления передачи сообщений разделяемой памяти
и выявления из них «победителя». Но разработчики Go должны хорошо понимать, как использовать конкурентность, каково ее влияние на современные
процессоры, когда следует предпочесть один подход другому и как избежать
при этом попадания в типичные ловушки. Этот пример подчеркивает, что хотя
каналы и горутины могут быть простыми для изучения, на практике это совсем
не просто.
Понятие «просто не значит легко» можно обобщить на многие аспекты Go, а не
только на конкурентность. И чтобы стать опытными Go-разработчиками, нужно
хорошо разбираться во всех его аспектах. А это требует времени, усилий и ошибок.
Цель книги — помочь ускорить наш путь к мастерству, рассмотрев 100 ошибок
в Go.

1.3. 100 ОШИБОК В GO
Почему следует прочитать эту книгу? Почему бы вместо этого не углубить знания с помощью «обычной» книги, которая достаточно подробно рассматривает
разные темы?
В статье, опубликованной в 2011 году, нейробиологи доказали, что столкновение
с ошибками — это лучшие моменты для развития способностей нашего мозга2.
1

2

T. Tu, X. Liu, et al. (с соавторами), Understanding Real-World Concurrency Bugs in Go,
работа была представлена на ASPLOS 2019, April 13–17, 2019.
J. S. Moser, H. S. Schroder, с соавторами, “Mind Your Errors: Evidence for a Neural
Mechanism Linking Growth Mindset to Adaptive Posterror Adjustments,” Psychological
Science, vol. 22, no. 12, pp. 1484–1489, Dec. 2011.

32  Глава 1. Go: просто научиться, но сложно освоить
Все мы проходили через процесс обучения на какой-то ошибке, вспоминая этот
случай через месяцы или даже годы, когда с ним был связан какой-то контекст.
В статье Джанет Меткалф (Janet Metcalfe) говорится, что это происходит потому, что ошибки оказывают стимулирующее воздействие1. Суть в том, что мы
можем помнить не только саму ошибку, но и ее контекст. И поэтому обучение
на ошибках так эффективно.
Чтобы усилить этот эффект, в книге каждая рассматриваемая типичная ошибка
подкреплена примерами из реальной практики. Эта книга не только о теории,
она поможет избежать ошибок и принимать взвешенные, осознанные решения.
Скажи мне, и я забуду. Научи меня, и я запомню. Вовлеки меня, и я научусь.
Неизвестный автор
Здесь представлены семь основных категорий ошибок, которые можно классифицировать как:
баги;
излишнюю сложность;
плохую читаемость;
неоптимальную или неидиоматическую организацию;
отсутствие удобства в API;
неоптимизированный код;
недостаточную производительность.
Далее я дам краткое описание каждой категории ошибок.

1.3.1. Баги
Первый и, возможно, самый очевидный тип — это ошибки в исходном коде.
В 2020 году исследование, проведенное Synopsys, оценило стоимость багов в ПО
только в США более чем в 2 триллиона долларов2.
1

2

J. Metcalfe, “Learning from Errors,” Annual Review of Psychology, vol. 68, pp. 465–489,
Jan. 2017.
Synopsys, “The Cost of Poor Software Quality in the US: A 2020 Report.” 2020. https://
news.synopsys.com/2021-01-06-Synopsys-Sponsored-CISQ-Research-Estimates-Cost-ofPoor-Software-Quality-in-the-US-2-08-Trillion-in-2020.

1.3. 100 ошибок в Go  33

Баги могут приводить и к трагическим последствиям. Вспомним случай с аппаратом для лучевой терапии Therac-25 производства компании Atomic Energy
of Canada Limited (AECL). Из-за состояния гонки машина дала своим пациентам дозы облучения, которые в сотни раз превышали ожидаемые, что привело
к смерти трех пациентов. Этот пример показывает, что баги могутповлечь за
собой не только денежные потери. И мы, как разработчики, должны помнить,
насколько важна наша работа.
Я рассмотрю множество случаев, которые могут привести к различным багам,
включая гонки данных, утечки, логические ошибки и др. Хотя точные тесты
и должны обнаруживать такие ошибки как можно раньше, иногда мы можем
пропускать их из-за различных факторов, например из-за нехватки времени
или их сложности. И разработчику важно убедиться, что для устранения таких
багов сделано все возможное.

1.3.2. Излишняя сложность
Следующая категория ошибок связана с излишней сложностью. Значительная
часть сложности ПО вызвана тем, что разработчики стремятся думать о своем
воображаемом будущем. Вместо того чтобы решать конкретные задачи прямо
сейчас, может возникнуть соблазн создать «эволюционирующее» ПО, которое
будет пригодным для любого будущего варианта использования. В большинстве
случаев это приводит к тому, что объем недостатков превышает число преимуществ, что делает код сложным для понимания и анализа.
Возвращаясь к Go, можно вспомнить множество примеров того, как у разработчиков возникает соблазн разработать абстрактные функции для будущего,
например интерфейсы или дженерики. В этой книге обсуждаются примеры,
когда следует проявлять особую осторожность, чтобы не переусложнить код.

1.3.3. Плохая читаемость
Как написал Роберт Мартин (Robert Martin) в книге «Clean Code: A Handbook
of Agile Software Craftsmanship»1, соотношение времени, затрачиваемого на
чтение и написание кода, значительно превышает 10 : 1. Большинство из нас
начинали программировать в собственных проектах, где удобочитаемость не так
важна. Но сегодняшняя разработка ПО — это программирование во временном
1

Мартин Р. «Чистый код: создание, анализ и рефакторинг». Санкт-Петербург, издательство «Питер».

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

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

1.3.5. Отсутствие удобства в API
Распространенные ошибки, снижающие удобство API для наших потребителей, — это еще один тип. Если API неудобен для пользователя, он будет менее
выразительным и, следовательно, более трудным для понимания и более подверженным дальнейшим ошибкам.
Такие ошибки встречаются во многих ситуациях и могут заключаться в чрезмерном использовании типа any, в использовании неправильных порождающих
паттернов при работе с опциями или в слепом применении стандартных методов
объектно-ориентированного программирования, что влияет на удобство использования API. Мы рассмотрим распространенные ошибки, мешающие передавать
в распоряжение наших пользователей удобные для них API.

1.3.6. Неоптимизированный код
Код, оптимизированный в недостаточной степени, — еще один тип ошибок разработчиков. Их можно сделать по разным причинам, например из-за непонимания особенностей языка или даже из-за отсутствия фундаментальных знаний.

Итоги  35

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

1.3.7. Недостаточная производительность
В большинстве случаев мы задаемся вопросом: какой язык лучше всего выбрать
для конкретного нового проекта? Ответ: тот, с которым мы работаем наиболее
продуктивно. Для достижения мастерства очень важно знать, как работает язык,
и использовать его по максимуму.
Мы рассмотрим конкретные примеры, которые помогут стать продуктивными
при работе на Go. Например, написание эффективных тестов для обеспечения
работоспособности кода, использование стандартной библиотеки для повышения эффективности, а также извлечение максимальной пользы из инструментов
профилирования и линтеров. Пришло время разобраться в этих 100 распространенных ошибках Go!

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

2

Организация кода и проекта

В этой главе:
33 Идиоматическая организация кода

33 Эффективная работа с абстракциями: интерфейсы и дженерики
33 Как структурировать проект: лучшие практики

Сделать текст кода в Go чистым, идиоматичным и удобным для сопровождения — непростая задача. Чтобы понять суть лучших практик, связанных с написанием кода и организацией проекта, потребуется накопить определенный опыт
и набить шишки. Каких ловушек следует избегать (например, затенения переменных и злоупотребления вложенным кодом)? Как структурировать пакеты?
Когда и где использовать интерфейсы или дженерики, функции инициализации
и пакеты утилит? Рассмотрим распространенные ошибки в организации кода.

2.1. ОШИБКА #1: НЕПРЕДНАМЕРЕННО
ЗАТЕНЯТЬ ПЕРЕМЕННЫЕ
Область видимости переменной — это те места кода, в которых можно ссылаться на эту переменную, другими словами, та часть приложения, где действует
привязка имени. В Go имя переменной, уже объявленное во внешней области

2.1. Ошибка #1  37

видимости, может быть повторно объявлено во внутренней области видимости.
Такая ситуация называется затенением переменной и может приводить к распространенным ошибкам.
В примере ниже показан непреднамеренный побочный эффект из-за наличия
затененной переменной. В этом фрагменте кода HTTP-клиент создается двумя
разными способами, в зависимости от булева значения tracing:
var client *http.Client
Объявляется переменная client
if tracing {
client, err := createClientWithTracing()
Создается HTTP-клиент со включенной
if err != nil {
трассировкой. (Переменная client
return err
затенена в этом блоке)
}
log.Println(client)
} else {
client, err := createDefaultClient()
Создается HTTP-клиент по умолчанию.
if err != nil {
(Переменная client также затенена
return err
в этом блоке)
}
log.Println(client)
}
// Использование переменной client

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

ПРИМЕЧАНИЕ Этот код компилируется, поскольку внутренние переменные client используются в вызовах логирования. В противном случае
появлялись бы ошибки компиляции: client declared and not used.
Как обеспечить присвоение значения именно исходной переменной client?
Есть два варианта.
var client *http.Client
if tracing {
c, err := createClientWithTracing()
Создается временная переменная c
if err != nil {
return err
}
client = c
Переменной client присваивается значение
} else {
этой временной переменной
// Та же логика
}

38  Глава 2. Организация кода и проекта
Здесь мы присваиваем результат временной переменной c, область видимости
которой находится только в пределах блока if. Затем присваиваем его обратно
переменной client. То же делаем для блока else.
Во втором варианте используется оператор присваивания (=) во внутренних
блоках для непосредственного присвоения результатов функции переменной
client. Но для этого нужно создать переменную error, поскольку оператор
присваивания работает только в том случае, если имя переменной уже было
объявлено. Например:
var client *http.Client
var err error
Объявляется переменная err
if tracing {
client, err = createClientWithTracing()
if err != nil {
return err
}
} else {
// Та же логика
}

Используется оператор присваивания,
чтобы напрямую присвоить переменной
client значение, возвращаемое *http.Client

Чтобы не присваивать значение временной переменной, мы можем напрямую
присвоить результат переменной client.
Оба способа вполне допустимы. Основное различие между ними заключается
в том, что во втором варианте мы выполняем только одно присваивание, что
можно считать более легким для чтения. Кроме того, со вторым вариантом
можно объединить и реализовать обработку ошибок вне блоков операторов if/
else, как показано в следующем примере:
if tracing {
client, err = createClientWithTracing()
} else {
client, err = createDefaultClient()
}
if err != nil {
// Типичная обработка ошибок
}

Затенение переменной происходит, когда ее имя повторно объявляется во внутренней области видимости, но мы видели, что эта практика чревата ошибками.
Установка правила, запрещающего затененные переменные, зависит от личного
вкуса. Иногда бывает удобно повторно использовать существующее имя, например err, для обозначения тех переменных, которые так или иначе связаны
с ошибками. Но следует быть начеку, потому что теперь мы знаем, что можем
столкнуться со сценарием, когда код компилируется, но переменная на самом

2.2. Ошибка #2  39

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

2.2. ОШИБКА #2: ЛИШНИЙ ВЛОЖЕННЫЙ КОД
Ментальная модель, относящаяся к конкретному программному продукту,
представляет собой внутреннее мысленное представление о том, как ведет себя
система. При программировании нужно придерживаться таких ментальных
моделей (например, общих взаимодействий в коде и реализациях функций).
Код считается удобочитаемым по множеству критериев: использование имен/
названий, согласованность, соответствующее форматирование и т. д. Читабельный код требует меньше когнитивных усилий для понимания его соответствия
ментальной модели, поэтому его легче читать и сопровождать.
Важнейший аспект удобочитаемости — это фактор количества вложенных
уровней. Предположим, что мы работаем над новым проектом и нужно понять,
что делает следующая функция join:
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
} else {
Вызывает функцию concatenate
if s2 == "" {
для выполнения определенной
return "", errors.New("s2 is empty")
конкатенации, но может возвращать
} else {
ошибки
concat, err := concatenate(s1, s2)
if err != nil {
return "", err

}

}

}

} else {
if len(concat) > max {
return concat[:max], nil
} else {
return concat, nil
}
}

func concatenate(s1 string, s2 string) (string, error) {
// ...
}

40  Глава 2. Организация кода и проекта
Эта функция join объединяет две строки и возвращает подстроку, если длина
больше максимальной. Кроме того, она обрабатывает проверки s1 и s2 и проверяет, возвращает ли вызов concatenate ошибку.
С точки зрения реализации функциональности все сделано правильно. Но
выстраивание ментальной модели, охватывающей все различные случаи,
скорее всего, будет непростой задачей. Почему? Из-за количества вложенных уровней.
Посмотрим на код, выполняющий ту же функцию, но реализованный подругому:
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
}
if s2 == "" {
return "", errors.New("s2 is empty")
}
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
}
if len(concat) > max {
return concat[:max], nil
}
return concat, nil
}
func concatenate(s1 string, s2 string) (string, error) {
// ...
}

Вы, наверное, заметили, что выстраивание ментальной модели в этой новой
версии кода требует меньше когнитивного напряжения, хотя код выполняет то
же самое, что и раньше. Здесь есть только два вложенных уровня. Как упомянул
Мэт Райер (Mat Ryer), эксперт, участвующий в дискуссии подкаста Go Time
(https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88):
Выровняйте «счастливый путь» (happy path) по левому краю — так вы сможете быстро просмотреть, что происходит ниже на каком-то одном уровне
и увидеть, что на нем ожидаемо выполняется.
В первой версии выполнения этого упражнения было сложно определить, что
из ожидаемого выполняется, из-за вложенных операторов if/else. И наоборот,
вторая версия требует просмотра вниз первого уровня, чтобы увидеть поток

2.2. Ошибка #2  41

выполняемых действий, и второго уровня, чтобы увидеть, как обрабатываются
пограничные случаи, как показано на рис. 2.1.
func join(s1, s2 string, max int) (string, error) {
if s1 == "" {
return "", errors.New("s1 is empty")
}
if s2 == "" {
return "", errors.New("s2 is empty")
}
concat, err := concatenate(s1, s2)
if err != nil {
return "", err
}
if len(concat) > max {
return concat[:max], nil
}
return concat, nil

Счастливый путь

Случаи ошибок и пограничных случаев

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

Как правило, чем больше вложенных уровней требует функция, тем сложнее ее
читать и понимать. Рассмотрим несколько различных применений этого правила,
чтобы оптимизировать код для удобства чтения:
Когда происходит возврат из блока if, следует во всех случаях опускать блок
else. Например, мы не должны писать:
if foo() {
// ...
return true
} else {
// ...
}

Вместо этого следует опустить блок else, как показано здесь:
if foo() {
// ...
return true
}
// ...

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

42  Глава 2. Организация кода и проекта
Можно следовать этой логике в случае с путем, не являющимся «счаст­
ливым»:
if s != "" {
// ...
} else {
return errors.New("empty string")
}

Здесь пустая переменная s определяет путь, не являющимся «счастливым».
Поэтому нужно изменить это условие так:
if s == "" {
Изменение условия в if
return errors.New("empty string")
}
// ...

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

2.3. ОШИБКА #3: НЕПРАВИЛЬНО ИСПОЛЬЗОВАТЬ
ФУНКЦИЮ ИНИЦИАЛИЗАЦИИ
Иногда в приложениях Go неправильно используются функции инициализации.
Потенциальные последствия — трудности в отслеживании и обработке ошибок
или сложный в понимании код. Освежим наше представление о том, что такое
функция инициализации, а затем рассмотрим, когда ее использование уместно.

2.3.1. Концепция
Функция инициализации (init) — это функция, используемая для инициализации состояния приложения. Она не имеет аргументов и не возвращает результата

2.3. Ошибка #3  43

(функция func()). Когда пакет инициализируется, оцениваются все объявления
констант и переменных в пакете. Затем выполняются функции инициализации.
Вот пример инициализации пакета main:
package main
import "fmt"
var a = func() int {
fmt.Println("var")
return 0
}()

Исполняется в первую очередь

func init() {
fmt.Println("init")
}

Исполняется во вторую очередь

func main() {
fmt.Println("main")
}

Исполняется в последнюю очередь

Исполнение кода этого примера выведет следующее:
var
init
main

Функция init выполняется при инициализации пакета. В следующем примере
мы определяем два пакета — main и redis, где main зависит от redis. Сначала
main.go из основного пакета:
package main
import (
"fmt"
)

"redis"

func init() {
// ...
}
func main() {
err := redis.Store("foo", "bar")
// ...
}

Указание на зависимость от пакета redis

44  Глава 2. Организация кода и проекта
А затем redis.go из пакета redis:
package redis
// imports
func init() {
// ...
}
func Store(key, value string) error {
// ...
}

Поскольку main зависит от redis, сначала выполняется функция инициализации
в пакете redis, затем — в основном пакете, а затем сама функция main. На рис. 2.2
показана эта последовательность.
Мы можем определить несколько функций инициализации init для каждого
пакета. В таком случае последовательность выполнения функции инициализации внутри пакета задается алфавитным порядком исходных файлов. Например, если пакет содержит файл a.go и файл b.go и в обоих содержится функция
инициализации, то первой выполняется та из них, что находится в a.go.
Пример с функциями инициализации
Пакет main

Пакет redis

main.go

redis.go

2

init()

3

main()

1

init()

Store(string, string)

Рис. 2.2. Сначала выполняется функция инициализации init из пакета redis,
затем функция инициализации init из пакета main и, наконец, сама функция main

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

2.3. Ошибка #3  45

Мы также можем определить несколько функций init в одном исходном файле.
Например, такой код вполне допустим:
package main
import "fmt"
func init() {
fmt.Println("init
}
func init() {
fmt.Println("init
}

Первая функция init
1")

Вторая функция init
2")

func main() {
}

Первая выполненная функция init является первой в исходном порядке. Вот
вывод этого кода:
init 1
init 2

Мы также можем использовать функции инициализации init для реализации побочных эффектов. В следующем примере мы определяем пакет main,
который не имеет сильной зависимости от foo (например, нет прямого использования публичной функции — public function). Но в примере требуется,
чтобы пакет foo был инициализирован. Мы можем сделать это, используя
оператор _:
package main
import (
"fmt"
_ "foo"
)

Импортом foo достигается побочный эффект

func main() {
// ...
}

В этом случае пакет foo инициализируется перед main. Следовательно, функции
инициализации (init) в foo выполняются.
Другой аспект функции init в том, что ее нельзя вызвать напрямую, как в следующем примере:

46  Глава 2. Организация кода и проекта
package main
func init() {}
func main() {
init().
}

Этот код выдаст ошибку компиляции:
$ go build .
./main.go:6:2: undefined: init

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

2.3.2. Когда использовать функции init
Рассмотрим пример уместного использования: удержание пула соединений
с базой данных. В функции init открывается база данных с помощью sql.Open.
Мы задаем эту базу данных как глобальную переменную, которую позже могут
использовать другие функции:
var db *sql.DB
func init() {
dataSourceName :=
os.Getenv("MYSQL_DATA_SOURCE_NAME")
Переменная среды
d, err := sql.Open("mysql", dataSourceName)
if err != nil {
log.Panic(err)
}
err = d.Ping()
if err != nil {
log.Panic(err)
}
db = d
Определяет связь DB с глобальной переменной db
}

Мы открываем базу данных, проверяем, можем ли ее пропинговать, а затем
связываем ее с глобальной переменной. Что можно сказать о такой реализации?
Опишем три ее основных недостатка.
Прежде всего, обработка ошибок в функции инициализации носит ограниченный характер. Действительно, поскольку функция инициализации не
выдает сообщения об ошибках, единственный способ сообщить о возможной
ошибке — вызвать прерывание по panic, что приведет к остановке выполнения

2.3. Ошибка #3  47

приложения. В нашем примере остановить приложение можно в любом случае,
если не удается открыть базу данных. Но решение о такой остановке не обязательно должно приниматься самим пакетом. Возможно, вызывающая сторона
предпочла бы реализовать повторную попытку или использовать резервный
механизм. В этом случае открытие базы данных в функции инициализации не
позволяет клиентским пакетам реализовать свою логику обработки ошибок.
Другой важный недостаток связан с тестированием. Если мы добавим в этот
файл тесты, функция инициализации будет выполняться перед запуском тестовых случаев, что не обязательно будет тем, что нужно (например, если добавить
юнит-тесты в служебную функцию, которая не требует создания такой связи).
Поэтому функция init в этом примере усложняет написание юнит-тестов.
Последний недостаток заключается в том, что в примере требуется присвоить
пул соединений базы данных глобальной переменной. Глобальные переменные
имеют ряд серьезных недостатков, например:
Внутри пакета глобальные переменные могут изменяться любыми функциями.
Юнит-тесты могут быть более сложными, поскольку функция, зависящая от
глобальной переменной, больше не будет изолирована.
В большинстве случаев следует инкапсулировать переменную, а не сохранять
ее глобальной.
По этим причинам предыдущую инициализацию, скорее всего, лучше будет
обрабатывать как часть простой старой функции, например:
func createClient(dsn string) (*sql.DB, error)
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
Возвращается ошибка
}
if err = db.Ping(); err != nil {
return nil, err
}
return db, nil
}

Принимается имя источника данных
и возвращается *sql.DB и ошибка

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

48  Глава 2. Организация кода и проекта
Появляется возможность создать интеграционный тест для проверки, работает ли эта функция.
Пул соединений/связей инкапсулирован внутри этой функции.
Нужно ли любой ценой избегать функций инициализации? Не совсем. Есть
случаи, когда эти функции могут быть полезны. Например, официальный блог
Go (http://mng.bz/PW6w) использует функцию инициализации для настройки
статической конфигурации HTTP:
func init() {
redirect := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
}
http.HandleFunc("/blog", redirect)
http.HandleFunc("/blog/", redirect)

}

static := http.FileServer(http.Dir("static"))
http.Handle("/favicon.ico", static)
http.Handle("/fonts.css", static)
http.Handle("/fonts/", static)
http.Handle("/lib/godoc/", http.StripPrefix("/lib/godoc/",
http.HandlerFunc(staticHandler)))

В этом примере функция инициализации не может стать причиной сбоя (http.
HandleFunc может вызвать panic, но только если обработчик равен nil, чего в данном случае нет). При этом нет необходимости создавать какие-либо глобальные
переменные, и функция не повлияет на возможные юнит-тесты. Таким образом,
этот фрагмент кода представляет собой хороший пример того, где функции инициализации могут оказаться полезны. Подводя итог, мы увидели, что функции
инициализации могут привести к некоторым проблемам:
Они могут ограничивать возможности по обработке ошибок.
Они могут усложнить реализацию тестов (например, понадобится устанавливать внешнюю зависимость, которая в рамках юнит-тестов может и не
потребоваться).
Если инициализация требует, чтобы мы определили какое-то состояние, то
это нужно будет сделать через использование глобальных переменных.
Использовать функции инициализации нужно очень внимательно. Но они могут
быть полезны в некоторых ситуациях, например при определении статической
конфигурации, как мы увидели в этом разделе. В противном случае, как и просто в большинстве случаев, инициализацию следует обрабатывать с помощью
специальных функций.

2.4. Ошибка #4  49

2.4. ОШИБКА #4: ЗЛОУПОТРЕБЛЯТЬ ГЕТТЕРАМИ
И СЕТТЕРАМИ
Инкапсуляция данных в программировании означает сокрытие значений или состояния объекта. Геттеры и сеттеры — это средства для включения инкапсуляции
путем предоставления экспортированных методов поверх неэкспортированных
полей объектов.
В Go нет автоматической поддержки геттеров и сеттеров, как в других языках. Не
считается обязательным или идиоматичным использование геттеров и сеттеров
для доступа к полям структуры (struct). Например, стандартная библио­тека
реализует структуры, где некоторые поля доступны напрямую, как структура
time.Timer:
timer := time.NewTimer(time.Second)
как с числовыми типами для сравнения значений, так и со строками для сравнения их
лексического порядка.

144  Глава 3. Типы данных
В последнем примере код не скомпилировался, так как структура была составлена на основе типа, не подлежащего сравнению (среза).
Нужно знать и о возможных проблемах использования == и != с типами any.
Например, разрешено сравнение двух целых чисел, присвоенных типам any:
var a any = 3
var b any = 3
fmt.Println(a == b)

В результате этот код выведет true.
Но что, если мы инициализируем два типа customer (в последней версии, содержащей поле среза) и присваиваем значения типам any? Вот пример:
var cust1 any = customer{id: "x", operations: []float64{1.}}
var cust2 any = customer{id: "x", operations: []float64{1.}}
fmt.Println(cust1 == cust2)

Этот код компилируется. Но поскольку оба типа нельзя сравнивать, так
как структура customer содержит поле среза, выполнение кода приводит
к ошибке:
panic: runtime error: comparing uncomparable type main.customer

Имея в виду такое поведение, как можно сравнить два среза, две карты или две
структуры, содержащие не подлежащие сравнению типы? Если мы придерживаемся стандартной библиотеки, один из вариантов — использовать отражение
во время выполнения с пакетом reflect.
Отражение — это форма метапрограммирования, которая относится к способности приложения анализировать и изменять свою структуру и поведение. Например, в Go можно использовать Reflect.DeepEqual. Эта функция сообщает,
являются ли два элемента глубоко равными (deeply equal), рекурсивно обходя
два значения. Элементы, которые могут быть ее аргументами, являются базовыми типами, а также массивами, структурами, срезами, картами, указателями,
интерфейсами и функциями.

ПРИМЕЧАНИЕ Reflect.DeepEqual ведет себя определенным образом
в зависимости от типа, который мы задаем. Прежде чем использовать эту
функцию, внимательно прочитайте документацию.

3.13. Ошибка #29  145

Запустим код из первого примера еще раз, добавив Reflect.DeepEqual:
cust1 := customer{id: "x", operations: []float64{1.}}
cust2 := customer{id: "x", operations: []float64{1.}}
fmt.Println(reflect.DeepEqual(cust1, cust2))

Несмотря на то что структура customer содержит не подлежащие сравнению
типы (срез), она работает, как и ожидалось, выдавая значение true.
При использовании Reflect.DeepEqual важно помнить о двух вещах. Во-первых,
эта функция делает различие между пустой и нулевой коллекцией, как обсуждалось в рассмотрении ошибки #22 (путать пустые и нулевые срезы). Является ли
это проблемой? Не обязательно — всё зависит от конкретного случая. Например,
если нужно сравнить результаты двух операций демаршалинга (например, из
JSON в структуру Go), то может потребоваться подчеркнуть это различие. Стоит
помнить о таком поведении, чтобы эффективно использовать Reflect.DeepEqual.
Другая загвоздка довольно стандартна для большинства языков. Поскольку эта
функция использует отражение, которое интроспективно исследует значения
во время выполнения, чтобы узнать, как они формируются, у нее есть проблемы с производительностью. Выполнение нескольких локальных бенчмарков
со структурами разного размера показывает, что в среднем Reflect.DeepEqual
примерно в 100 раз медленнее, чем ==. Это может быть причиной, по которой
лучше использовать ее в контексте тестирования, а не во время выполнения.
Если производительность — решающий фактор, другим вариантом может быть
реализация собственного метода сравнения. Вот пример, который сравнивает
две структуры customer и возвращает результат логического типа:
func (a customer) equal(b customer) bool {
if a.id != b.id {
Проводится сравнение полей id
return false
}
if len(a.operations) != len(b.operations) {
return false
}
for i := 0; i < len(a.operations); i++ {
if a.operations[i] != b.operations[i] {
return false
}
}
return true
}

Проверяется длина
обоих срезов

Проводится сравнение
каждого элемента
обоих срезов

146  Глава 3. Типы данных
Здесь мы создаем собственный метод сравнения со своими способами проверки
различных полей структуры customer. Запуск локального бенчмарка на срезе,
состоящем из 100 элементов, показывает, что этот метод equal примерно в 96 раз
быстрее, чем Reflect.DeepEqual.
Нужно помнить, что применение оператора == довольно ограниченно. Например,
он не работает со срезами и картами. В большинстве случаев задача сравнения
решается использованием Reflect.DeepEqual, но основным недостатком становится снижение производительности. В контексте юнит-тестов возможны другие
варианты: использование внешних библиотек с go-cmp (https://github.com/google/
go-cmp) или testify (https://github.com/stretchr/testify).
Но если при выполнении кода важна производительность, то использование
собственного метода может оказаться лучшим решением.
Важно помнить, что в стандартной библиотеке уже есть некоторые методы
сравнения. Например, можно использовать оптимизированную функцию bytes.
Compare для сравнения двух срезов байтов. Перед использованием собственного
метода убедитесь, что не занимаетесь велосипедостроением.

ИТОГИ
При чтении существующего кода имейте в виду, что целочисленные литералы,
начинающиеся с 0, являются восьмеричными числами. Для удобочитаемости
делайте восьмеричные целые числа явными, добавляя к ним префикс 0o.
Поскольку целочисленные переполнения и антипереполнения в Go обрабатываются автоматически, можно реализовать собственные функции для их
обнаружения.
Выполнение сравнений чисел с плавающей точкой по принципу попадания их
разницы в пределы заданной дельты может обеспечивать переносимость кода.
При выполнении операций сложения или вычитания для повышения точности группируйте операции с числами, переменными, значениями одинаковых порядков величины. Сначала делайте умножение и деление, а потом
сложение и вычитание.
Понимание разницы между длиной и емкостью среза очень важно в разработке на Go. Длина среза — это количество доступных в нем элементов,
а емкость — количество элементов в резервном массиве.
При создании среза инициализируйте его с заданной длиной или емкостью,
если его длина заранее известна. Это уменьшает количество операций по ре-

Итоги  147

зервированию места в памяти и повышает производительность. Та же логика
применима и к картам: при инициализации задайте их размер.
Использование копирования или полного выражения среза — это способ
предотвратить возникновение конфликтов при использовании функции
append, если две разные функции используют срезы с одним и тем же резервным массивом. Но только создание копии среза предотвращает утечку
памяти, если вы хотите уменьшить срез большого размера.
Если вы хотите скопировать один срез в другой с помощью встроенной
функции copy, помните, что количество копируемых элементов соответствует
минимуму из длин двух этих срезов.
Работая со срезом указателей или со структурами с полями указателей,
можно избежать утечек памяти, сделав исключенные операцией нарезки
элементы равными nil.
Чтобы избежать часто возникающей путаницы при использовании пакетов
encoding/json или reflect, нужно понимать разницу между нулевыми и пустыми срезами. Они оба являются срезами нулевых длины и емкости, но
только нулевой срез не требует для себя выделения места в памяти.
Чтобы убедиться, что срез вообще не содержит элементов, проверьте его
длину. Эта проверка работает независимо от того, нулевой срез или пустой.
То же самое касается и карт.
Для разработки однозначных API не следует проводить различие между
нулевыми и пустыми срезами.
Карта в памяти всегда может увеличиваться в размере, но никогда не уменьшается. И если это приводит к проблемам с памятью, попробуйте разные варианты действий: например, принудительно пересоздавать карты с помощью
внутренних средств Go или использовать указатели.
Для сравнения типов в Go используйте операторы == и !=, если два типа можно сравнивать в принципе: логические значения, числа, строки, указатели,
каналы и структуры, полностью состоящие из сопоставимых друг с другом
типов. В противном случае можно использовать Reflect.DeepEqual и заплатить цену за отражение либо использовать пользовательские реализации
и библиотеки.

4

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

В этой главе:

33 Как цикл с ключевым словом range присваивает значения
элементам и оценивает заданное выражение
33 Работа с циклами и указателями с range
33 Предотвращение типичных ошибок итераций карт и нарушения
выполнения цикла
33 Использование функции defer внутри цикла

Управляющие структуры в Go одновременно и похожи на аналогичные в C или
Java, и отличаются от них. Например, в Go нет циклов do или while, а есть только
обобщенный цикл for. Рассмотрим типичные ошибки, связанные с управляющими структурами, и особое внимание уделим ключевому слову range, которое
часто понимают неверно.

4.1. ОШИБКА #30: ИГНОРИРОВАТЬ ТО,
ЧТО ЭЛЕМЕНТЫ В ЦИКЛЕ RANGE КОПИРУЮТСЯ
range — это удобный способ итераций по различным структурам данных.

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

4.1. Ошибка #30  149

Но Go-разработчики могут забыть или не знать, как range присваивает значения,
что приводит к распространенным ошибкам. Об этом и поговорим далее.

4.1.1. Концепция
Цикл range позволяет проводить итерации по различным структурам данных:
строкам;
массивам;
указателям на массивы;
срезам;
картам;
принимающим каналам.
По сравнению с классическим циклом for цикл с range — это удобный способ
перебора всех элементов одной из этих структур данных благодаря лаконичному синтаксису. Он также в меньшей степени подвержен ошибкам, поскольку
не требует обрабатывать условия и переменную цикла вручную, что позволяет
избежать ошибки на единицу (off-by-one error). Вот пример с итерацией по
срезу строк:
s := []string{"a", "b", "c"}
for i, v := range s {
fmt.Printf("index=%d, value=%s\n", i, v)
}

Этот код перебирает каждый элемент среза. На каждой итерации, когда мы
перебираем срез, range создает пару значений: индекс и значение элемента, присваиваемые в i и v соответственно. Как правило, range создает два значения для
каждой структуры данных, кроме принимающего канала, для которого создает
только один элемент (значение).
В некоторых случаях нужно только значение элемента, а не его индекс. Так
как неиспользование локальной переменной приводит к ошибке компиляции,
можно использовать пустой идентификатор для замены индексной переменной:
s := []string{"a", "b", "c"}
for _, v := range s {
fmt.Printf("value=%s\n", v)
}

150  Глава 4. Управляющие структуры
Благодаря пустому идентификатору мы перебираем каждый элемент, игнорируя
его индекс и присваивая в v только значение элемента.
Если же значение нас не интересует, второй элемент можно опустить:
for i := range s {}

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

4.1.2. Копия значения
Чтобы эффективно использовать range, важно понимать, как во время каждой
итерации обрабатывается значение. Рассмотрим пример.
Создадим структуру account, содержащую единственное поле balance:
type account struct {
balance float32
}

Затем создадим срез структуры account и переберем каждый элемент, используя
цикл range. Во время каждой итерации мы увеличиваем balance для каждой
структуры account:
accounts := []account{
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
for _, a := range accounts {
a.balance += 1000
}

Как вы думаете, какой из этих двух вариантов показывает содержимое среза
в соответствии с приведенным кодом?
[{100} {200} {300}]
[{1100} {1200} {1300}]

Правильный ответ — [{100} {200} {300}]. В этом примере range не влияет на
содержимое среза.

4.1. Ошибка #30  151

В Go все, что мы присваиваем, является копией:
Если мы присваиваем результат выполнения функции, возвращающей
структуру, Go создает копию этой структуры.
Если мы присваиваем результат выполнения функции, возвращающей указатель, Go создает копию адреса памяти (в 64-битной архитектуре адрес
имеет длину 64 бита).
Об этом важно помнить, чтобы избежать типичных ошибок, в том числе связанных с циклами range. Когда range совершает итерацию по структуре данных,
выполняется копирование каждого элемента в переменную-значение (второй
элемент).
Вернемся к нашему примеру: перебор каждого элемента account приводит к тому,
что копия структуры присваивается переменной значения a. Следовательно,
увеличение balance с помощью a.balance += 1000 изменяет только переменную
значения (a), а не элемент в срезе.
Что будет, если нужно обновить элементы среза? Есть два варианта получить
доступ к элементу с помощью индекса среза. Этого можно добиться либо с помощью классического цикла for, либо с помощью цикла range, используя индекс
вместо переменной значения:
for i := range accounts {
Используется переменная индекса
accounts[i].balance += 1000
для доступа к элементу среза
}
for i := 0; i < len(accounts); i++ {
Используется традиционный цикл for
accounts[i].balance += 1000
}

Оба этих варианта приводят к одинаковому эффекту: к обновлению элементов
в срезе accounts.
Какой из них предпочтительнее? Зависит от контекста. Если нужно просмотреть
каждый элемент, первый цикл будет короче для записи и чтения. Если же нужно
проконтролировать, какой конкретно элемент мы хотим обновить (например,
один из двух), то следует использовать второй цикл.
Помните, что элемент значения в цикле range является копией. Поэтому если
значение представляет собой структуру, которую нужно изменить, мы будем
обновлять только копию, а не сам элемент (при условии, что модифицируемое

152  Глава 4. Управляющие структуры
значение или поле не являются указателем). Предпочтительным вариантом
является доступ к элементу через индекс с использованием цикла range или
классического цикла for.
Обновление элементов среза: третий вариант
Другой вариант: продолжить использовать цикл range и получить доступ к значению, но изменить тип среза на срез указателей account:
accounts := []*account{
Обновление типа среза до []*account
{balance: 100.},
{balance: 200.},
{balance: 300.},
}
for _, a := range accounts {
a.balance += 1000
Прямое обновление элементов среза
}

Как мы уже говорили, переменная a является копией указателя account, хранящегося в срезе. Но поскольку оба указателя ссылаются на одну и ту же структуру,
оператор a.balance += 1000 обновляет элемент среза.
У этого варианта есть два недостатка. Во-первых, требуется обновить тип среза,
что не всегда возможно. Во-вторых, если важна производительность, то итерация
по срезу указателей может быть менее эффективной для центрального процессора из-за отсутствия предсказуемости (обсудим этот момент в ошибке #91
(не понимать устройство кэша CPU)).

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

4.2. ОШИБКА #31: ИГНОРИРОВАТЬ ТО,
КАК В ЦИКЛАХ RANGE ВЫЧИСЛЯЮТСЯ
АРГУМЕНТЫ
Синтаксис цикла range требует наличия выражения. Например, в цикле for i,
v := range exp, exp — это выражение. Как мы видели, это может быть строка,
массив, указатель на массив, срез, карта или канал. Теперь поговорим о том,
как вычисляется это выражение. Это важный момент, позволяющий избежать
многих типичных ошибок.

4.2. Ошибка #31  153

Рассмотрим пример, где к срезу добавляется элемент, по которому мы выполняем
итерацию. Как вы считаете, завершится ли этот цикл?
s := []int{0, 1, 2}
for range s {
s = append(s, 10)
}

Чтобы понять суть, следует помнить, что при использовании цикла range указываемое выражение вычисляется только один раз — перед началом цикла.
В этом контексте слово «вычисляется» означает, что предоставленное выражение
копируется во временную переменную, а затем цикл range выполняет итерации
над этой переменной. В этом примере при вычислении выражения s результатом
будет копия среза, как показано на рис. 4.1.
s
Указатель
(ptr)

0

1

2

Длина (len)
3
Емкость (cap) копия range
3
Указатель
(ptr)
Длина (len)
3

Рис. 4.1. s копируется
во временную переменную,
    используемую в цикле range

Емкость (cap)
3

Цикл range использует эту временную переменную. Исходный срез s также
обновляется во время каждой итерации. Следовательно, после трех итераций
состояние будет таким, как на рис. 4.2.
s
Указатель
(ptr)

0

1

2

10

10

10

Длина (len)
6
Емкость (cap)
6

копия range
Указатель
(ptr)
Длина (len)
3
Емкость (cap)
3

Рис. 4.2. Временная
переменная остается срезом
длиной 3, поэтому итерации
    прекращаются

154  Глава 4. Управляющие структуры
Каждый шаг приводит к добавлению нового элемента. Но за три шага мы прошлись по всем его элементам. Длина временного среза, используемого в range,
остается равна 3, поэтому цикл завершается после трех итераций.
Такое поведение отличается от классического цикла for:
s := []int{0, 1, 2}
for i := 0; i < len(s); i++ {
s = append(s, 10)
}

В этом примере цикл никогда не закончится. Значение выражения len(s) вычисляется во время каждой итерации, и раз мы продолжаем добавлять элементы, то никогда не достигнем состояния завершения цикла. Чтобы правильно
использовать циклы в Go, важно помнить об этой разнице.
При использовании range помните, что вышеописанное поведение (выражение
вычисляется только один раз) также применимо ко всем типам данных. В качестве примера посмотрим на последствия такого поведения для двух других
типов: каналов и массивов.

4.2.1. Каналы
Рассмотрим пример, где цикл range осуществляет итерации по каналу. Мы создаем две горутины, каждая из которых отправляет элементы в два канала. Затем
в родительской горутине реализуем потребителя на одном канале, используя
цикл range, который пытается переключиться на другой канал во время выполнения цикла:
ch1 := make(chan int, 3)
go func() {
ch1