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

Pro Go. Полное руководство по программированию надежного и эффективного программного обеспечения с использованием Golang [Адам Фриман] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]
Адам Фриман

Pro Go
Полное руководство по программированию
надежного и эффективного программного
обеспечения с использованием Golang

ISBN 978-1-4842-7354-8

e-ISBN 978-1-4842-7355-5

Посвящается моей любимой жене Джеки Гриффит.
(А также Арахису.)

Любой исходный код или другие дополнительные материалы, на
которые ссылается автор в этой книге, доступны читателям на GitHub.
Для получения более подробной информации посетите сайт
www.apress.com/source-code.

Оглавление
Часть I: Понимание языка Go
Глава 1: Ваше первое приложение Go
Настройка сцены
Установка средств разработки
Установка Git
Выбор редактора кода
Создание проекта
Определение типа данных и коллекции
Создание HTML-шаблонов
Загрузка шаблонов
Создание обработчиков HTTP и сервера
Написание функции обработки формы
Обработка данных формы
Добавление проверки данных
Резюме
Глава 2: Go в контексте
Почему вам стоит изучать Go?
В чем подвох?
Это действительно настолько плохо?
Что вы должны знать?
Какова структура этой книги?
Часть 1: Понимание языка Go
Часть 2: Использование стандартной библиотеки Go
Часть 3: Применение Go
Что не охватывает эта книга?

Что делать, если вы нашли ошибку в книге?
Много ли примеров?
Какое программное обеспечение вам нужно для примеров?
На каких платформах будут работать примеры?
Что делать, если у вас возникли проблемы с примерами?
Где взять пример кода?
Почему некоторые примеры имеют странное форматирование?
Как связаться с автором?
Что, если мне действительно понравилась эта книга?
Что, если эта книга меня разозлила, и я хочу пожаловаться?
Резюме
Глава 3: Использование инструментов Go
Использование команды Go
Создание проекта Go
Понимание объявления пакета
Понимание оператора импорта
Понимание функции
Понимание оператора кода
Компиляция и запуск исходного кода
Очистка
Использование команды go run
Определение модуля
Отладка кода Go
Подготовка к отладке
Использование отладчика
Использование подключаемого модуля редактора Delve

Линтинг Go-кода
Использование линтера
Отключение правил линтера
Исправление распространенных проблем в коде Go
Форматирование кода Go
Резюме
Глава 4. Основные типы, значения и указатели
Подготовка к этой главе
Использование стандартной библиотеки Go
Понимание основных типов данных
Понимание литеральных значений
Использование констант
Понимание нетипизированных констант
Определение нескольких констант с помощью одного
оператора
Пересмотр литеральных значений
Использование переменных
Пропуск типа данных переменной
Пропуск присвоения значения переменной
Определение нескольких переменных с помощью одного
оператора
Использование краткого синтаксиса объявления
переменных
Использование пустого идентификатора
Понимание указателей
Определение указателя
Следование указателю

Понимание нулевых значений указателя
Указывание на указатели
Понимание того, почему указатели полезны
Резюме
Глава 5: Операции и преобразования
Подготовка к этой главе
Понимание операторов Go
Понимание операторов Go
Объединение строк
Понимание операторов сравнения
Понимание логических операторов
Преобразование, анализ и форматирование значений
Выполнение явных преобразований типов
Преобразование значений с плавающей запятой в целые
числа
Парсинг из строк
Форматирование значений как строк
Резюме
Глава 6: Управление потоком
Подготовка к этой главе
Понимание управления потоком выполнения
Использование ключевого слова else
Использование ключевого слова else
Понимание области действия оператора if
Использование оператора инициализации с оператором if
Использование циклов for

Включение условия в цикл
Использование операторов инициализации и завершения
Продолжение цикла
Перечисление последовательностей
Использование операторов switch
Сопоставление нескольких значений
Принудительный переход к следующему оператору case
Предоставление пункта по умолчанию
Использование оператора инициализации
Исключение значения сравнения
Использование операторов меток
Резюме
Глава 7: Использование массивов, срезов и карт
Подготовка к этой главе
Работа с массивами
Использование литерального синтаксиса массива
Понимание типов массивов
Понимание значений массива
Сравнение массивов
Перечисление массива
Работа со срезами
Добавление элементов в срез
Добавление одного среза к другому
Создание срезов из существующих массивов
Указание емкости при создании среза из массива
Создание срезов из других срезов

Использование функции копирования
Удаление элементов среза
Перечисление срезов
Сортировка срезов
Сравнение срезов
Получение массива, лежащего в основе среза
Работа с картами
Использование литерального синтаксиса карты
Проверка элементов в карте
Удаление объектов с карты
Перечисление содержимого карты
Понимание двойной природы строк
Преобразование строки в руны
Перечисление строк
Резюме
Глава 8: Определение и использование функций
Подготовка к этой главе
Определение простой функции
Определение и использование параметров функции
Пропуск типов параметров
Пропуск имен параметров
Определение и использование результатов функции
Использование указателей в качестве параметров функций
Определение и использование результатов функции
Возврат функцией нескольких результатов
Использование ключевого слова defer

Резюме
Глава 9: Использование типов функций
Подготовка к этой главе
Понимание типов функций
Понимание сравнения функций и нулевого типа
Использование функций в качестве аргументов
Использование функций в качестве результатов
Создание псевдонимов функциональных типов
Использование литерального синтаксиса функции
Понимание области действия функциональной переменной
Непосредственное использование значений функций
Понимание замыкания функции
Резюме
Глава 10: Определение структур
Подготовка к этой главе
Определение и использование структуры
Создание структурных значений
Использование значения структуры
Частичное присвоение значений структуры
Использование позиций полей для создания значений
структуры
Определение встроенных полей
Сравнение значений структуры
Определение анонимных типов структур
Создание массивов, срезов и карт, содержащих структурные
значения
Понимание структур и указателей

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

Резюме
Глава 12: Создание и использование пакетов
Подготовка к этой главе
Понимание файла модуля
Создание пользовательского пакета
Использование пользовательского пакета
Понимание управления доступом к пакетам
Добавление файлов кода в пакеты
Разрешение конфликтов имен пакетов
Создание вложенных пакетов
Использование функций инициализации пакета
Использование внешних пакетов
Управление внешними пакетами
Резюме
Глава 13: Тип и состав интерфейса
Подготовка к этой главе
Понимание композиции типов
Определение базового типа
Типы композиций
Создание цепочки вложенных типов
Использование нескольких вложенных типов в одной и той
же структуре
Понимание, когда продвижение не может быть выполнено
Понимание композиции и интерфейсов
Использование композиции для реализации интерфейсов
Составление интерфейсов

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

Паника после восстановления
Восстановление после паники в горутинах
Резюме
Часть II: Использование стандартной библиотеки Go
Глава 16. Обработка строк и регулярные выражения
Подготовка к этой главе
Обработка строк
Сравнение строк
Преобразование регистра строк
Работа с регистром символов
Проверка строк
Манипулирование строками
Обрезка строк
Изменение строк
Построение и генерация строк
Использование регулярных выражений
Компиляция и повторное использование шаблонов
Разделение строк с помощью регулярного выражения
Использование подвыражений
Замена подстрок с помощью регулярного выражения
Резюме
Глава 17: Форматирование и сканирование строк
Подготовка к этой главе
Написание строк
Форматирование строк
Понимание глаголов форматирования

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

Представление продолжительности
Использование функций времени для горутин и каналов
Перевод горутины в сон
Отсрочка выполнения функции
Получение уведомлений по времени
Получение повторяющихся уведомлений
Резюме
Глава 20:Чтение и запись данных
Подготовка к этой главе
Понимание средств чтения и записи
Понимание средств чтения
Понимание средств записи
Использование служебных функций для программ чтения и
записи
Использование специализированных средств чтения и записи
Использование пайпов
Объединение нескольких средств чтения
Объединение нескольких средств записи
Повторение данных чтения во Writer
Ограничение чтения данных
Буферизация данных
Использование дополнительных методов буферизованного
чтения
Выполнение буферизованной записи
Форматирование и сканирование с помощью средств чтения и
записи
Сканирование значений из считывателя

Запись отформатированных строк в Writer
Использование Replacer с Writer
Резюме
Глава 21: Работа с данными JSON
Подготовка к этой главе
Чтение и запись данных JSON
Кодирование данных JSON
Декодирование данных JSON
Резюме
Глава 22: Работа с файлами
Подготовка к этой главе
Чтение файлов
Использование функции удобства чтения
Использование файловой структуры для чтения файла
Запись в файлы
Использование функции удобства записи
Использование файловой структуры для записи в файл
Запись данных JSON в файл
Использование удобных функций для создания новых файлов
Работа с путями к файлам
Управление файлами и каталогами
Изучение файловой системы
Определение существования файла
Поиск файлов с помощью шаблона
Обработка всех файлов в каталоге
Резюме

Глава 23: Использование HTML и текстовых шаблонов
Подготовка к этой главе
Создание HTML-шаблонов
Загрузка и выполнение шаблонов
Понимание действий шаблона
Создание текстовых шаблонов
Резюме
Глава 24: Создание HTTP-серверов
Подготовка к этой главе
Создание простого HTTP-сервера
Создание прослушивателя и обработчика HTTP
Проверка запроса
Фильтрация запросов и генерация ответов
Использование удобных функций ответа
Использование обработчика удобной маршрутизации
Поддержка HTTPS-запросов
Создание статического HTTP-сервера
Создание статического маршрута к файлу
Использование шаблонов для генерации ответов
Ответ с данными JSON
Обработка данных формы
Чтение данных формы из запросов
Чтение составных форм
Чтение и настройка файлов cookie
Резюме
Глава 25: Создание HTTP-клиентов

Подготовка к этой главе
Отправка простых HTTP-запросов
Отправка POST-запросов
Настройка запросов HTTP-клиента
Использование удобных функций для создания запроса
Работа с файлами cookie
Управление перенаправлениями
Создание составных форм
Резюме
Глава 26: Работа с базами данных
Подготовка к этой главе
Подготовка базы данных
Установка драйвера базы данных
Открытие базы данных
Выполнение операторов и запросов
Запрос нескольких строк
Выполнение операторов с заполнителями
Выполнение запросов для отдельных строк
Выполнение других запросов
Использование подготовленных операторов
Использование транзакций
Использование рефлексии для сканирования данных в
структуру
Резюме
Глава 27: Использование рефлексии
Подготовка к этой главе

Понимание необходимости рефлексии
Использование рефлексии
Использование основных функций типа
Использование базовых возможностей Value
Определение типов
Идентификация байтовых срезов
Получение базовых значений
Установка Value с использованием рефлексии
Установка одного Value с помощью другого
Сравнение Value
Использование удобной функции сравнения
Преобразование значений
Преобразование числовых типов
Создание новых значений
Резюме
Глава 28: Использование рефлексии, часть 2
Подготовка к этой главе
Работа с указателями
Работа со значениями указателя
Работа с типами массивов и срезов
Работа со значениями массива и среза
Перечисление срезов и массивов
Создание новых срезов из существующих срезов
Создание, копирование и добавление элементов в срезы
Работа с типами карт
Работа со значениями карты

Установка и удаление значений карты
Создание новых карт
Работа с типами структур
Обработка вложенных полей
Поиск поля по имени
Проверка тегов структуры
Создание типов структур
Работа со структурными значениями
Установка значений поля структуры
Резюме
Глава 29: Использование рефлексии, часть 3
Подготовка к этой главе
Работа с типами функций
Работа со значениями функций
Создание и вызов новых типов функций и значений
Работа с методами
Вызов методов
Работы с интерфейсами
Получение базовых значений из интерфейсов
Изучение методов интерфейса
Работа с типами каналов
Работа со значениями канала
Создание новых типов и значений каналов
Выбор из нескольких каналов
Резюме
Глава 30: Координация горутин

Подготовка к этой главе
Использование групп ожидания
Использование взаимного исключения
Использование мьютекса чтения-записи
Использование условий для координации горутин
Обеспечение однократного выполнения функции
Использование контекстов
Отмена запроса
Установка крайнего срока
Предоставление данных запроса
Резюме
Глава 31. Модульное тестирование, бенчмаркинг и логирование
Подготовка к этой главе
Использование тестирования
Запуск модульных тестов
Управление выполнением теста
Код бенчмаркинга
Удаление установки из теста
Выполнение суббенчмаркингов
Журналирование ланных
Создание пользовательских регистраторов
Резюме
Часть III: Применение Go
Глава 32: Создание веб-платформы
Создание проекта
Создание некоторых основных функций платформы

Создание системы ведения журнала
Создание системы конфигурации
Управление службами с внедрением зависимостей
Определение жизненных циклов сервиса
Определение внутренних сервисных функций
Определение функций регистрации службы
Определение функций разрешения службы
Регистрация и использование сервисов
Резюме
Глава 33. ПО промежуточного слоя, шаблоны и обработчики
Создание конвейера запросов
Определение интерфейса компонента промежуточного
программного обеспечения
Создание конвейера запросов
Создание базовых компонентов
Создание HTTP-сервера
Настройка приложения
Оптимизация разрешения сервиса
Создание HTML-ответов
Создание макета и шаблона
Реализация выполнения шаблона
Создание и использование службы шаблонов
Знакомство с обработчиками запросов
Генерация URL-маршрутов
Подготовка значений параметров для метода обработчика
Сопоставление запросов с маршрутами

Резюме
Глава 34: Действия, сеансы и авторизация
Представляем результаты действий
Определение общих результатов действий
Обновление заполнителей для использования результатов
действий
Вызов обработчиков запросов из шаблонов
Обновление обработки запросов
Настройка приложения
Создание URL-адресов из маршрутов
Создание службы генератора URL
Определение альтернативных маршрутов
Проверка данных запроса
Выполнение проверки данных
Добавление сеансов
Отсрочка записи данных ответа
Создание интерфейса сеанса, службы и промежуточного
программного обеспечения
Создание обработчика, использующего сеансы
Настройка приложения
Добавление авторизации пользователя
Определение основных типов авторизации
Реализация интерфейсов платформы
Реализация контроля доступа
Реализация функций заполнителя приложения
Создание обработчика аутентификации
Настройка приложения

Резюме
Глава 35: SportsStore: настоящее приложение
Создание проекта SportsStore
Настройка приложения
Запуск модели данных
Отображение списка продуктов
Реализация (временного) репозитория
Отображение списка продуктов
Создание шаблона и макета
Настройка приложения
Добавление пагинации
Стилизация содержимого шаблона
Установка CSS-файла Bootstrap
Обновление макета
Стилизация содержимого шаблона
Добавление поддержки фильтрации категорий
Обновление обработчика запросов
Создание обработчика категории
Отображение навигации по категориям в шаблоне списка
товаров
Регистрация обработчика и обновление псевдонимов
Резюме
Глава 36: SportsStore: корзина и база данных
Создание корзины покупок
Определение модели корзины и репозитория
Создание обработчика запроса корзины

Добавление товаров в корзину
Настройка приложения
Добавление виджета «Сводка корзины»
Использование репозитория базы данных
Создание типов репозиториев
Открытие базы данных и загрузка команд SQL
Определение начального числа и операторов
инициализации
Определение основных запросов
Определение постраничных запросов
Определение службы репозитория SQL
Настройка приложения для использования репозитория
SQL
Резюме
Глава 37: SportsStore: оформление заказа и администрирование
Создание процесса оформления заказа
Определение модели
Расширение репозитория
Отключение временного репозитория
Определение методов и команд репозитория
Создание обработчика запросов и шаблонов
Создание функций администрирования
Создание функции администрирования продукта
Создание функции администрирования категорий
Резюме
Глава 38: SportsStore: отделка и развертывание
Завершение функций администрирования

Расширение репозитория
Реализация обработчиков запросов
Создание шаблонов
Ограничение доступа к функциям администрирования
Создание пользовательского хранилища и обработчика
запросов
Настройка приложения
Создание веб-службы
Подготовка к развертыванию
Установка сертификатов
Настройка приложения
Сборка приложения
Установка рабочего стола Docker
Creating the Docker Configuration Files
Резюме

Об авторе
Адам Фриман
Опытный
ИТ-специалист,
который
занимал руководящие должности в ряде
компаний, в последнее время —
технический директор и главный
операционный директор глобального
банка. Теперь на пенсии, он тратит свое
время на написание книг и бег на
длинные дистанции.

О техническом рецензенте
Фабио Клаудио Ферраккиати
Является старшим консультантом и старшим аналитиком/
разработчиком, использующим технологии Microsoft. Он работает на
BluArancio (www.bluarancio.com). Он является сертифицированным
разработчиком решений Microsoft для .NET, сертифицированным
разработчиком приложений Microsoft для .NET, сертифицированным
специалистом Microsoft, а также плодовитым автором и техническим
обозревателем. За последние десять лет он написал статьи для
итальянских и международных журналов и стал соавтором более
десяти книг по различным компьютерным темам.

Часть I
Понимание языка Go

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

Настройка сцены
Представьте, что подруга решила устроить вечеринку в канун Нового года и попросила меня
создать веб-приложение, которое позволяет ее приглашенным в электронном виде отвечать на
вопросы. Она попросила эти ключевые функции:
Домашняя страница с информацией о вечеринке
Форма, которую можно использовать для RSVP, которая будет отображать страницу
благодарности
Проверка заполнения формы RSVP
Сводная страница, которая показывает, кто придет на вечеринку
В этой главе я создаю проект Go и использую его для создания простого приложения, которое
содержит все эти функции.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех остальных глав этой книги
— с https://github.com/apress/pro-go. См. Главу 2 о том, как получить помощь, если у вас
возникнут проблемы с запуском примеров.

Установка средств разработки
Первым шагом является установка инструментов разработки Go. Перейдите на
https://golang.org/dl и загрузите установочный файл для вашей операционной системы.
Установщики доступны для Windows, Linux и macOS. Следуйте инструкциям по установке,
которые можно найти по адресу https://golang.org/doc/install для вашей платформы. Когда
вы завершите установку, откройте командную строку и выполните команду, показанную в
листинге 1-1, которая подтвердит, что инструменты Go были установлены, распечатав версию
пакета.
ОБНОВЛЕНИЯ ЭТОЙ КНИГИ
Go активно разрабатывается, и существует постоянный поток новых выпусков, а это значит,
что к тому времени, когда вы будете читать эту книгу, может быть доступна более поздняя
версия. Go имеет прекрасную политику поддержки совместимости, поэтому у вас не должно
возникнуть проблем с примерами из этой книги, даже в более поздних версиях. Если у вас
возникнут
проблемы,
см.
репозиторий
этой
книги
на
GitHub,
https://github.com/apress/pro-go, где я буду публиковать бесплатные обновления,
устраняющие критические изменения.
Для меня (и для Apress) обновление такого рода является продолжающимся экспериментом,
и оно продолжает развиваться — не в последнюю очередь потому, что я не знаю, что будет

содержать будущие версии Go. Цель состоит в том, чтобы продлить жизнь этой книги,
дополнив содержащиеся в ней примеры.
Я не даю никаких обещаний относительно того, какими будут обновления, какую форму
они примут или как долго я буду их выпускать, прежде чем включить их в новое издание этой
книги. Пожалуйста, будьте непредвзяты и проверяйте репозиторий этой книги при выпуске
новых версий. Если у вас есть идеи о том, как можно улучшить обновления, напишите мне по
адресу adam@adam-freeman.com и дайте мне знать.
go version
Листинг 1-1 Проверка установки Go

Текущая версия на момент написания статьи — 1.17.1, что приводит к следующему выводу
на моем компьютере с Windows:
go version go1.17.1 windows/amd64
Неважно, видите ли вы другой номер версии или другую информацию об операционной
системе — важно то, что команда go работает и выдает результат.

Установка Git
Некоторые команды Go полагаются на систему контроля версий Git. Перейдите на https://gitscm.com и следуйте инструкциям по установке для вашей операционной системы.

Выбор редактора кода
Единственный другой шаг — выбрать редактор кода. Файлы исходного кода Go представляют
собой обычный текст, что означает, что вы можете использовать практически любой редактор.
Однако некоторые редакторы предоставляют специальную поддержку для Go. Наиболее
популярным выбором является Visual Studio Code, который можно использовать бесплатно и
который поддерживает новейшие функции языка Go. Visual Studio Code — это редактор, который
я рекомендую, если у вас еще нет предпочтений. Visual Studio Code можно загрузить с
http://code.visualstudio.com, и существуют установщики для всех популярных операционных
систем. Вам будет предложено установить расширения Visual Studio Code для Go, когда вы
начнете работу над проектом в следующем разделе.
Если вам не нравится код Visual Studio, вы можете найти список доступных опций по адресу
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins. Для выполнения примеров из
этой книги не требуется специального редактора кода, и все задачи, необходимые для создания и
компиляции проектов, выполняются в командной строке.

Создание проекта
Откройте командную строку, перейдите в удобное место и создайте папку с именем
partyinvites. Перейдите в папку partyinvites и выполните команду, показанную в листинге 12, чтобы запустить новый проект Go.
go mod init partyinvites
Листинг 1-2 Запуск проекта Go

Команда go используется почти для каждой задачи разработки, как я объясню в Главе 3. Эта
команда создает файл с именем go.mod, который используется для отслеживания пакетов, от
которых зависит проект, а также может использоваться для публикации проекта, если необходимо.

Файлы кода Go имеют расширение .go. Используйте выбранный вами редактор для создания
файла с именем main.go в папке partyinvites с содержимым, показанным в листинге 1-3. Если
вы используете Visual Studio Code и впервые редактируете файл Go, вам будет предложено
установить расширения, поддерживающие язык Go.
package main
import "fmt"
func main() {
fmt.Println("TODO: add some features")
}
Листинг 1-3 Содержимое файла main.go в папке partyinvites

Синтаксис Go будет вам знаком, если вы использовали любой C или C-подобный язык,
например C# или Java. В этой книге я подробно описываю язык Go, но вы можете многое понять,
просто взглянув на ключевые слова и структуру кода в листинге 1-3.
Функции сгруппированы в пакеты (package), поэтому в листинге 1-3 есть оператор пакета.
Зависимости пакетов создаются с помощью оператора импорта, который позволяет получить
доступ к функциям, которые они используют, в файле кода. Операторы сгруппированы в функции,
которые определяются с помощью ключевого слова func. В листинге 1-3 есть одна функция,
которая называется main. Это точка входа для приложения, что означает, что это точка, с которой
начнется выполнение, когда приложение будет скомпилировано и запущено.
Функция main содержит один оператор кода, который вызывает функцию с именем Println,
предоставляемую пакетом с именем fmt. Пакет fmt является частью обширной стандартной
библиотеки Go, описанной во второй части этой книги. Функция Println выводит строку
символов.
Хотя детали могут быть незнакомы, назначение кода в листинге 1-3 легко понять: когда
приложение выполняется, оно выводит простое сообщение. Запустите команду, показанную в
листинге 1-4, в папке partyinvites, чтобы скомпилировать и выполнить проект. (Обратите
внимание, что в этой команде после слова run стоит точка.)
go run .
Листинг 1-4 Компиляция и выполнение проекта

Команда go run полезна во время разработки, поскольку выполняет задачи компиляции и
выполнения за один шаг. Приложение выдает следующий вывод:
TODO: add some features
Если вы получили ошибку компилятора, вероятно, причина в том, что вы не ввели код точно
так, как показано в листинге 1-3. Go настаивает на том, чтобы код определялся определенным
образом. Вы можете предпочесть, чтобы открывающие фигурные скобки отображались на
отдельной строке, и вы могли автоматически отформатировать код таким образом, как показано в
листинге 1-5.
package main
import "fmt"
func main()
{
fmt.Println("TODO: add some features")
}

Листинг 1-5 Ставим фигурную скобку на новую строку в файле main.go в папке partyinvites

Запустите команду, показанную в листинге 1-4, для компиляции проекта, и вы получите
следующие ошибки:
# partyinvites
.\main.go:5:6: missing function body
.\main.go:6:1: syntax error: unexpected semicolon or newline before {
Go настаивает на определенном стиле кода и необычным образом обрабатывает
распространенные элементы кода, такие как точки с запятой. Подробности синтаксиса Go
описаны в следующих главах, но сейчас важно точно следовать приведенным примерам, чтобы
избежать ошибок.

Определение типа данных и коллекции
Следующим шагом является создание пользовательского типа данных, который будет
представлять ответы RSVP, как показано в листинге 1-6.
package main
import "fmt"
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
func main() {
fmt.Println("TODO: add some features");
}
Листинг 1-6 Определение типа данных в файле main.go в папке partyinvites

Go позволяет определять пользовательские типы и присваивать им имена с помощью
ключевого слова type. В листинге 1-6 создается тип данных struct с именем Rsvp. Структуры
позволяют группировать набор связанных значений. Структура Rsvp определяет четыре поля,
каждое из которых имеет имя и тип данных. Типы данных, используемые полями Rsvp, — string
и bool, которые являются встроенными типами для представления строки символов и логических
значений. (Встроенные типы Go описаны в главе 4.)
Далее мне нужно собрать вместе значения Rsvp. В последующих главах я объясню, как
использовать базу данных в приложении Go, но для этой главы будет достаточно хранить ответы в
памяти, что означает, что ответы будут потеряны при остановке приложения.
Go имеет встроенную поддержку массивов фиксированной длины, массивов переменной
длины (известных как срезы) и карт (словарей), содержащих пары ключ-значение. В листинге 1-7
создается срез, что является хорошим выбором, когда количество сохраняемых значений заранее
неизвестно.
package main
import "fmt"
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}

var responses = make([]*Rsvp, 0, 10)
func main() {
fmt.Println("TODO: add some features");
}
Листинг 1-7 Определение среза в файле main.go в папке partyinvites

Этот новый оператор основан на нескольких функциях Go, которые проще всего понять, если
начать с конца оператора и прорабатывать в обратном направлении.
Go предоставляет встроенные функции для выполнения общих операций с массивами,
срезами и картами. Одной из таких функций является make, которая используется в листинге 1-7
для инициализации нового среза. Последние два аргумента функции make — это начальный
размер и начальная емкость.
...
var responses = make([]*Rsvp, 0, 10)
...
Я указал ноль для аргумента размера, чтобы создать пустой срез. Размеры срезов изменяются
автоматически по мере добавления новых элементов, а начальная емкость определяет, сколько
элементов можно добавить, прежде чем размер среза нужно будет изменить. В этом случае к срезу
можно добавить десять элементов, прежде чем его размер нужно будет изменить.
Первый аргумент метода make указывает тип данных, для хранения которого будет
использоваться срез:
...
var responses = make([]*Rsvp, 0, 10)
...
Квадратные скобки [] обозначают срез. Звездочка * обозначает указатель. Часть типа Rsvp
обозначает тип структуры, определенный в листинге 1-6. В совокупности []*Rsvp обозначает срез
указателей на экземпляры структуры Rsvp.
Вы, возможно, вздрогнули от термина указатель, если вы пришли к Go из C# или Java,
которые не позволяют использовать указатели напрямую. Но вы можете расслабиться, потому что
Go не допускает операций над указателями, которые могут создать проблемы для разработчика.
Как я объясню в главе 4, использование указателей в Go определяет только то, копируется ли
значение при его использовании. Указав, что мой срез будет содержать указатели, я говорю Go не
создавать копии моих значений Rsvp, когда я добавляю их в срез.
Остальная часть оператора присваивает инициализированный срез переменной, чтобы я мог
использовать его в другом месте кода:
...
var responses = make([]*Rsvp, 0, 10)
...
Ключевое слово var указывает, что я определяю новую переменную, которой присваивается
имя responses. Знак равенства, =, является оператором присваивания Go и устанавливает
значение переменной responses для вновь созданного среза. Мне не нужно указывать тип
переменной responses, потому что компилятор Go выведет его из присвоенного ей значения.

Создание HTML-шаблонов
Go поставляется с обширной стандартной библиотекой, которая включает поддержку HTMLшаблонов. Добавьте файл с именем layout.html в папку partyinvites с содержимым,

показанным в листинге 1-8.




Let's Party!



{{ block "body" . }} Content Goes Here {{ end }}


Листинг 1-8 Содержимое файла layout.html в папке partyinvites

Этот шаблон будет макетом, содержащим содержимое, общее для всех ответов, которые будет
создавать приложение. Он определяет базовый HTML-документ, включая элемент link (ссылки),
указывающий таблицу стилей из CSS-фреймворка Bootstrap, которая будет загружаться из сети
распространения контента (CDN). Я продемонстрирую, как обслуживать этот файл из папки в
главе 24, но для простоты в этой главе я использовал CDN. Пример приложения по-прежнему
будет работать в автономном режиме, но вы увидите элементы HTML без стилей, показанных на
рисунках.
Двойные фигурные скобки в листинге 1-8, {{ и }}, используются для вставки динамического
содержимого в выходные данные, созданные шаблоном. Используемое здесь выражение block
(блок) определяет содержимое заполнителя, которое будет заменено другим шаблоном во время
выполнения.
Чтобы создать содержимое, которое будет приветствовать пользователя, добавьте файл с
именем welcome.html в папку partyinvites с содержимым, показанным в листинге 1-9.
{{ define "body"}}

We're going to have an exciting party!
And YOU are invited!

RSVP Now


{{ end }}
Листинг 1-9 Содержимое файла welcome.html в папке partyinvites

Чтобы создать шаблон, который позволит пользователю дать свой ответ на RSVP, добавьте
файл с именем form.html в папку partyinvites с содержимым, показанным в листинге 1-10.
{{ define "body"}}
RSVP
{{ if gt (len .Errors) 0}}

{{ range .Errors }}
{{ . }}
{{ end }}


{{ end }}


Your name:



Your email:



Your phone number:



Will you attend?


Yes, I'll be there


No, I can't come




Submit RSVP


{{ end }}
Листинг 1-10 Содержимое файла form.html в папке partyinvites

Чтобы создать шаблон, который будет представлен посетителям, добавьте файл с именем
thanks.html в папку partyinvites с содержимым, показанным в листинге 1-11.
{{ define "body"}}

Thank you, {{ . }}!
It's great that you're coming. The drinks are already in the fridge!
Click here to see who else is coming.

{{ end }}
Листинг 1-11 Содержимое файла thanks.html в папке partyinvites

Чтобы создать шаблон, который будет отображаться при отклонении приглашения, добавьте
файл с именем sorry.html в папку partyinvites с содержимым, показанным в листинге 1-12.
{{ define "body"}}

It won't be the same without you, {{ . }}!
Sorry to hear that you can't make it, but thanks for letting us know.


Click here to see who is coming,
just in case you change your mind.


{{ end }}
Листинг 1-12 Содержимое файла sorry.html в папке partyinvites

Чтобы создать шаблон, отображающий список участников, добавьте файл с именем list.html
в папку partyinvites с содержимым, показанным в листинге 1-13.
{{ define "body"}}

Here is the list of people attending the party


NameEmailPhone


{{ range . }}
{{ if .WillAttend }}

{{ .Name }}
{{ .Email }}
{{ .Phone }}

{{ end }}
{{ end }}



{{ end }}
Листинг 1-13 Содержимое файла list.html в папке partyinvites

Загрузка шаблонов
Следующим шагом является загрузка шаблонов, чтобы их можно было использовать для создания
контента, как показано в листинге 1-14. Я собираюсь написать код, чтобы сделать это поэтапно,
объясняя, что делает каждое изменение по ходу дела. (Вы можете увидеть подсветку ошибок в
редакторе кода, но это будет устранено, когда я добавлю новые операторы кода в более поздние
списки.)
package main
import (
"fmt"
"html/template"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)

func loadTemplates() {
// TODO - load templates here
}
func main() {
loadTemplates()
}
Листинг 1-14 Загрузка шаблонов из файла main.go в папку partyinvites

Первое изменение относится к оператору импорта import и объявляет зависимость от
функций, предоставляемых пакетом html/template, который является частью стандартной
библиотеки Go. Этот пакет поддерживает загрузку и отображение HTML-шаблонов и подробно
описан в главе 23.
Следующий новый оператор создает переменную с именем templates. Тип значения,
присваиваемого этой переменной, выглядит сложнее, чем есть на самом деле:
...
var templates = make(map[string]*template.Template, 3)
...
Ключевое слово map обозначает карту, тип ключа которой указывается в квадратных скобках,
за которым следует тип значения. Тип ключа для этой карты — string, а тип значения —
*template.Template, что означает указатель на структуру Template, определенную в пакете
шаблона. Когда вы импортируете пакет, для доступа к его функциям используется последняя
часть имени пакета. В этом случае доступ к функциям, предоставляемым пакетом html/template,
осуществляется с помощью шаблона, и одной из этих функций является структура с именем
Template. Звездочка указывает на указатель, что означает, что карта использует string ключи,
используемые для хранения указателей на экземпляры структуры Template, определенной
пакетом html/template.
Затем я создал новую функцию с именем loadTemplates, которая пока ничего не делает, но
будет отвечать за загрузку файлов HTML, определенных в предыдущих листингах, и их обработку
для создания значений *template.Template, которые будут храниться на карте. Эта функция
вызывается внутри функции main. Вы можете определять и инициализировать переменные
непосредственно в файлах кода, но самые полезные функции языка можно реализовать только
внутри функций.
Теперь мне нужно реализовать функцию loadTemplates. Каждый шаблон загружается с
макетом, как показано в листинге 1-15, что означает, что мне не нужно повторять базовую
структуру HTML-документа в каждом файле.
package main
import (
"fmt"
"html/template"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)

func loadTemplates() {
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
for index, name := range templateNames {
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
}
}
func main() {
loadTemplates()
}
Листинг 1-15 Загрузка шаблонов из файла main.go в папку partyinvites

Первый оператор в теле loadTemplates определяет переменные, используя краткий синтаксис
Go, который можно использовать только внутри функций. Этот синтаксис определяет имя, за
которым следует двоеточие (:), оператор присваивания (=) и затем значение:
...
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
...
Этот оператор создает переменную с именем templateNames, и ее значение представляет
собой массив из пяти строковых значений, которые выражены с использованием литеральных
значений. Эти имена соответствуют именам файлов, определенных ранее. Массивы в Go имеют
фиксированную длину, и массив, присвоенный переменной templateNames, может содержать
только пять значений.
Эти пять значений перечисляются в цикле for с использованием ключевого слова range,
например:
...
for index, name := range templateNames {
...
Ключевое слово range используется с ключевым словом for для перечисления массивов,
срезов и карт. Операторы внутри цикла for выполняются один раз для каждого значения в
источнике данных, которым в данном случае является массив, и этим операторам присваиваются
два значения для работы:
...
for index, name := range templateNames {
...
Переменной index присваивается позиция значения в массиве, который в настоящее время
перечисляется. Переменной name присваивается значение в текущей позиции. Тип первой
переменной всегда int, это встроенный тип данных Go для представления целых чисел. Тип
другой переменной соответствует значениям, хранящимся в источнике данных. Перечисляемый в
этом цикле массив содержит строковые значения, что означает, что переменной name будет
присвоена строка в позиции в массиве, указанной значением индекса.
Первый оператор в цикле for загружает шаблон:

...
t, err := template.ParseFiles("layout.html", name + ".html")
...
Пакет html/templates предоставляет функцию ParseFiles, которая используется для
загрузки и обработки HTML-файлов. Одной из самых полезных и необычных возможностей Go
является то, что функции могут возвращать несколько результирующих значений. Функция
ParseFiles возвращает два результата: указатель на значение template.Template и ошибку,
которая является встроенным типом данных для представления ошибок в Go. Краткий синтаксис
для создания переменных используется для присвоения этих двух результатов переменным,
например:
...
t, err := template.ParseFiles("layout.html", name + ".html")
...
Мне не нужно указывать типы переменных, которым присваиваются результаты, потому что
они уже известны компилятору Go. Шаблон присваивается переменной с именем t, а ошибка
присваивается переменной с именем err. Это распространенный шаблон в Go, и он позволяет мне
определить, былли загружен шаблон, проверив, равно ли значение err nil, что является нулевым
значением Go:
...
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
...
Если err равен nil, я добавляю на карту пару ключ-значение, используя значение name в
качестве ключа и *template.Tempate, назначенный t в качестве значения. Go использует
стандартную нотацию индекса для присвоения значений массивам, срезам и картам.
Если значение err не равно nil, то что-то пошло не так. В Go есть функция panic, которую
можно вызвать при возникновении неисправимой ошибки. Эффект вызова panic может быть
разным, как я объясню в главе 15, но для этого приложения он будет иметь эффект записи
трассировки стека и прекращения выполнения.
Скомпилируйте и запустите проект с помощью команды go run.; вы увидите следующий
вывод по мере загрузки шаблонов:
Loaded
Loaded
Loaded
Loaded
Loaded

template
template
template
template
template

0
1
2
3
4

welcome
form
thanks
sorry
list

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

который будет /, и когда им предоставляется список участников, который будет запрошен с путем
URL-адреса /list, как показано в листинге 1-16.
package main
import (
"fmt"
"html/template"
"net/http"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)
func loadTemplates() {
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
for index, name := range templateNames {
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
}
}
func welcomeHandler(writer http.ResponseWriter, request *http.Request) {
templates["welcome"].Execute(writer, nil)
}
func listHandler(writer http.ResponseWriter, request *http.Request) {
templates["list"].Execute(writer, responses)
}
func main() {
loadTemplates()

}

http.HandleFunc("/", welcomeHandler)
http.HandleFunc("/list", listHandler)

Листинг 1-16 Определение обработчиков начальных запросов в файле main.go в папке partyinvites

Функциональность для работы с HTTP-запросами определена в пакете net/http, который
является частью стандартной библиотеки Go. Функции, обрабатывающие запросы, должны иметь
определенную комбинацию параметров, например:
...
func welcomeHandler(writer http.ResponseWriter, request *http.Request) {
...

Второй аргумент — это указатель на экземпляр структуры Request, определенной в пакете
net/http, который описывает обрабатываемый запрос. Первый аргумент — это пример
интерфейса, поэтому он не определен как указатель. Интерфейсы определяют набор методов,
которые может реализовать любой тип структуры, что позволяет писать код для использования
любого типа, реализующего эти методы, которые я подробно объясню в главе 11.
Одним из наиболее часто используемых интерфейсов является Writer, который используется
везде, где можно записывать данные, такие как файлы, строки и сетевые подключения. Тип
ResponseWriter добавляет дополнительные функции, относящиеся к работе с ответами HTTP.
Go имеет умный, хотя и необычный подход к интерфейсам и абстракции, следствием которого
является то, что ResponseWriter, полученный функциями, определенными в листинге 1-16, может
использоваться любым кодом, который знает, как записывать данные с использованием
интерфейса Writer. Это включает в себя метод Execute, определенный типом *Template, который
я создал при загрузке шаблонов, что упрощает использование вывода от рендеринга шаблона в
ответе HTTP:
...
templates["list"].Execute(writer, responses)
...
Этот оператор считывает *template.Template из карты, назначенной переменной templates,
и вызывает определенный им метод Execute. Первый аргумент — это ResponseWriter, куда будут
записываться выходные данные ответа, а второй аргумент — это значение данных, которое можно
использовать в выражениях, содержащихся в шаблоне.
Пакет net/http определяет функцию HandleFunc, которая используется для указания URLадреса и обработчика, который будет получать соответствующие запросы. Я использовал
HandleFunc для регистрации своих новых функций-обработчиков, чтобы они реагировали на
URL-пути / и /list:
...
http.HandleFunc("/", welcomeHandler)
http.HandleFunc("/list", listHandler)
...
Я продемонстрирую, как можно настроить процесс отправки запросов в последующих главах,
но стандартная библиотека содержит базовую систему маршрутизации URL-адресов, которая
будет сопоставлять входящие запросы и передавать их функции-обработчику для обработки. Я не
определил все функции обработчика, необходимые приложению, но их достаточно, чтобы начать
обработку запросов с помощью HTTP-сервера, как показано в листинге 1-17.
package main
import (
"fmt"
"html/template"
"net/http"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)

func loadTemplates() {
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
for index, name := range templateNames {
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
}
}
func welcomeHandler(writer http.ResponseWriter, request *http.Request) {
templates["welcome"].Execute(writer, nil)
}
func listHandler(writer http.ResponseWriter, request *http.Request) {
templates["list"].Execute(writer, responses)
}
func main() {
loadTemplates()
http.HandleFunc("/", welcomeHandler)
http.HandleFunc("/list", listHandler)

}

err := http.ListenAndServe(":5000", nil)
if (err != nil) {
fmt.Println(err)
}
Листинг 1-17 Создание HTTP-сервера в файле main.go в папке partyinvites

Новые операторы создают HTTP-сервер, который прослушивает запросы через порт 5000,
указанный первым аргументом функции ListenAndServe. Второй аргумент равен nil, что говорит
серверу, что запросы должны обрабатываться с использованием функций, зарегистрированных с
помощью функции HandleFunc. Запустите команду, показанную в листинге 1-18, в папке
partyinvites, чтобы скомпилировать и выполнить проект.
go run .
Листинг 1-18 Компиляция и выполнение проекта

Откройте новый веб-браузер и запросите URL-адрес http://localhost:5000, что даст ответ,
показанный на рисунке 1-1. (Если вы используете Windows, вам может быть предложено
подтвердить разрешение брандмауэра Windows, прежде чем запросы смогут быть обработаны
сервером. Вам нужно будет предоставлять одобрение каждый раз, когда вы используете команду
go run . в этой главе. В последующих главах представлен простой сценарий PowerShell для
решения этой проблемы.)

Рисунок 1-1 Обработка HTTP-запросов

Нажмите Ctrl+C, чтобы остановить приложение, как только вы подтвердите, что оно может
дать ответ.

Написание функции обработки формы
Нажатие кнопки RSVP Now не имеет никакого эффекта, поскольку для URL-адреса /form, на
который он нацелен, нет обработчика. В листинге 1-19 определяется новая функция-обработчик и
начинается реализация функций, необходимых приложению.
package main
import (
"fmt"
"html/template"
"net/http"
)
type Rsvp struct {
Name, Email, Phone string
WillAttend bool
}
var responses = make([]*Rsvp, 0, 10)
var templates = make(map[string]*template.Template, 3)
func loadTemplates() {
templateNames := [5]string { "welcome", "form", "thanks", "sorry", "list" }
for index, name := range templateNames {
t, err := template.ParseFiles("layout.html", name + ".html")
if (err == nil) {
templates[name] = t
fmt.Println("Loaded template", index, name)
} else {
panic(err)
}
}
}
func welcomeHandler(writer http.ResponseWriter, request *http.Request) {
templates["welcome"].Execute(writer, nil)

}
func listHandler(writer http.ResponseWriter, request *http.Request) {
templates["list"].Execute(writer, responses)
}
type formData struct {
*Rsvp
Errors []string
}
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{}, Errors: []string {},
})
}
}
func main() {
loadTemplates()
http.HandleFunc("/", welcomeHandler)
http.HandleFunc("/list", listHandler)
http.HandleFunc("/form", formHandler)

}

err := http.ListenAndServe(":5000", nil)
if (err != nil) {
fmt.Println(err)
}

Листинг 1-19 Добавление функции обработчика форм в файл main.go в папке partyinvites

Шаблон form.html ожидает получить определенную структуру данных значений данных для
отображения своего содержимого. Для представления этой структуры я определил новый тип
структуры с именем formData. Структуры Go могут быть больше, чем просто группа полей «имязначение», и одна из предоставляемых ими функций — поддержка создания новых структур с
использованием существующих структур. В этом случае я определил структуру formData,
используя указатель на существующую структуру Rsvp, например:
...
type formData struct {
*Rsvp
Errors []string
}
...
В результате структуру formData можно использовать так, как будто она определяет поля Name,
Email, Phone и WillAttend из структуры Rsvp, и я могу создать экземпляр структуры formData,
используя существующее значение Rsvp. Звездочка обозначает указатель, что означает, что я не
хочу копировать значение Rsvp при создании значения formData.
Новая функция-обработчик проверяет значение поля request.Method, которое возвращает тип
полученного HTTP-запроса. Для GET-запросов выполняется шаблон form, например:
...
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {

})

Rsvp: &Rsvp{}, Errors: []string {},

...
Нет данных для использования при ответе на запросы GET, но мне нужно предоставить
шаблон с ожидаемой структурой данных. Для этого я создаю экземпляр структуры formData,
используя значения по умолчанию для ее полей:
...
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{}, Errors: []string {},
})
...
В Go нет ключевого слова new, а значения создаются с помощью фигурных скобок, при этом
значения по умолчанию используются для любого поля, для которого значение не указано.
Поначалу такой оператор может быть трудно разобрать, но он создает структуру formData путем
создания нового экземпляра структуры Rsvp и создания среза строк, не содержащего значений.
Амперсанд (символ &) создает указатель на значение:
...
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{}, Errors: []string {},
})
...
Структура formData была определена так, чтобы ожидать указатель на значение Rsvp, которое
мне позволяет создать амперсанд. Запустите команду, показанную в листинге 1-20, в папке
partyinvites, чтобы скомпилировать и выполнить проект.
go run .
Листинг 1-20 Компиляция и выполнение проекта

Откройте новый веб-браузер, запросите URL-адрес http://localhost:5000 и нажмите
кнопку RSVP Now. Новый обработчик получит запрос от браузера и отобразит HTML-форму,
показанную на рисунке 1-2.

Рисунок 1-2 Отображение HTML-формы

Обработка данных формы
Теперь мне нужно обработать POST-запросы и прочитать данные, которые пользователь ввел в
форму, как показано в листинге 1-21. В этом листинге показаны только изменения функции
formHandler; остальная часть файла main.go остается неизменной.
...
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{}, Errors: []string {},
})
} else if request.Method == http.MethodPost {
request.ParseForm()
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
responses = append(responses, &responseData)
if responseData.WillAttend {
templates["thanks"].Execute(writer, responseData.Name)
} else {
templates["sorry"].Execute(writer, responseData.Name)
}
}
...

}

Листинг 1-21 Обработка данных формы в файле main.go в папке partyinvites

Метод ParseForm обрабатывает данные формы, содержащиеся в HTTP-запросе, и заполняет
карту, доступ к которой можно получить через поле Form. Затем данные формы используются для
создания значения Rsvp:
...
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
...
Этот оператор демонстрирует, как структура создается со значениями для ее полей, в отличие
от значений по умолчанию, которые использовались в листинге 1-19. HTML-формы могут
включать несколько значений с одним и тем же именем, поэтому данные формы представлены в
виде среза значений. Я знаю, что для каждого имени будет только одно значение, и я обращаюсь к
первому значению в срезе, используя стандартную нотацию индекса с отсчетом от нуля, которую
используют большинство языков.
Создав значение Rsvp, я добавляю его в срез, присвоенный переменной responses:
...
responses = append(responses, &responseData)
...

Функция append используется для добавления значения к срезу. Обратите внимание, что я
использую амперсанд для создания указателя на созданное значение Rsvp. Если бы я не
использовал указатель, то мое значение Rsvp дублировалось бы при добавлении в срез.
Остальные операторы используют значение поля WillAttend для выбора шаблона, который
будет представлен пользователю.
Запустите команду, показанную в листинге 1-22, в папке partyinvites, чтобы скомпилировать
и выполнить проект.
go run .
Листинг 1-22 Компиляция и выполнение проекта

Откройте новый веб-браузер, запросите URL-адрес http://localhost:5000 и нажмите
кнопку RSVP Now. Заполните форму и нажмите кнопку Submit RSVP; вы получите ответ,
выбранный на основе значения, которое вы выбрали с помощью элемента выбора HTML.
Щелкните ссылку в ответе, чтобы просмотреть сводку ответов, полученных приложением, как
показано на рисунке 1-3.

Рисунок 1-3 Обработка данных формы

Добавление проверки данных
Все, что требуется для завершения приложения, — это некоторая базовая проверка, чтобы
убедиться, что пользователь заполнил форму, как показано в листинге 1-23. В этом листинге
показаны изменения в функции formHandler, а остальная часть файла main.go осталась
неизменной.
...
func formHandler(writer http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodGet {
templates["form"].Execute(writer, formData {
Rsvp: &Rsvp{}, Errors: []string {},
})
} else if request.Method == http.MethodPost {
request.ParseForm()
responseData := Rsvp {
Name: request.Form["name"][0],
Email: request.Form["email"][0],
Phone: request.Form["phone"][0],
WillAttend: request.Form["willattend"][0] == "true",
}
errors := []string {}
if responseData.Name == "" {
errors = append(errors, "Please enter your name")
}

}

if responseData.Email == "" {
errors = append(errors, "Please enter your email address")
}
if responseData.Phone == "" {
errors = append(errors, "Please enter your phone number")
}
if len(errors) > 0 {
templates["form"].Execute(writer, formData {
Rsvp: &responseData, Errors: errors,
})
} else {
responses = append(responses, &responseData)
if responseData.WillAttend {
templates["thanks"].Execute(writer, responseData.Name)
} else {
templates["sorry"].Execute(writer, responseData.Name)
}
}

}
...
Листинг 1-23 Проверка данных формы в файле main.go в папке partyinvites

Приложение получит пустую строку ("") из запроса, если пользователь не предоставит
значение для поля формы. Новые операторы в листинге 1-23 проверяют поля Name, EMail и Phone
и добавляют сообщение к срезу строк для каждого поля, не имеющего значения. Я использую
встроенную функцию len, чтобы получить количество значений в срезе ошибок, и если есть
ошибки, я снова визуализирую содержимое шаблона form, включая сообщения об ошибках в
данных, которые получает шаблон. Если ошибок нет, то используется шаблон thanks или sorry.
Запустите команду, показанную в листинге 1-24, в папке partyinvites, чтобы скомпилировать
и выполнить проект.
go run .
Листинг 1-24 Компиляция и выполнение проекта

Откройте новый веб-браузер, запросите URL-адрес http://localhost:5000 и нажмите
кнопку RSVP Now. Нажмите кнопку Submit RSVP, не вводя никаких значений в форму; вы
увидите предупреждающие сообщения, как показано на рисунке 1-4. Введите некоторые данные в
форму и отправьте ее снова, и вы увидите окончательное сообщение.

Рисунок 1-4 Проверка данных

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

2. Включение Go в контекст
Go, часто называемый Golang, — это язык, первоначально
разработанный в Google, который начал получать широкое
распространение. Go синтаксически похож на C, но имеет безопасные
указатели, автоматическое управление памятью и одну из самых
полезных и хорошо написанных стандартных библиотек, с которыми
мне приходилось сталкиваться.

Почему вам стоит изучать Go?
Go можно использовать практически для любых задач
программирования, но лучше всего он подходит для разработки
серверов или систем. Обширная стандартная библиотека включает
поддержку наиболее распространенных задач на стороне сервера,
таких как обработка HTTP-запросов, доступ к базам данных SQL и
рендеринг шаблонов HTML. Он имеет отличную поддержку
многопоточности, а комплексная система отражения позволяет писать
гибкие API для платформ и фреймворков.
Go поставляется с полным набором инструментов разработки, а
также имеется хорошая поддержка редактора, что упрощает создание
качественной среды разработки.
Go является кроссплатформенным, что означает, что вы можете
писать, например, в Windows и развертывать на серверах Linux. Или,
как я показываю в этой книге, вы можете упаковать свое приложение в
контейнеры Docker для простого развертывания на общедоступных
платформах хостинга.

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

возвращать несколько результатов, чтобы одно значение не должно
было представлять как успешные, так и неудачные результаты. В Go
есть несколько выдающихся функций, в том числе интуитивно
понятная поддержка многопоточности, которые обагатили бы многие
другие языки.
Раздражающие мнения превращают написание Go в затяжной спор
с компилятором, что-то вроде спора о программировании «и еще коечто…». Если ваш стиль кодирования не совпадает с мнением
дизайнеров Go, вы можете ожидать появления множества ошибок
компилятора. Если, как и я, вы пишете код в течение длительного
времени и у вас есть укоренившиеся привычки, перенятые со многих
языков, то вы разработаете новые и инновационные ругательства,
которые будете использовать, когда компилятор неоднократно
отвергает ваш код для выражений, которые бы компилировались на
любом другом основном языке программирования за последние 30 лет.
Кроме того, у Go есть определенный уклон в сторону системного
программирования и разработки на стороне сервера. Например, есть
пакеты,
которые
обеспечивают
поддержку
разработки
пользовательского интерфейса, но это не та область, в которой Go
сияет, и есть лучшие альтернативы.

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

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

Какова структура этой книги?
Эта книга разделена на три части, каждая из которых охватывает набор
связанных тем.

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

Часть 2: Использование стандартной библиотеки Go
Во второй части этой книги я описываю наиболее полезные пакеты из
обширной стандартной библиотеки Go. Вы узнаете о функциях
форматирования строк, чтения и записи данных; создание HTTPсерверов и клиентов; использование баз данных; и использование
мощной поддержки для рефлексии.

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

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

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

Что делать, если вы нашли ошибку в книге?
Вы можете сообщать мне об ошибках по электронной почте
adam@adam-freeman.com, хотя я прошу вас сначала проверить список
опечаток/исправлений для этой книги, который вы можете найти в
репозитории
книги
на
GitHub
по
адресу
https://github.com/apress/pro-go,
если
о
проблеме
уже
сообщалось.
Я добавляю ошибки, которые могут запутать читателей, особенно
проблемы с примерами кода, в файл опечаток/исправлений в
репозитории GitHub с благодарностью первому читателю,
сообщившему об этом. Я также веду список менее серьезных проблем,
которые обычно означают ошибки в тексте, окружающем примеры, и я
использую их, когда пишу новое издание.

Много ли примеров?
Есть масса примеров. Лучший способ учиться — на примерах, и я
собрал в этой книге столько примеров, сколько смог. Чтобы облегчить
следование примерам, я принял простое соглашение, которому следую,
когда это возможно. Когда я создаю новый файл, я перечисляю его
полное содержимое, как показано в листинге 2-1. Все листинги кода
включают имя файла в заголовке листинга вместе с папкой, в которой
его можно найти.
package store
type Product struct {
Name, Category string
price float64
}

func (p *Product) Price(taxRate float64) float64 {
return p.price + (p.price * taxRate)
}
Листинг 2-1 Содержимое файла product.go в папке store

Этот листинг взят из главы 13. Не беспокойтесь о том, что он
делает; просто имейте в виду, что это полный листинг, в котором
показано все содержимое файла, а в заголовке указано, как называется
файл и где он находится в проекте.
Когда я вношу изменения в код, я выделяю измененные операторы
жирным шрифтом, как показано в листинге 2-2.
package store
type Product struct {
Name, Category string
price float64
}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}

float64)

func (p *Product) Price(taxRate float64) float64 {
return p.price + (p.price * taxRate)
}
Листинг 2-2 Определение конструктора в файле product.go в папке store

Этот список взят из более позднего примера, который требует
изменения в файле, созданном в листинге 2-1. Чтобы помочь вам
следовать примеру, изменения выделены жирным шрифтом.
Некоторые примеры требуют небольших изменений в большом
файле. Чтобы не тратить место на перечисление неизмененных частей
файла, я просто показываю изменяющуюся область, как показано в
листинге 2-3. Вы можете сказать, что этот список показывает только
часть файла, потому что он начинается и заканчивается многоточием
(...).
...

func queryDatabase(db *sql.DB) {
rows, err := db.Query("SELECT * from Products")
if (err == nil) {
for (rows.Next()) {
var id, category int
var name int
var price float64
scanErr := rows.Scan(&id, &name, &category,
&price)
if (scanErr == nil) {
Printfln("Row: %v %v %v %v", id, name,
category, price)
} else {
Printfln("Scan error: %v", scanErr)
break
}
}
} else {
Printfln("Error: %v", err)
}
}
...
Листинг 2-3 Несовпадающее сканирование в файле main.go в папке data

В некоторых случаях мне нужно внести изменения в разные части
одного и того же файла, и в этом случае я опускаю некоторые
элементы или операторы для краткости, как показано в листинге 2-4. В
этом листинге добавлены новые операторы использования и
определены дополнительные методы для существующего файла,
большая часть которых не изменилась и была исключена из листинга.
package main
import "database/sql"
// ...код пропущен для краткости...
func insertAndUseCategory(db *sql.DB, name string, productIDs
...int) (err error) {
tx, err := db.Begin()
updatedFailed := false

if (err == nil) {

catResult,
err
:=
tx.Stmt(insertNewCategory).Exec(name)
if (err == nil) {
newID, _ := catResult.LastInsertId()
preparedStatement
:=
tx.Stmt(changeProductCategory)
for _, id := range productIDs {
changeResult, err :=
preparedStatement.Exec(newID, id)
if (err == nil) {
changes, _ := changeResult.RowsAffected()
if (changes == 0) {
updatedFailed = true
break
}
}
}
}
}
if (err != nil || updatedFailed) {
Printfln("Aborting transaction %v", err)
tx.Rollback()
} else {
tx.Commit()
}
return
}
Листинг 2-4 Использование транзакции в файле main.go в папке data

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

Какое программное обеспечение вам нужно
для примеров?
Единственное программное обеспечение, необходимое для разработки
на Go, описано в главе 1. Я устанавливаю некоторые сторонние пакеты
в последующих главах, но их можно получить с помощью уже
настроенной вами команды go. Я использую Docker контейнеры в
части 3, но это необязательно.

На каких платформах будут работать
примеры?
Все примеры были протестированы на Windows и Linux (в частности,
на Ubuntu 20.04), и все сторонние пакеты поддерживают эти
платформы. Go поддерживает другие платформы, и примеры должны
работать на этих платформах, но я не могу помочь, если у вас
возникнут проблемы с примерами из этой книги.

Что делать, если у вас возникли проблемы с
примерами?
Первое, что нужно сделать, это вернуться к началу главы и начать
заново. Большинство проблем вызвано случайным пропуском шага
или неполным применением изменений, показанных в листинге.
Обратите особое внимание на листинг кода, выделенный жирным
шрифтом, который показывает необходимые изменения.
Затем проверьте список опечаток/исправлений, который включен в
репозиторий книги на GitHub. Технические книги сложны, и ошибки
неизбежны, несмотря на все мои усилия и усилия моих редакторов.
Проверьте список ошибок, чтобы найти список известных ошибок и
инструкции по их устранению.
Если у вас все еще есть проблемы, загрузите проект главы, которую
вы
читаете,
из
GitHub-репозитория
книги,
https://github.com/apress/pro-go, и сравните его со своим
проектом. Я создаю код для репозитория GitHub, прорабатывая

каждую главу, поэтому в вашем проекте должны быть одни и те же
файлы с одинаковым содержимым.
Если вы по-прежнему не можете заставить примеры работать, вы
можете связаться со мной по адресу adam@adam-freeman.com для
получения помощи. Пожалуйста, укажите в письме, какую книгу вы
читаете и какая глава/пример вызывает проблему. Номер страницы или
список кодов всегда полезны. Пожалуйста, помните, что я получаю
много писем и могу не ответить сразу.

Где взять пример кода?
Вы можете загрузить примеры проектов для всех глав этой книги с
https://github.com/apress/pro-go.

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

Как связаться с автором?
Вы можете написать мне по адресу adam@adam-freeman.com. Прошло
несколько лет с тех пор, как я впервые опубликовал адрес электронной
почты в своих книгах. Я не был полностью уверен, что это была
хорошая идея, но я рад, что сделал это. Я получаю электронные
письма со всего мира от читателей, работающих или обучающихся в
каждой отрасли, и, во всяком случае, по большей части электронные
письма позитивны, вежливы, и их приятно получать.
Я стараюсь отвечать быстро, но получаю много писем, а иногда
получаю невыполненные работы, особенно когда пытаюсь закончить

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

Что, если мне действительно понравилась эта
книга?
Пожалуйста, напишите мне по адресу adam@adam-freeman.com и дайте
мне знать. Всегда приятно получать известия от довольных читателей,
и я ценю время, затрачиваемое на отправку этих писем. Написание
этих книг может быть трудным, и эти электронные письма
обеспечивают существенную мотивацию, чтобы упорствовать в
деятельности, которая иногда может казаться невозможной..

Что, если эта книга меня разозлила, и я хочу
пожаловаться?
Вы по-прежнему можете написать мне по адресу adam@adamfreeman.com, и я все равно постараюсь вам помочь. Имейте в виду, что
я могу помочь только в том случае, если вы объясните, в чем проблема
и что вы хотите, чтобы я с ней сделал. Вы должны понимать, что
иногда единственным выходом является признание того, что я не
писатель для вас, и что мы удовлетворитесь только тогда, когда вы
вернете эту книгу и выберете другую. Я тщательно обдумаю все, что
вас расстроило, но после 25 лет написания книг я пришел к выводу,
что не всем нравится читать книги, которые я люблю писать.

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

инструменты, которые Go предоставляет именно для этого.

3. Использование инструментов Go
В этой главе я описываю инструменты разработки Go, большинство из
которых были установлены как часть пакета Go в главе 1. Я описываю
базовую структуру проекта Go, объясняю, как компилировать и
выполнять код Go, и показываю, как установить и использовать
отладчик для приложений Go. Я также описываю инструменты Go для
линтинга и форматирования.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.

Использование команды Go
Команда go предоставляет доступ ко всем функциям, необходимым для
компиляции и выполнения кода Go, и используется в этой книге.
Аргумент, используемый с командой go, определяет операцию, которая
будет выполнена, например, аргумент run, используемый в главе 1,
который компилирует и выполняет исходный код Go. Команда go
поддерживает большое количество аргументов; Таблица 3-1 описывает
наиболее полезные из них.
Таблица 3-1 Используемые аргументы в команде go
Аргументы Описание
build

Команда go build компилирует исходный код в текущем каталоге и создает
исполняемый файл, как описано в разделе «Компиляция и запуск исходного
кода».

clean

Команда go clean удаляет выходные данные, созданные командой go build,
включая исполняемый файл и любые временные файлы, созданные во время
сборки, как описано в разделе «Компиляция и запуск исходного кода».

Аргументы Описание
doc

Команда go doc генерирует документацию из исходного кода. Смотрите простой
пример в разделе «Линтинг кода Go».

fmt

Команда go fmt обеспечивает согласованный отступ и выравнивание в файлах
исходного кода, как описано в разделе «Форматирование кода Go».

get

Команда go get загружает и устанавливает внешние пакеты, как описано в главе
12.

install

Команда go install загружает пакеты и обычно используется для установки
пакетов инструментов, как показано в разделе «Отладка кода Go».

help

Команда go help отображает справочную информацию по другим функциям Go.
Например, команда go help build отображает информацию об аргументе
build.

mod

Команда go mod используется для создания модуля Go и управления им, как
показано в разделе «Определение модуля» и более подробно описано в главе 12.

run

Команда go run создает и выполняет исходный код в указанной папке без
создания исполняемого вывода, как описано в разделе «Использование команды
go run».

test

Команда go test выполняет модульные тесты, как описано в Uлаве 31.

version

Команда go version выводит номер версии Go.

vet

Команда go vet обнаруживает распространенные проблемы в коде Go, как
описано в разделе «Устранение распространенных проблем в коде Go».

Создание проекта Go
Проекты Go не имеют сложной структуры и быстро настраиваются.
Откройте новую командную строку и создайте папку с именем tools в
удобном месте. Добавьте файл с именем main.go в папку инструментов
с содержимым, показанным в листинге 3-1.
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
}
Листинг 3-1 Содержимое файла main.go в папке tools

Я подробно расскажу о языке Go в последующих главах, но для
начала на рисунке 3-1 показаны ключевые элементы файла main.go.

Рисунок 3-1 Ключевые элементы в файле кода

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

Рисунок 3-2 Указание пакета для файла кода

Понимание оператора импорта
Следующий оператор — это оператор импорта, который используется
для объявления зависимостей от других пакетов. За ключевым словом
import следует имя пакета, заключенное в двойные кавычки, как
показано на рисунке 3-3. Оператор import в листинге 3-1 задает пакет с
именем fmt, который является встроенным пакетом Go для чтения и
записи форматированных строк (подробно описанный в главе 17).

Рисунок 3-3 Объявление зависимости пакета

Подсказка
Полный список встроенных пакетов Go доступен по адресу
https://golang.org/pkg.

Понимание функции
Остальные операторы в файле main.go определяют функцию с именем
main. Я подробно описываю функции в главе 8, но функция main
особенная. Когда вы определяете функцию с именем main в пакете с
именем main, вы создаете точку входа, с которой начинается
выполнение в приложении командной строки. Рисунок 3-4
иллюстрирует структуру функции main.

Рисунок 3-4 Структура функции main

Базовая структура функций Go аналогична другим языкам.
Ключевое слово func обозначает функцию, за которым следует имя
функции, которое в данном примере — main.
Функция в листинге 3-1 не определяет никаких параметров, что
обозначено пустыми скобками и не дает результата. Я опишу более

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

Понимание оператора кода
Функция main содержит один оператор кода. Когда вы объявляете
зависимость от пакета с помощью оператора import, результатом
является ссылка на пакет, которая обеспечивает доступ к функциям
пакета. По умолчанию ссылке на пакет назначается имя пакета, так что
функции, предоставляемые пакетом fmt, например, доступны через
ссылку на пакет fmt, как показано на рисунке 3-5.

Рисунок 3-5 Доступ к функциям пакета

Этот оператор вызывает функцию с именем Println,
предоставляемую пакетом fmt. Эта функция записывает строку в
стандартный вывод, что означает, что она будет отображаться в консоли
при сборке и выполнении проекта в следующем разделе.
Для доступа к функции используется имя пакета, за которым
следует точка, а затем функция: fmt.Println. Этой функции передается
один аргумент — строка, которая будет записана.
ИСПОЛЬЗОВАНИЕ ТОЧКИ С ЗАПЯТОЙ В КОДЕ GO
В Go необычный подход к точкам с запятой: они необходимы для
завершения операторов кода, но не требуются в файлах исходного
кода. Вместо этого инструменты сборки Go выясняют, куда должны

идти точки с запятой, когда они обрабатывают файлы, действуя так,
как будто они были добавлены разработчиком.
В результате точки с запятой можно использовать в файлах
исходного кода Go, но они не обязательны и обычно опускаются.
Некоторые странности возникают, если вы не следуете
ожидаемому стилю кода Go. Например, вы получите ошибки
компилятора, если попытаетесь поместить открывающую фигурную
скобку для функции или цикла for на следующей строке, например:
package main
import "fmt"
func main()
{
fmt.Println("Hello, Go")
}
Ошибки сообщают о неожиданной точке с запятой и
отсутствующем теле функции. Это связано с тем, что инструменты
Go автоматически вставили точку с запятой следующим образом:
package main
import "fmt"
func main();
{
fmt.Println("Hello, Go")
}
Сообщения об ошибках имеют больше смысла, когда вы
понимаете, почему они возникают, хотя может быть сложно
приспособиться к ожидаемому формату кода, если это ваше
предпочтительное размещение фигурной скобки.
В этой книге я пытался следовать соглашению об отсутствии
точки с запятой, но я десятилетиями пишу код на языках, требующих
точки с запятой, поэтому вы можете найти случайный пример, когда
я добавлял точки с запятой исключительно по привычке. Команда go
fmt, которую я описываю в разделе «Форматирование кода Go»,

удалит точки с
форматированием.

запятой

и

устранит

другие

проблемы

с

Компиляция и запуск исходного кода
Команда go build компилирует исходный код Go и создает
исполняемый файл. Запустите команду, показанную в листинге 3-2, в
папке tools, чтобы скомпилировать код.
go build main.go
Листинг 3-2 Использование компилятора

Компилятор обрабатывает инструкции в файле main.go и создает
исполняемый файл, который называется main.exe в Windows и main на
других платформах. (Компилятор начнет создавать файлы с более
удобными именами, как только я добавлю модули в раздел
«Определение модуля».)
Запустите команду, показанную в листинге 3-3, в папке tools,
чтобы запустить исполняемый файл.
./main
Листинг 3-3 Запуск скомпилированного исполняемого файла

Точка входа проекта — функция с именем main в пакете, который
тоже называется main — выполняется и выдает следующий результат:
Hello, Go
НАСТРОЙКА КОМПИЛЯТОРА GO
Поведение компилятора Go можно настроить с помощью
дополнительных аргументов, хотя для большинства проектов
достаточно настроек по умолчанию. Двумя наиболее полезными
являются -a, вызывающая полную пересборку даже для
неизмененных файлов, и -o, указывающая имя скомпилированного
выходного файла. Используйте команду go help build, чтобы
увидеть полный список доступных опций. По умолчанию
компилятор создает исполняемый файл, но доступны и другие

выходные
данные

подробности
см.
https://golang.org/cmd/go/#hdr-Build_modes.

на

странице

Очистка
Чтобы удалить выходные данные процесса компиляции, запустите
команду, показанную в листинге 3-4, в папке tools.
go clean main.go
Листинг 3-4 Очистка

Скомпилированный исполняемый файл, созданный в предыдущем
разделе, удаляется, остается только файл исходного кода.

Использование команды go run
Обычно разработка выполняется с помощью команды go
Запустите команду, показанную в листинге 3-5, в папке tools.

run.

go run main.go
Листинг 3-5 Использование команды go run

Файл компилируется и выполняется за один шаг, без создания
исполняемого файла в папке инструментов. Создается исполняемый
файл, но во временной папке, из которой он затем запускается. (Именно
эта серия временных местоположений заставляла брандмауэр Windows
запрашивать разрешение каждый раз, когда в главе 1 использовалась
команда go run. Каждый раз, когда запускалась команда, исполняемый
файл создавался в новой временной папке и который казался
совершенно новым файлом для брандмауэра.)
Команда в листинге 3-5 выводит следующий результат:
Hello, Go

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

использовать сторонние пакеты и может упростить процесс сборки.
Запустите команду, показанную в листинге 3-6, в папке tools.
go mod init tools
Листинг 3-6 Создание модуля

Эта команда добавляет файл с именем go.mod в папку tools.
Причина, по которой большинство проектов начинается с команды go
mod init, заключается в том, что она упрощает процесс сборки. Вместо
указания конкретного файла кода проект может быть построен и
выполнен с использованием точки, указывающей проект в текущем
каталоге. Запустите команду, показанную в листинге 3-7, в папке
инструментов, чтобы скомпилировать и выполнить содержащийся в ней
код, не указывая имя файла кода.
go run .
Листинг 3-7 Компиляция и выполнение проекта

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

Отладка кода Go
Стандартный отладчик для приложений Go называется Delve. Это
сторонний инструмент, но он хорошо поддерживается и рекомендуется
командой разработчиков Go. Delve поддерживает Windows, macOS,
Linux и FreeBSD. Чтобы установить пакет Delve, откройте новую
командную строку и выполните команду, показанную в листинге 3-8.
Подсказка
См.
https://github.com/godelve/delve/tree/master/Documentation/installation
для
получения подробных инструкций по установке для каждой
платформы. Для выбранной операционной системы может
потребоваться дополнительная настройка.

go install github.com/go-delve/delve/cmd/dlv@latest
Листинг 3-8 Установка пакета отладчика

Команда go install загружает и устанавливает пакет и
используется для установки таких инструментов, как отладчики.
Аналогичная команда — go get — выполняет аналогичную задачу для
пакетов, предоставляющих функции кода, которые должны быть
включены вприложение, как показано в главе 12.
Чтобы убедиться, что отладчик установлен, выполните команду,
показанную в листинге 3-9.
dlv version
Листинг 3-9 Запуск отладчика

Если вы получаете сообщение об ошибке, что команда dlv не может
быть найдена, попробуйте указать путь напрямую. По умолчанию
команда dlv будет установлена в папку ~/go/bin (хотя это можно
переопределить, задав переменную среды GOPATH), как показано в
листинге 3-10.
~/go/bin/dlv
Листинг 3-10 Запуск отладчика с путем

Если пакет был установлен правильно, вы увидите вывод,
аналогичный следующему, хотя вы можете увидеть другой номер
версии и идентификатор сборки:
Delve Debugger
Version: 1.7.1
Build: $Id: 3bde2354aafb5a4043fd59838842c4cd4a8b6f0b $
ОТЛАДКА С ФУНКЦИЕЙ PRINTLN
Мне нравятся такие отладчики, как Delve, но я использую их только
для решения проблем, которые не могу решить с помощью своего
основного метода отладки: функции Println. Я использую Println,
потому что это быстро, просто и надежно, а также потому, что
большинство ошибок (по крайней мере, в моем коде) возникают изза того, что функция не получила ожидаемого значения или из-за

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

Подготовка к отладке
В файле main.go недостаточно кода для отладки. Добавьте операторы,
показанные в листинге 3-11, чтобы создать цикл, который будет
распечатывать ряд числовых значений.
package main
import "fmt"
func main() {
fmt.Println("Hello, Go")
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
Листинг 3-11 Добавление цикла в файл main.go в папке tools

Я описываю синтаксис for в главе 6, но для этой главы мне просто
нужны операторы кода, чтобы продемонстрировать, как работает
отладчик. Скомпилируйте и выполните код с помощью команды go
run. команда; вы получите следующий вывод:
Hello, Go

0
1
2
3
4

Использование отладчика
Чтобы запустить отладчик, выполните команду, показанную в листинге
3-12, в папке tools.
dlv debug main.go
Листинг 3-12 Запуск отладчика

Эта команда запускает текстовый клиент отладки, который поначалу
может сбивать с толку, но становится чрезвычайно мощным, как только
вы привыкнете к тому, как он работает. Первым шагом является
создание точки останова, что делается путем указания местоположения
в коде, как показано в листинге 3-13.
break bp1 main.main:3
Листинг 3-13 Создание точки останова

Команда break создает точку останова. Аргументы задают имя
точки останова и расположение. Расположение можно указать поразному, но расположение, используемое в листинге 3-13, определяет
пакет, функцию в этом пакете и строку внутри этой функции, как
показано на рисунке 3-6.

Рисунок 3-6 Указание расположения точки останова

Имя точки останова — bp1, а ее местоположение указывает на
третью строку основной функции в основном пакете. Отладчик
отображает следующее подтверждающее сообщение:
Breakpoint
1
set
c:/tools/main.go:8

at

0x697716

for

main.main()

Далее я собираюсь создать условие для точки останова, чтобы
выполнение было остановлено только тогда, когда указанное
выражение оценивается как true (истинное). Введите в отладчик
команду, показанную в листинге 3-14, и нажмите клавишу Return.
condition bp1 i == 2
Листинг 3-14 Указание условия точки останова в отладчике

Аргументы команды condition задают точку останова и
выражение. Эта команда сообщает отладчику, что точка останова с
именем bp1 должна остановить выполнение только тогда, когда
выражение i == 2 истинно. Чтобы начать выполнение, введите
команду, показанную в листинге 3-15, и нажмите клавишу Return. The
arguments for the condition command specify a breakpoint and an
expression. This command tells the debugger that the breakpoint named bp1
should halt execution only when the expression i == 2 is true. To start
execution, enter the command shown in Listing 3-15 and press Return.
continue
Листинг 3-15 Запуск выполнения в отладчике

Отладчик начинает выполнять код, выдавая следующий результат:
Hello, Go
0
1
Выполнение останавливается, когда выполняется условие,
указанное в листинге 3-15, и отладчик отображает код и точку
остановки выполнения, которую я выделил жирным шрифтом:
> [bp1] main.main() c:/tools/main.go:8 (hits goroutine(1):1

total:1) (PC: 0x207716)
3: import "fmt"
4:
5: func main() {
6:
fmt.Println("Hello, Go")
7:
for i := 0; i < 5; i++ {
=> 8:
fmt.Println(i)
9:
}
10: }
Отладчик предоставляет полный набор команд для проверки и
изменения состояния приложения, наиболее полезные из которых
показаны в Таблице 3-2. (Полный набор команд, поддерживаемых
отладчиком, см. на странице https://github.com/go-delve/delve.)
Таблица 3-2 Полезные команды состояния отладчика
Команда

Описание

print


Эта команда оценивает выражение и отображает результат. Его можно
использовать для отображения значения (print i) или выполнить более
сложный тест (print i > 0).

set
Эта команда изменяет значение указанной переменной.

=
locals

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

whatis


Эта команда выводит тип указанного выражения, например whatis i. Я
описываю типы Go в главе 4.

Запустите команду, показанную в листинге 3-16, чтобы отобразить
текущее значение переменной с именем i.
print i
Листинг 3-16 Печать значения в отладчике

Отладчик отображает ответ 2, который является текущим значением
переменной и соответствует условию, которое я указал для точки
останова в листинге 3-16. Отладчик предоставляет полный набор
команд для управления выполнением, наиболее полезные из которых
показаны в Таблице 3-3.
Таблица 3-3 Полезные команды отладчика для управления выполнением

Команда Описание
continue Эта команда возобновляет выполнение приложения.
next

This command moves to the next statement.

step

Эта команда переходит в текущий оператор.

stepout

Эта команда выходит за пределы текущего оператора.

restart

Эта команда перезапускает процесс. Используйте команду continue, чтобы начать
выполнение.

exit

Эта команда закрывает отладчик.

Введите команду continue, чтобы возобновить выполнение, что
приведет к следующему выводу:
2
3
4
Process 3160 has exited with status 0
Условие, которое я указал для точки останова, больше не
выполняется, поэтому программа работает до тех пор, пока не
завершится. Используйте команду exit, чтобы выйти из отладчика и
вернуться в командную строку.

Использование подключаемого модуля редактора Delve
Delve также поддерживается рядом подключаемых модулей редактора,
которые создают возможности отладки на основе пользовательского
интерфейса для Go. Полный список подключаемых модулей можно
найти по адресу https://github.com/go-delve/delve, но один из
лучших способов отладки Go/Delve предоставляется Visual Studio Code
и устанавливается автоматически при установке языковых
инструментов для Go.
Если вы используете Visual Studio Code, вы можете создавать точки
останова, щелкая в поле редактора кода, и запускать отладчик с
помощью команды «Запустить отладку» в меню «Выполнить».
Если вы получили сообщение об ошибке или вам было предложено
выбрать среду, откройте файл main.go для редактирования, щелкните
любой оператор кода в окне редактора и снова выберите команду
«Запустить отладку».

Я не собираюсь подробно описывать процесс отладки с помощью
Visual Studio Code или любого другого редактора, но на рисунке 3-7
показан отладчик после остановки выполнения в условной точке
останова, воссоздающий пример командной строки из предыдущего
раздела.

Рисунок 3-7 Использование подключаемого модуля редактора Delve

Линтинг Go-кода
Линтер — это инструмент, проверяющий файлы кода с помощью
набора правил, описывающих проблемы, вызывающие путаницу,
приводящие к неожиданным результатам или снижающие
читабельность кода. Наиболее широко используемый линтер для Go
называется golint, который применяет правила, взятые из двух
источников. Первый — это документ Effective Go, созданный Google
(https://golang.org/doc/effective_go.html), который содержит
советы по написанию ясного и лаконичного кода Go. Второй источник

это
коллекция
комментариев
из
обзоров
кода
(https://github.com/golang/go/wiki/CodeReviewComments).

Проблема с golint заключается в том, что он не предоставляет
параметров конфигурации и всегда будет применять все правила, что
может привести к тому, что предупреждения, которые вам
небезразличны, могут быть потеряны в длинном списке
предупреждений для правил, которые вам не нужны. Я предпочитаю
использовать revive пакет линтера, который является прямой заменой
golint, но с поддержкой контроля применяемых правил. Чтобы
установить пакет восстановления, откройте новую командную строку и
выполните команду, показанную в листинге 3-17.
go install github.com/mgechev/revive@latest
Листинг 3-17 Установка пакета линтера

РАДОСТЬ И ПЕЧАЛЬ ЛИНТИНГА
Линтеры могут быть мощным инструментом во благо, особенно в
команде разработчиков с разным уровнем навыков и опыта. Линтеры
могут обнаруживать распространенные проблемы и незаметные
ошибки, которые приводят к непредвиденному поведению или
долгосрочным проблемам обслуживания. Мне нравится этот вид
линтинга, и мне нравится запускать свой код в процессе линтинга
после того, как я завершил основную функцию приложения или до
того, как я передам свой код в систему контроля версий.
Но линтеры также могут быть инструментом разделения и
борьбы, когда правила используются для обеспечения соблюдения
личных предпочтений одного разработчика во всей команде. Обычно
это делается под лозунгом «мнения». Логика в том, что разработчики
тратят слишком много времени на споры о разных стилях
кодирования, и всем лучше, если их заставят писать одинаково.
Мой опыт показывает, что разработчики просто найдут, о чем
поспорить, и что навязывание стиля кода часто является просто
предлогом, чтобы сделать предпочтения одного человека
обязательными для всей команды разработчиков.
В этой главе я не использовал популярный пакет golint, потому
что в нем нельзя отключить отдельные правила. Я уважаю твердое
мнение разработчиков golint, но использование golint заставляет
меня чувствовать, что у меня постоянный спор с кем-то, кого я даже

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

Использование линтера
Файл main.go настолько прост, что линтеру не составит труда его
выделить. Добавьте операторы, показанные в листинге 3-18, которые
являются допустимым кодом Go, который не соответствует правилам,
применяемым линтером.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
func PrintNumber(number int) {
fmt.Println(number)
}
Листинг 3-18 Добавление утверждений в файл main.go в папку tools

Сохраните изменения и используйте командную строку для запуска
команды, показанной в листинге 3-19. (Как и в случае с командой dlv,

для запуска этой команды вам может потребоваться указать путь go/bin
в вашей домашней папке.)
revive
Листинг 3-19 Запуск линтера

Линтер проверяет файл main.go и сообщает о следующей проблеме:
main.go:12:1: exported function PrintHello
comment or be unexported
main.go:16:1: exported function PrintNumber
comment or be unexported

should

have

should

have

Как я объясню в главе 12, функции, имена которых начинаются с
заглавной буквы, считаются экспортируемыми и доступными для
использования за пределами пакета, в котором они определены. По
соглашению
для
экспортируемых
функций
предоставляется
описательный комментарий. Линтер пометил факт отсутствия
комментариев для функций PrintHello и PrintNumber. Листинг 3-20
добавляет комментарий к одной из функций.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
// This function writes a number using the fmt.Println
function
func PrintNumber(number int) {
fmt.Println(number)
}

Листинг 3-20 Добавление комментария в файл main.go в папке tools

Запустите команду revive еще раз; вы получите другую ошибку для
функции PrintNumber:
main.go:12:1: exported function PrintHello should have
comment or be unexported
main.go:16:1: comment on exported function PrintNumber should
be of the form "PrintNumber ..."
Некоторые правила линтера специфичны по своим требованиям.
Комментарий в листинге 3-20 не принимается, поскольку в Effective Go
указано, что комментарии должны содержать предложение,
начинающееся с имени функции, и должны давать краткий обзор
назначения
функции,
как
описано
на
https://golang.org/doc/effective_go.html#commentary.
Листинг
3-21
исправляет комментарий, чтобы он следовал требуемой структуре.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
// PrintNumber writes a number using the fmt.Println function
func PrintNumber(number int) {
fmt.Println(number)
}
Листинг 3-21 Редактирование комментария в файле main.go в папке

Запустите команду revive еще раз; линтер завершится без
сообщений об ошибках для функции PrintNumber, хотя для функции

PrintHello все равно будет выдано предупреждение, поскольку у нее
нет комментария.
ПОНИМАНИЕ ДОКУМЕНТАЦИИ GO
Причина, по которой линтер так строго относится к комментариям,
заключается в том, что они используются командой go doc, которая
генерирует документацию из комментариев исходного кода.
Подробную информацию о том, как используется команда go doc,
можно найти по адресу https://blog.golang.org/godoc, но вы
можете запустить команду go doc -all в папке tools, чтобы быстро
продемонстрировать, как она использует комментарии для
документирования пакета.

Отключение правил линтера
Пакет revive можно настроить с помощью комментариев в файлах
кода, отключив одно или несколько правил для разделов кода. В
листинге 3-22 я использовал комментарии, чтобы отключить правило,
вызывающее предупреждение для функции PrintNumber.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
// revive:disable:exported
func PrintHello() {
fmt.Println("Hello, Go")
}
// revive:enable:exported
// PrintNumber writes a number using the fmt.Println function
func PrintNumber(number int) {

}

fmt.Println(number)

Листинг 3-22 Отключение правила Linter для функции в файле main.go в папке tools

Синтаксис, необходимый для управления линтером, таков: revive,
за которым следует двоеточие, enable (включить) или disable
(отключить) и, возможно, еще одно двоеточие и имя правила линтера.
Так, например, комментарий revive:disable:exported не позволяет
линтеру применить правило с именем exported, которое генерирует
предупреждения. Комментарий revive:disable:exported включает
правило, чтобы оно применялось к последующим операторам в файле
кода.
Вы можете найти список правил, поддерживаемых линтером, по
адресу https://github.com/mgechev/revive#available-rules. Кроме того, вы
можете опустить имя правила из комментария, чтобы управлять
применением всех правил.

Создание конфигурационного файла линтера
Использование комментариев к коду полезно, когда вы хотите подавить
предупреждения для определенной области кода, но при этом
применить правило в другом месте проекта. Если вы вообще не хотите
применять правило, вы можете использовать файл конфигурации в
TOML-формате. Добавьте в папку tools файл с именем revive.toml,
содержимое которого показано в листинге 3-23.
Подсказка
Формат TOML предназначен специально для файлов конфигурации
и описан на странице https://toml.io/en. Полный набор
параметров настройки восстановления описан на странице
https://github.com/mgechev/revive#configuration.
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 0
warningCode = 0
[rule.blank-imports]

[rule.context-as-argument]
[rule.context-keys-type]
[rule.dot-imports]
[rule.error-return]
[rule.error-strings]
[rule.error-naming]
#[rule.exported]
[rule.if-return]
[rule.increment-decrement]
[rule.var-naming]
[rule.var-declaration]
[rule.package-comments]
[rule.range]
[rule.receiver-naming]
[rule.time-naming]
[rule.unexported-return]
[rule.indent-error-flow]
[rule.errorf]
Листинг 3-23 Содержимое файла vanilla.toml в папке tools

Это конфигурация revive по умолчанию, описанная на
https://github.com/mgechev/revive#recommended-configuration,
за
исключением того, что я поставил символ # перед записью, которая
включает правило exported. В листинге 3-24 я удалил комментарии из
файла main.go, которые больше не требуются для проверки линтера.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
func PrintNumber(number int) {

fmt.Println(number)
}
Листинг 3-24 Удаление комментариев из файла main.go в папке tools

Чтобы использовать линтер с файлом конфигурации, выполните
команду, показанную в листинге 3-25, в папке tools.
revive -config revive.toml
Листинг 3-25 Запуск линтера с конфигурационным файлом

Вывода не будет, потому что единственное правило, вызвавшее
ошибку, отключено.
ЛИНТИНГ В РЕДАКТОРЕ КОДА
Некоторые редакторы кода автоматически поддерживают анализ
кода. Например, если вы используете Visual Studio Code, анализ
выполняется в фоновом режиме, а проблемы помечаются как
предупреждения. Код линтера Visual Studio по умолчанию время от
времени меняется; на момент написания статьи это staticcheck,
который можно настроить, но ранее он был golint, а это не так.
Линтер легко заменить на revive, используя параметр настройки
Preferences ➤ Extensions ➤ Go ➤ Lint Tool. Если вы хотите
использовать пользовательский файл конфигурации, используйте
параметр конфигурации Lint Flags, чтобы добавить флаг со
значением -config=./revive.toml, который выберет файл
vanilla.toml.

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

проблемой. В листинге 3-26 я добавил в файл main.go оператор,
намеренно вносящий ошибку в код.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
i = i
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
func PrintNumber(number int) {
fmt.Println(number)
}
Листинг 3-26 Добавление заявления в файл main.go в папке tools

Новый оператор присваивает переменной i саму себя, что
разрешено компилятором Go, но, скорее всего, будет ошибкой. Чтобы
проанализировать код, используйте командную строку для запуска
команды, показанной в листинге 3-27, в папке tools.
go vet main.go
Листинг 3-27 Анализ кода

Команда go vet проверит операторы в файле main.go и выдаст
следующее предупреждение:
# _/C_/tools
.\main.go:8:9: self-assignment of i to i
Предупреждения, выдаваемые командой go vet, указывают место в
коде, где была обнаружена проблема, и предоставляют описание
проблемы.

Команда go vet применяет к коду несколько анализаторов, и вы
можете
увидеть
список
анализаторов
на
странице
https://golang.org/cmd/vet. Вы можете выбрать отдельные
анализаторы для включения или отключения, но может быть трудно
определить, какой анализатор сгенерировал конкретное сообщение.
Чтобы выяснить, какой анализатор отвечает за предупреждение,
запустите команду, показанную в листинге 3-28, в папке tools.
go vet -json main.go
Листинг 3-28 Идентификация анализатора

Аргумент json генерирует вывод в формате JSON, который
группирует предупреждения по анализатору, например:
# _/C_/tools {
"_/C_/tools": {
"assign": [
{
"posn": "C:\\tools\\main.go:8:9",
"message": "self-assignment of i to i"
}
]
}
}
Использование этой команды показывает, что анализатор с именем
assign отвечает за предупреждение, сгенерированное для файла
main.go. Когда имя известно, анализатор можно включить или
отключить, как показано в листинге 3-29.
go vet -assign=false
go vet -assign
Листинг 3-29 Выбор анализаторов

Первая команда в листинге 3-29 запускает все анализаторы, кроме
assign, анализатора, выдавшего предупреждение для оператора
самоназначения. Вторая команда запускает только анализатор assign.
ПОНИМАНИЕ, ЧТО ДЕЛАЕТ КАЖДЫЙ АНАЛИЗАТОР

Может быть трудно понять, что ищет каждый анализатор go vet. Я
считаю модульные тесты, которые команда Go написала для
анализаторов, полезными, поскольку они содержат примеры
искомых
типов
проблем.
Тесты
находятся
на
https://github.com/golang/go/tree/master/src/cmd/vet/testda
ta.
Некоторые редакторы, в том числе Visual Studio Code, отображают
сообщения от go vet в окне редактора, как показано на рисунке 3-8,
что позволяет легко воспользоваться преимуществами анализа без
необходимости явного запуска команды.

Рисунок 3-8 Потенциальная проблема с кодом в редакторе кода

Visual Studio Code помечает ошибку в окне редактора и отображает
подробности в окне «Проблемы». Анализ с помощью go vet включен
по умолчанию, вы можете отключить эту функцию с помощью
элемента конфигурации Настройки ➤ Расширения ➤ Go ➤ Vet On Save.

Форматирование кода Go
Команда go fmt форматирует файлы исходного кода Go для
согласованности. Нет параметров конфигурации для изменения

форматирования, применяемого командой go fmt, которая преобразует
код в стиль, указанный командой разработчиков Go. Наиболее
очевидными изменениями являются использование табуляции для
отступов, последовательное выравнивание комментариев и устранение
ненужных точек с запятой. В листинге 3-30 показан код с
несогласованными отступами, смещенными комментариями и точками
с запятой там, где они не требуются.
Подсказка
Вы можете обнаружить, что ваш редактор автоматически
форматирует код, когда он вставляется в окно редактора или когда
файл сохраняется.
package main
import "fmt"
func main() {
PrintHello ()
for i := 0; i < 5; i++ { // loop with a counter
PrintHello(); // print out a message
PrintNumber(i); // print out the counter
}
}
func PrintHello () {
fmt.Println("Hello, Go");
}
func PrintNumber (number int) {
fmt.Println(number);
}
Листинг 3-30 Создание задач форматирования в файле main.go в папке tools

Запустите команду, показанную в листинге 3-31, в папке tools,
чтобы переформатировать код.
go fmt main.go
Листинг 3-31 Форматирование исходного кода

Средство форматирования удалит точки с запятой, отрегулирует
отступ
и
выровняет
комментарии,
создав
следующий
отформатированный код:
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ { // loop with a counter
PrintHello() // print out a message
PrintNumber(i) // print out the counter
}
}
func PrintHello() {
fmt.Println("Hello, Go")
}
func PrintNumber(number int) {
fmt.Println(number)
}
Я не использовал go fmt для примеров в этой книге, потому что
использование вкладок вызывает проблемы с макетом на печатной
странице. Я должен использовать пробелы для отступов, чтобы код
выглядел должным образом при печати книги, и они заменяются
вкладками с помощью go fmt.

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

4. Основные типы, значения и
указатели
В этой главе я начинаю описывать язык Go, сосредоточившись на
основных типах данных, прежде чем перейти к тому, как они
используются для создания констант и переменных. Я также
представляю поддержку Go для указателей. Указатели могут быть
источником путаницы, особенно если вы переходите к Go с таких
языков, как Java или C#, и я описываю, как работают указатели Go,
демонстрирую, почему они могут быть полезны, и объясняю, почему
их не следует бояться.
Функции, предоставляемые любым языком программирования,
предназначены для совместного использования, что затрудняет их
постепенное внедрение. Некоторые примеры в этой части книги
основаны на функциях, описанных ниже. Эти примеры содержат
достаточно подробностей, чтобы обеспечить контекст, и включают
ссылки на ту часть книги, где можно найти дополнительную
информацию. В Таблице 4-1 показаны основные функции Go в
контексте.
Таблица 4-1 Помещение базовых типов, значений и указателей в контекст
Вопрос

Ответ

Кто они
такие?

Типы данных используются для хранения основных значений, общих для
всех программ, включая числа, строки и значения true/false. Эти типы
данных можно использовать для определения постоянных и переменных
значений. Указатели — это особый тип данных, в котором хранится адрес
памяти.

Почему они
полезны?

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

Как они
Базовые типы данных имеют собственные имена, такие как int и float64, и
используются? могут использоваться с ключевыми словами const и var. Указатели
создаются с помощью оператора адреса &.

Вопрос

Ответ

Есть ли
подводные
камни или
ограничения?

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

Есть ли
Нет альтернатив основным типам данных, которые используются при
альтернативы? разработке Go.

Таблица 4-2 резюмирует главу.
Таблица 4-2 Краткое содержание главы
Проблема

Решение

Листинг

Использовать значение напрямую

Используйте значение литерала

6

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

Используйте ключевое слово const

7, 10

Определите константу, которую можно Создать нетипизированную константу
преобразовать в связанный тип данных

8, 9, 11

Определить переменную

Используйте ключевое слово var или
используйте короткий синтаксис
объявления

12-21

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

Используйте пустой идентификатор

22, 23

Определить указатель

Используйте оператор адреса

24, 25,
29–30

Значение по указателю

Используйте звездочку с именем
переменной-указателя

26–28,
31

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем
basicFeatures. Запустите команду, показанную в листинге 4-1, чтобы
создать файл go.mod для проекта.
go mod init basicfeatures
Листинг 4-1 Создание проекта примера

Добавьте файл с именем main.go в папку basicFeatures с
содержимым, показанным в листинге 4-2.

Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println(rand.Int())
}
Листинг 4-2 Содержимое файла main.go в папке basicFeatures

Используйте командную строку для запуска команды, показанной в
листинге 4-3, в папке basicFeatures.
go run .
Листинг 4-3 Запуск примера проекта

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

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

набора пакетов, являющихся частью установщика Go, используемого
в главе 1.
Я описываю способ создания и использования пакетов Go в главе
12, но некоторые примеры основаны на пакетах из стандартной
библиотеки, и важно понимать, как они используются.
Каждый пакет в стандартной библиотеке объединяет набор
связанных функций. Код в листинге 4-2 использует два пакета: пакет
fmt предоставляет возможности для форматирования и записи строк, а
пакет math/rand работает со случайными числами.
Первым шагом в использовании пакета является определение
оператора import. Рисунок 4-1 иллюстрирует оператор импорта,
используемый в листинге 4-2.

Рисунок 4-1 Импорт пакета

В операторе импорта есть две части: ключевое слово import и пути
к пакетам. Пути сгруппированы в круглых скобках, если
импортируется более одного пакета.
Оператор import создает ссылку на пакет, через которую можно
получить доступ к функциям, предоставляемым пакетом. Имя ссылки
на пакет — это последний сегмент пути к пакету. Путь к пакету fmt
состоит только из одного сегмента, поэтому ссылка на пакет будет fmt.
В пути math/rand есть два сегмента — math и rand, поэтому ссылка на
пакет будет rand. (Я объясню, как выбрать собственное имя ссылки на
пакет, в главе 12.)
Пакет fmt определяет функцию Println, которая записывает
значение в стандартный вывод, а пакет math/rand определяет
функцию Int, которая генерирует случайное целое число. Чтобы
получить доступ к этим функциям, я использую их ссылку на пакет, за

которой следует точка и затем имя функции, как показано на рисунке
4-2.

Рисунок 4-2 Использование ссылки на пакет

Подсказка
Список пакетов стандартной библиотеки Go доступен по адресу
https://golang.org/pkg. Наиболее полезные пакеты описаны во
второй части.
Связанная с этим функция, предоставляемая пакетом fmt, — это
возможность составлять строки путем объединения статического
содержимого со значениями данных, как показано в листинге 4-4.
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("Value:", rand.Int())
}
Листинг 4-4 Составление строки в файле main.go в папке basicFeatures

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

используйте командную строку для запуска команды, показанной в
листинге 4-5, в папке basicFeatures.
go run .
Листинг 4-5 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Value: 5577006791947779410
Есть более полезные способы составления строк, которые я
описываю в второй части, но это простой и полезный для меня способ
предоставления вывода в примерах.

Понимание основных типов данных
Go предоставляет набор основных типов данных, которые описаны в
Таблице 4-3. В следующих разделах я опишу эти типы и объясню, как
они используются. Эти типы являются основой разработки Go, и
многие характеристики этих типов будут знакомы из других языков.
Таблица 4-3 Основные типы данных Go
Имя

Описание

int

Этот тип представляет целое число, которое может быть положительным или
отрицательным. Размер типа int зависит от платформы и может быть либо 32,
либо 64 бита. Существуют также целые типы, которые имеют определенный
размер, например int8, int16, int32 и int64, но следует использовать тип int,
если вам не нужен определенный размер.

uint

Этот тип представляет положительное целое число. Размер типа uint зависит
от платформы и может составлять 32 или 64 бита. Существуют также
целочисленные типы без знака, которые имеют определенный размер, например
uint8, uint16, uint32 и uint64, но следует использовать тип uint, если вам не
нужен определенный размер.

byte

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

float32,
float64

Эти типы представляют числа с дробью. Эти типы выделяют 32 или 64 бита для
хранения значения.

complex64, Эти типы представляют числа, которые имеют действительные и мнимые
complex128 компоненты. Эти типы выделяют 64 или 128 бит для хранения значения.

Имя

Описание

bool

Этот тип представляет булеву истину со значениями true и false.

string

Этот тип представляет собой последовательность символов.

rune

Этот тип представляет одну кодовую точку Unicode. Юникод сложен, но, грубо
говоря, это представление одного символа. Тип rune является псевдонимом для
int32.

КОМПЛЕКСНЫЕ ЧИСЛА В GO
Как отмечено в Таблице 4-3, в Go есть встроенная поддержка
комплексных чисел, у которых есть действительные и мнимые
части. Я помню, как узнал о комплексных числах в школе и быстро
забыл о них, пока не начал читать спецификацию языка Go. В этой
книге я не описываю использование комплексных чисел, потому что
они используются только в определенных областях, таких как
электротехника. Вы можете узнать больше о комплексных числах на
странице https://en.wikipedia.org/wiki/Complex_number.

Понимание литеральных значений
Значения Go могут быть выражены буквально, где значение
определяется непосредственно в файле исходного кода. Обычное
использование литеральных значений включает операнды в
выражениях и аргументы функций, как показано в листинге 4-6.
Подсказка
Обратите внимание, что я закомментировал пакет math/rand из
оператора import в листинге 4-6. Ошибка в Go — импортировать
пакет, который не используется.
package main
import (
"fmt"
//"math/rand"
)
func main() {
fmt.Println("Hello, Go")

fmt.Println(20 + 20)
fmt.Println(20 + 30)
}
Листинг 4-6 Использование литеральных значений в файле main.go в папке basicFeatures

Первый оператор в функции main использует строковый литерал,
который обозначается двойными кавычками, в качестве аргумента
функции fmt.Println. Другие операторы используют литеральные
значения int в выражениях, результаты которых используются в
качестве аргумента функции fmt.Println. Скомпилируйте и
выполните код, и вы увидите следующий вывод:
Hello, Go
40
50
Вам не нужно указывать тип при использовании буквального
значения, потому что компилятор выведет тип на основе способа
выражения значения. Для быстрого ознакомления в Таблице 4-4
приведены примеры литеральных значений для основных типов.
Таблица 4-4 Примеры литерального значения
Тип

Примеры

int

20, -20. Значения также могут быть выражены в шестнадцатеричной (0x14),
восьмеричной (0o24) и двоичной записи (0b0010100).

unit

Нет литералов uint. Все литеральные целые числа обрабатываются как значения
int.

byte

Байтовых литералов нет. Байты обычно выражаются как целочисленные литералы
(например, 101) или литералы выполнения ('e'), поскольку тип byte является
псевдонимом для типа uint8.

float64 20.2, -20.2, 1.2е10, 1.2е-10. Значения также могут быть выражены в
шестнадцатеричном представлении (0x2p10), хотя показатель степени выражается
десятичными цифрами.
bool

true, false.

string

"Hello". Последовательности символов, экранированные обратной косой чертой,
интерпретируются, если значение заключено в двойные кавычки ("Hello\n").
Escape-последовательности не интерпретируются, если значение заключено в
обратные кавычки (`Hello\n`).

rune

'A', '\n', '\u00A5', '¥'.Символы, глифы и escape-последовательности
заключаются в одинарные кавычки (символ ').

Использование констант
Константы — это имена для определенных значений, что позволяет
использовать их многократно и согласованно. В Go есть два способа
определения
констант:
типизированные
константы
и
нетипизированные константы. В листинге 4-7 показано использование
типизированных констант.
package main
import (
"fmt"
//"math/rand"
)
func main() {
const price float32 = 275.00
const tax float32 = 27.50
fmt.Println(price + tax)
}
Листинг 4-7 Определение типизированных констант в файле main.go в папке basicFeatures

Типизированные константы определяются с помощью ключевого
слова const, за которым следует имя, тип и присвоенное значение, как
показано на рисунке 4-3.

Рисунок 4-3 Определение типизированной константы

Этот оператор создает float32 именованную константу price,
значение которой равно 275.00. Код в листинге 4-7 создает две
константы и использует их в выражении, которое передается функции

fmt.Println. Скомпилируйте и запустите код, и вы получите
следующий вывод:
302.5

Понимание нетипизированных констант
Go имеет строгие правила в отношении типов данных и не выполняет
автоматических преобразований типов, что может усложнить общие
задачи программирования, как показано в листинге 4-8.
package main
import (
"fmt"
//"math/rand"
)
func main() {
const price float32 = 275.00
const tax float32 = 27.50
const quantity int = 2
fmt.Println("Total:", quantity * (price + tax))
}
Листинг 4-8 Смешивание типов данных в файле main.go в папке basicFeatures

Тип новой константы — int, что является подходящим выбором,
например, для количества, которое может представлять только целое
количество продуктов. Константа используется в выражении,
переданном функции fmt.Println для расчета общей цены. Но
компилятор сообщает о следующей ошибке при компиляции кода:
.\main.go:12:26: invalid operation: quantity * (price + tax)
(mismatched types int and float32)
Большинство
языков
программирования
автоматически
преобразовали бы типы, чтобы можно было вычислить выражение, но
более строгий подход Go означает, что типы int и float32 нельзя
смешивать. Функция нетипизированных констант упрощает работу с

константами, поскольку компилятор Go будет выполнять ограниченное
автоматическое преобразование, как показано в листинге 4-9.
package main
import (
"fmt"
//"math/rand"
)
func main() {
const price float32 = 275.00
const tax float32 = 27.50
const quantity = 2
fmt.Println("Total:", quantity * (price + tax))
}
Листинг 4-9 UИспользование нетипизированной константы в файле main.go в папке
basicFeatures

Нетипизированная константа определяется без типа данных, как
показано на рисунок 4-4.

Рисунок 4-4 Определение нетипизированной константы

Отсутствие типа при определении константы quantity сообщает
компилятору Go, что он должен быть более гибким в отношении типа
константы. Когда выражение, переданное функции fmt.Println,
оценивается, компилятор Go преобразует значение quantity в
float32. Скомпилируйте и выполните код, и вы получите следующий
вывод:
Total: 605

Нетипизированные константы будут преобразованы, только если
значение может быть представлено в целевом типе. На практике это
означает, что вы можете смешивать нетипизированные целые и
числовые значения с плавающей запятой, но преобразования между
другими типами данных должны выполняться явно, как я описываю в
главе 5.
ПОНИМАНИЕ IOTA
Ключевое слово iota можно использовать для создания серии
последовательных нетипизированных целочисленных констант без
необходимости присваивать им отдельные значения. Вот пример
iota:
...
const (
Watersports = iota
Soccer
Chess
)
...
Этот шаблон создает серию констант, каждой из которых
присваивается целочисленное значение, начиная с нуля. Вы можете
увидеть примеры iota в третьей части.

Определение нескольких констант с помощью одного
оператора
Один оператор может использоваться для определения нескольких
констант, как показано в листинге 4-10.
package main
import (
"fmt"
//"math/rand"
)
func main() {
const price, tax float32 = 275, 27.50

}

const quantity, inStock = 2, true
fmt.Println("Total:", quantity * (price + tax))
fmt.Println("In stock: ", inStock)

Листинг 4-10 Определение нескольких констант в файле main.go в папке basicFeatures

За ключевым словом const следует список имен, разделенных
запятыми, знак равенства и список значений, разделенных запятыми,
как показано на рисунке 4-5. Если указан тип, все константы будут
созданы с этим типом. Если тип опущен, то создаются
нетипизированные константы, и тип каждой константы будет выведен
из ее значения.

Рисунок 4-5 Определение нескольких констант

Компиляция и выполнение кода из листинга 4-10 приводит к
следующему результату:
Total: 605
In stock: true

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

package main
import (
"fmt"
//"math/rand"
)
func main() {
const price, tax float32 = 275, 27.50
const quantity, inStock = 2, true
fmt.Println("Total:", 2 * quantity * (price + tax))
fmt.Println("In stock: ", inStock)
}
Листинг 4-11 Использование литерального значения в файле main.go в папке basicFeatures

Выделенное выражение использует буквальное значение 2, которое
является значением int, как описано в Таблице 4-4, вместе с двумя
значениями float32. Поскольку значение int может быть
представлено как float32, значение будет преобразовано
автоматически. При компиляции и выполнении этот код выдает
следующий результат:
Total: 1210
In stock: true

Использование переменных
Переменные определяются с помощью ключевого слова var, и, в
отличие от констант, значение, присвоенное переменной, можно
изменить, как показано в листинге 4-12.
package main
import "fmt"
func main() {
var price float32 = 275.00
var tax float32 = 27.50
fmt.Println(price + tax)
price = 300

}

fmt.Println(price + tax)

Листинг 4-12 Использование констант в файле main.go в папке basicFeatures

Переменные объявляются с использованием ключевого слова var,
имени, типа и присвоения значения, как показано на рисунке 4-6.

Рисунок 4-6 Определение перменной

Листинг 4-12 определяет переменные price и tax, которым
присвоены значения float32. Новое значение переменной цены
присваивается с помощью знака равенства, который является
оператором присваивания Go, как показано на рисунке 4-7. (Обратите
внимание, что я могу присвоить значение 300 переменной с
плавающей запятой. Это потому, что буквальное значение 300 является
нетипизированной константой, которая может быть представлена как
значение float32.)

Рисунок 4-7 Присвоение нового значения переменной

Код в листинге 4-12 записывает две строки в стандартный вывод с
помощью функции fmt.Println, производя следующий вывод после
компиляции и выполнения кода:
302.5
327.5

Пропуск типа данных переменной

Компилятор Go может вывести тип переменных на основе начального
значения, что позволяет опустить тип, как показано в листинге 4-13.
package main
import "fmt"
func main() {
var price = 275.00
var price2 =price
fmt.Println(price)
fmt.Println(price2)
}
Листинг 4-13 Пропуск типа переменной в файле main.go в папке basicFeatures

Переменная определяется с помощью ключевого слова var, имени
и присваивания значения, но тип опускается, как показано на рисунке
4-8. Значение переменной может быть установлено с использованием
буквального значения или имени константы или другой переменной. В
листинге
значение
переменной
price
устанавливается
с
использованием литерального значения, а значение price2
устанавливается равным текущему значению price.

Рисунок 4-8 Определение переменной без указания типа

Компилятор выведет тип из значения, присвоенного переменной.
Компилятор проверит буквальное значение, присвоенное price, и
выведет его тип как float64, как описано в Таблице 4-4. Тип price2
также будет выведен как float64, поскольку его значение
устанавливается с использованием значения цены. Код в листинге 4-13
выдает следующий результат при компиляции и выполнении:
275

275
Отсутствие типа не имеет такого же эффекта для переменных, как
для констант, и компилятор Go не позволит смешивать разные типы,
как показано в листинге 4-14.
package main
import "fmt"
func main() {
var price = 275.00
var tax float32 = 27.50
fmt.Println(price + tax)
}
Листинг 4-14 Смешивание типов данных в файле main.go в папке basicFeatures

Компилятор всегда будет определять тип буквенных значений с
плавающей запятой как float64, что не соответствует типу float32
переменной tax. Строгое соблюдение типов в Go означает, что
компилятор выдает следующую ошибку при компиляции кода:
.\main.go:10:23: invalid operation: price + tax (mismatched
types float64 and float32)
Чтобы использовать переменные price и tax в одном выражении,
они должны иметь один и тот же тип или быть конвертируемыми в
один и тот же тип. Я объясню различные способы преобразования
типов в главе 5.

Пропуск присвоения значения переменной
Переменные могут быть определены без начального значения, как
показано в листинге 4-15.
package main
import "fmt"
func main() {
var price float32

fmt.Println(price)
price = 275.00
fmt.Println(price)
}
Листинг 4-15 Определение переменной без начального значения в файле main.go в папке
basicFeatures

Переменные определяются с помощью ключевого слова var, за
которым следуют имя и тип, как показано на рисунке 4-9. Тип нельзя
опустить, если нет начального значения.

Рисунок 4-9 Определение переменной без начального значения в файле main.go в папке
basicFeatures

Переменным, определенным таким образом, присваивается нулевое
значение для указанного типа, как описано в Таблице 4-5.
Таблица 4-5 Нулевые значения для основных типов данных
Type

Zero Value

int

0

unit

0

byte

0

float64 0
bool

false

string

"" (пустая строка)

rune

0

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

которым следует значение, назначенное явно в последующем
операторе:
0
275

Определение нескольких переменных с помощью одного
оператора
Один оператор может использоваться для определения нескольких
переменных, как показано в листинге 4-16.
package main
import "fmt"
func main() {
var price, tax = 275.00, 27.50
fmt.Println(price + tax)
}
Листинг 4-16 Определение переменных в файле main.go в папке basicFeatures

Это тот же подход, который используется для определения
констант, и начальное значение, присвоенное каждой переменной,
используется для определения ее типа. Тип должен быть указан, если
начальные значения не присвоены, как показано в листинге 4-17, и все
переменные будут созданы с использованием указанного типа и им
будет присвоено нулевое значение.
package main
import "fmt"
func main() {
var price, tax float64
price = 275.00
tax = 27.50
fmt.Println(price + tax)
}
Листинг 4-17 Определение переменных без начальных значений в файле main.go в
папке basicFeatures

Листинг 4-16 и листинг 4-17 дают одинаковый результат при
компиляции и выполнении:
302.5

Использование краткого синтаксиса объявления
переменных
Краткое объявление переменной обеспечивает сокращение для
объявления переменных, как показано в листинге 4-18.
package main
import "fmt"
func main() {
price := 275.00
fmt.Println(price)
}
Листинг 4-18 Использование синтаксиса краткого объявления переменных в файле main.go в
папке basicFeatures

В сокращенном синтаксисе указывается имя переменной,
двоеточие, знак равенства и начальное значение, как показано на
рисунке 4-10. Ключевое слово var не используется, и тип данных не
может быть указан.

Рисунок 4-10 Синтаксис короткого объявления переменных

Код в листинге 4-18 выдает следующий результат после
компиляции и выполнения кода:
275

Несколько переменных могут быть определены с помощью одного
оператора путем создания списков имен и значений, разделенных
запятыми, как показано в листинге 4-19.
package main
import "fmt"
func main() {
price, tax, inStock := 275.00, 27.50, true
fmt.Println("Total:", price + tax)
fmt.Println("In stock:", inStock)
}
Листинг 4-19 Определение нескольких переменных в файле main.go в папке
basicFeatures

В сокращенном синтаксисе типы не указаны, что означает, что
можно создавать переменные разных типов, полагаясь на то, что
компилятор выведет типы из значений, присвоенных каждой
переменной. Код в листинге 4-19 выдает следующий результат при
компиляции и выполнении:
Total: 302.5
In stock: true
Синтаксис короткого объявления переменных можно использовать
только внутри функций, таких как main функция в листинге 4-19.
Функции Go подробно описаны в главе 8.

Использование краткого синтаксиса переменных для
переопределения переменных
Go обычно не позволяет переопределять переменные, но делает
ограниченное исключение, когда используется короткий синтаксис.
Чтобы продемонстрировать поведение по умолчанию, в листинге 4-20
ключевое слово var используется для определения переменной с тем
же именем, что и уже существующая в той же функции.
package main

import "fmt"
func main() {
price, tax, inStock := 275.00, 27.50, true
fmt.Println("Total:", price + tax)
fmt.Println("In stock:", inStock)

}

var price2, tax = 200.00, 25.00
fmt.Println("Total 2:", price2 + tax)

Листинг 4-20 Переопределение переменной в файле main.go в папке basicFeatures

Первый новый оператор использует ключевое слово var для
определения переменных с именами price2 и tax. В функции main
уже есть переменная с именем tax, что вызывает следующую ошибку
при компиляции кода:
.\main.go:10:17: tax redeclared in this block
Однако
переопределение
переменной
разрешено,
если
используется короткий синтаксис, как показано в листинге 4-21, если
хотя бы одна из других определяемых переменных еще не существует
и тип переменной не изменяется.
package main
import "fmt"
func main() {
price, tax, inStock := 275.00, 27.50, true
fmt.Println("Total:", price + tax)
fmt.Println("In stock:", inStock)

}

price2, tax := 200.00, 25.00
fmt.Println("Total 2:", price2 + tax)
Листинг 4-21 Использование краткого синтаксиса в файле main.go в папке basicFeatures

Скомпилируйте и запустите проект, и вы увидите следующий
вывод:

Total: 302.5
In stock: true
Total 2: 225

Использование пустого идентификатора
В Go запрещено определять переменную и не использовать ее, как
показано в листинге 4-22.
package main
import "fmt"
func main() {
price, tax, inStock, discount := 275.00, 27.50, true,
true
var salesPerson = "Alice"
fmt.Println("Total:", price + tax)
fmt.Println("In stock:", inStock)
}
Листинг 4-22 Определение неиспользуемых переменных в файле main.go в папке
basicFeatures

В листинге определены переменные с именами discount и
salesperson, ни одна из которых не используется в остальной части
кода. При компиляции кода сообщается следующая ошибка:
.\main.go:6:26: discount declared but not used
.\main.go:7:9: salesPerson declared but not used
Один из способов решить эту проблему — удалить
неиспользуемые переменные, но это не всегда возможно. Для таких
ситуаций Go предоставляет пустой идентификатор, который
используется для обозначения значения, которое не будет
использоваться, как показано в листинге 4-23.
package main
import "fmt"

func main() {
price, tax, inStock, _ := 275.00, 27.50, true, true
var _ = "Alice"
fmt.Println("Total:", price + tax)
fmt.Println("In stock:", inStock)
}
Листинг 4-23 Использование пустого идентификатора в файле main.go в папке
basicFeatures

Пустым идентификатором является символ подчеркивания (символ
_), и его можно использовать везде, где использование имени создаст
переменную, которая впоследствии не будет использоваться. Код в
листинге 4-23 при компиляции и выполнении выдает следующий
результат:
Total: 302.5
In stock: true
Это еще одна особенность, которая кажется необычной, но она
важна при использовании функций в Go. Как я объясню в главе 8,
функции Go могут возвращать несколько результатов, и пустой
идентификатор полезен, когда вам нужны некоторые из этих значений
результата, но не другие.

Понимание указателей
Указатели часто неправильно понимают, особенно если вы пришли к
Go с такого языка, как Java или C#, где указатели используются за
кулисами, но тщательно скрыты от разработчика. Чтобы понять, как
работают указатели, лучше всего начать с понимания того, что делает
Go, когда указатели не используются, как показано в листинге 4-24.
Подсказка
Последний пример в этом разделе обеспечивает простую
демонстрацию того, чем могут быть полезны указатели, а не просто
объясняет, как они используются.
package main

import "fmt"
func main() {
first := 100
second := first
first++
fmt.Println("First:", first)
fmt.Println("Second:", second)
}
Листинг 4-24 Определение переменных в файле main.go в папке basicFeatures

Код в листинге 4-24 выдает следующий результат при компиляции
и выполнении:
First: 101
Second: 100
Код в листинге 4-24 создает две переменные. Значение переменной
с именем first устанавливается с помощью строкового литерала.
Значение переменной с именем second устанавливается с
использованием значения first, например:
...
first := 100
second := first
...
Go копирует текущее значение first при создании second, после
чего эти переменные не зависят друг от друга. Каждая переменная
является ссылкой на отдельную ячейку памяти, где хранится ее
значение, как показано на рисунке 4-11.

Рисунок 4-11 Независимые значения

Когда я использую оператор ++ для увеличения переменной first в
листинге 4-24, Go считывает значение в ячейке памяти, связанной с
переменной, увеличивает значение и сохраняет его в той же ячейке
памяти. Значение, присвоенное переменной second, остается
прежним, поскольку изменение влияет только на значение,
сохраненное переменной first, как показано на рисунке 4-12.

Рисунок 4-12 Изменение значения

ПОНИМАНИЕ АРИФМЕТИКИ УКАЗАТЕЛЕЙ
Указатели имеют плохую репутацию из-за арифметики указателей.
Указатели сохраняют ячейки памяти в виде числовых значений, что
означает, что ими можно манипулировать с помощью
арифметических операторов, обеспечивая доступ к другим ячейкам
памяти. Например, вы можете начать с местоположения,
указывающего на значение int; увеличить значение на количество
битов, используемых для хранения int; и прочитайте соседнее
значение. Это может быть полезно, но может привести к
неожиданным результатам, таким как попытка доступа к
неправильному расположению или расположению за пределами
памяти, выделенной программе.
Go не поддерживает арифметику указателей, что означает, что
указатель на одно местоположение нельзя использовать для
получения других местоположений. Компилятор сообщит об
ошибке, если вы попытаетесь выполнить арифметические действия
с помощью указателя.

Определение указателя

Указатель — это переменная, значением которой является адрес
памяти. В листинге 4-25 определяется указатель.
package main
import "fmt"
func main() {
first := 100
var second *int = &first
first++

}

fmt.Println("First:", first)
fmt.Println("Second:", second)

Листинг 4-25 Определение указателя в файле main.go в папке basicFeatures

Указатели определяются с помощью амперсанда (символа &),
известного как оператор адреса, за которым следует имя переменной,
как показано на рисунке 4-13.

Рисунок 4-13 Определение указателя

Указатели такие же, как и другие переменные в Go. У них есть тип
и значение. Значением переменной second будет адрес памяти,
используемый Go для хранения значения переменной first.
Скомпилируйте и выполните код, и вы увидите такой вывод:
First: 101
Second: 0xc000010088

Вы увидите разные выходные данные в зависимости от того, где Go
решил сохранить значение для переменной first. Конкретное место в
памяти не имеет значения, интерес представляют отношения между
переменными, показанные на рисунке 4-14.

Рисунок 4-14 Указатель и его расположение в памяти

Тип указателя основан на типе переменной, из которой он создан, с
префиксом звездочки (символ *). Тип переменной с именем second —
*int, потому что она была создана путем применения оператора
адреса к переменной first, значение которой равно int. Когда вы
видите тип *int, вы знаете, что это переменная, значением которой
является адрес памяти, в котором хранится переменная типа int.
Тип указателя фиксирован, потому что все типы Go фиксированы, а
это означает, что когда вы создаете указатель, например, на int, вы
меняете значение, на которое он указывает, но вы не можете
использовать его для указания на адрес памяти, используемый для
хранения другого типа, например, float64. Это ограничение важно,
поскольку в Go указатели — это не просто адреса памяти, а, скорее,
адреса памяти, которые могут хранить определенный тип значения.

Следование указателю
Фраза, следование указателю, означает чтение значения по адресу
памяти, на который указывает указатель, и это делается с помощью
звездочки (символа *), как показано в листинге 4-26. Я также
использовал короткий синтаксис объявления переменной для указателя
в этом примере. Go выведет тип указателя так же, как и с другими
типами.
package main
import "fmt"

func main() {
first := 100
second := &first
first++
fmt.Println("First:", first)
fmt.Println("Second:", *second)
}
Листинг 4-26 Следование указателю в файле main.go в папке basicFeatures

Звездочка сообщает Go, что нужно следовать указателю и получить
значение в ячейке памяти, как показано на рисунке 4-15. Это известно
как разыменование указателя.

Рисунок 4-15 Следование указателю

Код в листинге 4-26 выдает следующий результат при компиляции
и выполнении:
First: 101
Second: 101
Распространенным заблуждением является то, что first и second
переменные имеют одинаковое значение, но это не так. Есть два
значения. Существует значение int, доступ к которому можно
получить, используя переменную с именем first. Существует также
значение *int, в котором хранится место в памяти значения first.
Можно использовать значение *int, которое будет обращаться к
сохраненному значению int. Но поскольку значение *int является
значением, его можно использовать само по себе, а это значит, что его

можно присваивать другим переменным, использовать в качестве
аргумента для вызова функции и т.д.
Листинг 4-27 демонстрирует первое использование указателя. За
указателем следуют, и значение в ячейке памяти увеличивается.
package main
import "fmt"
func main() {
first := 100
second := &first
first++
*second++

}

fmt.Println("First:", first)
fmt.Println("Second:", *second)

Листинг 4-27 Следование указателю и изменение значения в файле main.go в папке
basicFeatures

Этот код производит следующий вывод при компиляции и
выполнении:
First: 102
Second: 102
В листинге 4-28 показано второе использование указателя, то есть
его использование в качестве самостоятельного значения и присвоение
его другой переменной.
package main
import "fmt"
func main() {
first := 100
second := &first

first++
*second++
var myNewPointer *int
myNewPointer = second
*myNewPointer++
fmt.Println("First:", first)
fmt.Println("Second:", *second)
}
Листинг 4-28 Присвоение значения указателя другой переменной в файле main.go в
папке basicFeatures

Первый новый оператор определяет новую переменную, которую я
создал с ключевым словом var, чтобы подчеркнуть, что тип
переменной *int, что означает указатель на значение int. Следующий
оператор присваивает значение переменной second новой переменной,
а это означает, что значения как second, так и myNewPointer являются
расположением в памяти значения first. По любому указателю
осуществляется доступ к одному и тому же адресу памяти, что
означает, что увеличение myNewPointer влияет на значение,
полученное при переходе по second указателю. Скомпилируйте и
выполните код, и вы увидите следующий вывод:
First: 103
Second: 103

Понимание нулевых значений указателя
Указатели, которые определены, но не имеют значения, имеют нулевое
значение nil, как показано в листинге 4-29.
package main
import "fmt"
func main() {
first := 100
var second *int

}

fmt.Println(second)
second = &first
fmt.Println(second)

Листинг 4-29 Определение неинициализированного указателя в файле main.go в папке
basicFeatures

Указатель second определяется, но не инициализируется значением
и выводится с помощью функции fmt.Println. Оператор адреса
используется для создания указателя на переменную first, а значение
second записывается снова. Код в листинге 4-29 выдает следующий
результат при компиляции и выполнении (игнорируйте < и > в
результате, который просто обозначает nil функцией Println):

0xc000010088
Ошибка выполнения произойдет, если вы будете пытаться
получить значение по указателю, которому не присвоено значение, как
показано в листинге 4-30.
package main
import "fmt"
func main() {
first := 100
var second *int
fmt.Println(*second)
second = &first
fmt.Println(second == nil)
}
Листинг 4-30 Следование неинициализированному указателю в файле main.go в папке
basicFeatures

Этот код компилируется, но при выполнении выдает следующую
ошибку:
panic: runtime error: invalid memory address or nil pointer

dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xec798a]
goroutine 1 [running]:
main.main()
C:/basicFeatures/main.go:10 +0x2a
exit status 2

Указывание на указатели
Учитывая, что указатели хранят ячейки памяти, можно создать
указатель, значением которого будет адрес памяти другого указателя,
как показано в листинге 4-31.
package main
import "fmt"
func main() {
first := 100
second := &first
third := &second

}

fmt.Println(first)
fmt.Println(*second)
fmt.Println(**third)

Листинг 4-31 Создание указателя на указатель в файле main.go в папке basicFeatures

Синтаксис для следующих цепочек указателей может быть
неудобным. В этом случае необходимы две звездочки. Первая
звездочка следует за указателем на ячейку памяти, чтобы получить
значение, хранящееся в переменной с именем second, которая является
значением *int. Вторая звездочка следует за указателем с именем
second, который дает доступ к расположению в памяти значения,
сохраненного переменной first. Это не то, что вам нужно делать в
большинстве проектов, но это дает хорошее подтверждение того, как
работают указатели и как вы можете следовать цепочке, чтобы
добраться до значения данных. Код в листинге 4-31 выдает следующий
результат при компиляции и выполнении:

100
100
100

Понимание того, почему указатели полезны
Легко потеряться в деталях того, как работают указатели, и упустить
из виду, почему они могут быть друзьями программиста. Указатели
полезны, потому что они позволяют программисту выбирать между
передачей значения и передачей ссылки. В последующих главах есть
много примеров, в которых используются указатели, но в завершение
этой главы будет полезна быстрая демонстрация. Тем не менее,
листинги в этом разделе основаны на функциях, которые объясняются
в следующих главах, поэтому вы можете вернуться к этим примерам
позже. В листинге 4-32 приведен пример полезной работы со
значениями.
package main
import (
"fmt"
"sort"
)
func main() {
names := [3]string {"Alice", "Charlie", "Bob"}
secondName := names[1]
fmt.Println(secondName)
sort.Strings(names[:])
fmt.Println(secondName)
}
Листинг 4-32 Работа со значениями в файле main.go в папке basicFeatures

Синтаксис может быть необычным, но этот пример прост.
Создается массив из трех строковых значений, и значение в позиции 1
присваивается переменной с именем secondName. Значение

переменной secondName записывается в консоль, массив сортируется,
и значение переменной secondName снова записывается в консоль.
Этот код производит следующий вывод при компиляции и
выполнении:
Charlie
Charlie
Когда создается переменная secondName, значение строки в
позиции 1 массива копируется в новую ячейку памяти, поэтому
операция сортировки не влияет на это значение. Поскольку значение
было скопировано, теперь оно совершенно не связано с массивом, и
сортировка массива не влияет на значение переменной secondName.
В листинге 4-33 в примере представлена переменная-указатель.
package main
import (
"fmt"
"sort"
)
func main() {
names := [3]string {"Alice", "Charlie", "Bob"}
secondPosition := &names[1]
fmt.Println(*secondPosition)
sort.Strings(names[:])
}

fmt.Println(*secondPosition)
Листинг 4-33 Использование указателя в файле main.go в папке basicFeatures

При создании переменной secondPosition ее значением является
адрес памяти, используемый для хранения строкового значения в
позиции 1 массива. Когда массив отсортирован, порядок элементов в
массиве изменяется, но указатель по-прежнему ссылается на ячейку

памяти для позиции 1, что означает, что следуя указателю
возвращается отсортированное значение, производится следующий
вывод, после того как код скомпилируется и выполнится:
Charlie
Bob
Указатель означает, что я могу сохранить ссылку на
местоположение 1 таким образом, чтобы обеспечить доступ к
текущему значению, отражающему любые изменения, внесенные в
содержимое массива. Это простой пример, но он показывает, как
указатели предоставляют разработчику выбор между копированием
значений и использованием ссылок.
Если вы все еще не уверены в указателях, подумайте, как проблема
значения и ссылки решается в других языках, с которыми вы знакомы.
C#, например, который я часто использую, поддерживает как
структуры, которые передаются по значению, так и классы,
экземпляры которых передаются как ссылки. И Go, и C# позволяют
мне выбирать, хочу ли я использовать копию или ссылку. Разница в
том, что C# заставляет меня выбирать один раз, когда я создаю тип
данных, а Go позволяет мне выбирать каждый раз, когда я использую
значение. Подход Go более гибкий, но требует большего внимания со
стороны программиста.

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

5. Операции и преобразования
В этой главе я описываю операторы Go, которые используются для
выполнения арифметических операций, сравнения значений и
создания логических выражений, выдающих true/false результаты. Я
также объясню процесс преобразования значения из одного типа в
другой, который можно выполнить, используя комбинацию
встроенных функций языка и средств, предоставляемых стандартной
библиотекой Go. В Таблице 5-1 операции и преобразования Go
показаны в контексте.
Таблица 5-1 Помещение операций и конверсий в контекст
Вопрос

Ответ

Кто они
такие?

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

Почему они
полезны?

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

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

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

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

Таблица 5-2 резюмирует главу.
Таблица 5-2 Краткое содержание главы
Проблема

Решение

Листинг

Проблема

Решение

Листинг

Выполнить арифметику

Используйте арифметические
операторы

4–7

Объединить строки

Используйте оператор +

8

Сравните два значения

Используйте операторы сравнения

9–11

Объединить выражения

Используйте логические операторы

12

Преобразование из одного типа в другой

Выполнить явное преобразование

13–15

Преобразование значения с плавающей
запятой в целое число

Используйте функции, определенные
пакетом math

16

Разобрать строку в другой тип данных

Используйте функции, определенные
пакетом strconv

17–28

Выразить значение в виде строки

Используйте функции, определенные
пакетом strconv

29–32

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем operations.
Запустите команду, показанную в листинге 5-1,
чтобы
инициализировать проект.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init operations
Листинг 5-1 Инициализация проекта

Добавьте файл с именем main.go в папку operations с
содержимым, показанным в листинге 5-2.
package main
import "fmt"

func main() {
fmt.Println("Hello, Operations")

}

Листинг 5-2 Содержимое файла main.go в папке operations

Используйте командную строку для запуска команды, показанной в
листинге 5-3, в папке operations.
go run .
Листинг 5-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Hello, Operations

Понимание операторов Go
Go предоставляет стандартный набор операторов, в Таблице 5-3
описаны те из них, с которыми вы будете сталкиваться чаще всего,
особенно при работе с типами данных, описанными в главе 4.
Таблица 5-3 Основные операторы Go
Оператор Описание
+, -, *, /,
%

Эти операторы используются для выполнения арифметических операций с
числовыми значениями, как описано в разделе «Знакомство с арифметическими
операторами». Оператор + также можно использовать для объединения строк, как
описано в разделе «Объединение строк».

==, !=, =

Эти операторы сравнивают два значения, как описано в разделе «Общие сведения
об операторах сравнения».

||, &&, !

Это логические операторы, которые применяются к bool значениям и
возвращают bool значение, как описано в разделе «Понимание логических
операторов».

=, :=

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

Оператор Описание
-=, +=, ++, Эти операторы увеличивают и уменьшают числовые значения, как описано в
-разделе «Использование операторов увеличения и уменьшения».
&, |, ^, &^, Это побитовые операторы, которые можно применять к целочисленным

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

Понимание операторов Go
Арифметические операторы можно применять к числовым типам
данных (float32, float64, int, uint и типам, зависящим от размера,
описанным в главе 4). Исключением является оператор остатка (%),
который можно использовать только с целыми числами. Таблица 5-4
описывает арифметические операторы.
Таблица 5-4 Арифметические операторы
Оператор Описание
+

Этот оператор возвращает сумму двух операндов.

-

Этот оператор возвращает разницу между двумя операндами.

*

Этот оператор возвращает произведение двух операндов.

/

Этот оператор возвращает частное двух операторов.

%

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

Значения, используемые с арифметическими операторами, должны
быть одного типа (например, все значения int) или быть представлены
одним и тем же типом, например нетипизированные числовые
константы. В листинге 5-4 показано использование арифметических
операторов.
package main
import "fmt"
func main() {
price, tax := 275.00, 27.40

sum := price + tax
difference := price - tax
product := price * tax
quotient := price / tax

}

fmt.Println(sum)
fmt.Println(difference)
fmt.Println(product)
fmt.Println(quotient)

Листинг 5-4 Использование арифметических операторов в файле main.go в папке
operations

Код в листинге 5-4 выдает следующий результат при компиляции и
выполнении:
302.4
247.6
7535
10.036496350364963

Понимание арифметического переполнения
Go позволяет целочисленным значениям переполняться путем
переноса, а не сообщать об ошибке. Значения с плавающей запятой
переполняются до положительной или отрицательной бесконечности.
В листинге 5-5 показаны переполнения для обоих типов данных.
package main
import (
"fmt"
"math"
)
func main() {
var intVal = math.MaxInt64
var floatVal = math.MaxFloat64
fmt.Println(intVal * 2)
fmt.Println(floatVal * 2)

fmt.Println(math.IsInf((floatVal * 2), 0))
}
Листинг 5-5 Переполнение числовых значений в файле main.go в папке operations

Преднамеренно вызвать переполнение проще всего с помощью
пакета math, который является частью стандартной библиотеки Go. Я
опишу этот пакет более подробно в главе 18, но в этой главе меня
интересуют константы, предусмотренные для наименьшего и
наибольшего значений, которые может представлять каждый тип
данных, а также функция IsInf, которая может использоваться для
определения того, является ли значение с плавающей запятой достигло
бесконечности. В листинге я использую константы MaxInt64 и
MaxFloat64 для установки значений двух переменных, которые затем
переполняются в выражениях, передаваемых функции fmt.Println.
Листинг производит следующий вывод, когда он компилируется и
выполняется:
-2
+Inf
true
Целочисленное значение переносится, чтобы получить значение
-2, а значение с плавающей запятой переполняется до +Inf, что
обозначает положительную бесконечность. Функция math.IsInf
используется для обнаружения бесконечности.

Использование оператора остатка от деления
Go предоставляет оператор %, который возвращает остаток при
делении одного целочисленного значения на другое. Его часто
ошибочно принимают за оператор по модулю, предоставляемый
другими языками программирования, такими как Python, но, в отличие
от этих операторов, оператор остатка от деления Go может возвращать
отрицательные значения, как показано в листинге 5-6.
package main
import (
"fmt"
"math"

)
func main() {
posResult := 3 % 2
negResult := -3 % 2
absResult := math.Abs(float64(negResult))

}

fmt.Println(posResult)
fmt.Println(negResult)
fmt.Println(absResult)

Листинг 5-6 Использование оператора остатка в файле main.go в папке operations

Оператор остатка от деления используется в двух выражениях,
чтобы продемонстрировать возможность получения положительных и
отрицательных результатов. Пакет math предоставляет функцию Abs,
которая возвращает абсолютное значение float64, хотя результатом
также является float64. Код в листинге 5-6 выдает следующий
результат при компиляции и выполнении:
1
-1
1

Использование операторов инкремента и декремента
Go предоставляет набор операторов для увеличения и уменьшения
числовых значений, как показано в листинге 5-7. Эти операторы могут
применяться к целым числам и числам с плавающей запятой.
package main
import (
"fmt"
//
"math"
)
func main() {
value := 10.2
value++
fmt.Println(value)
value += 2

}

fmt.Println(value)
value -= 2
fmt.Println(value)
value-fmt.Println(value)

Листинг 5-7 Использование операторов увеличения и уменьшения в файле main.go в папке
operations

Операторы ++ и -- увеличивают или уменьшают значение на
единицу. += и -= увеличивают или уменьшают значение на указанную
величину. Эти операции подвержены описанному ранее поведению
переполнения, но в остальном они согласуются с сопоставимыми
операторами в других языках, кроме операторов ++ и --, которые
могут быть только постфиксными, что означает отсутствие поддержки
выражения, такого как --value. Код в листинге 5-7 выдает следующий
результат при компиляции и выполнении:
11.2
13.2
11.2
10.2

Объединение строк
Оператор + можно использовать для объединения строк для получения
более длинных строк, как показано в листинге 5-8.
package main
import (
"fmt"
//
"math"
)
func main() {
greeting := "Hello"
language := "Go"
combinedString := greeting + ", " + language
}

fmt.Println(combinedString)

Листинг 5-8 Объединение строк в файле main.go в папке operations

Результатом оператора + является новая строка, а код в листинге 58 выдает следующий результат при компиляции и выполнении:
Hello, Go
Go не объединяет строки с другими типами данных, но
стандартная библиотека включает функции, которые составляют
строки из значений разных типов, как описано в главе 17.

Понимание операторов сравнения
Операторы сравнения сравнивают два значения, возвращая логическое
значение true, если они совпадают, и false в противном случае.
Таблица 5-5 описывает сравнение, выполненное каждым оператором.
Таблица 5-5 Операторы сравнения
Оператор Описание
==

Этот оператор возвращает true, если операнды равны.

!=

Этот оператор возвращает true, если операнды не равны.

<

Этот оператор возвращает значение true, если первый операнд меньше второго
операнда.

>

Этот оператор возвращает значение true, если первый операнд больше второго
операнда.

=

Этот оператор возвращает значение true, если первый операнд больше или равен
второму операнду.

Значения, используемые с операторами сравнения, должны быть
одного типа или должны быть нетипизированными константами,
которые могут быть представлены как целевой тип, как показано в
листинге 5-9.
package main
import (
"fmt"
//
"math"

)
func main() {
first := 100
const second = 200.00
equal := first == second
notEqual := first != second
lessThan := first < second
lessThanOrEqual := first second
greaterThanOrEqual := first >= second

}

fmt.Println(equal)
fmt.Println(notEqual)
fmt.Println(lessThan)
fmt.Println(lessThanOrEqual)
fmt.Println(greaterThan)
fmt.Println(greaterThanOrEqual)

Листинг 5-9 Использование нетипизированной константы в файле main.go в папке
operations

Нетипизированная константа представляет собой значение с
плавающей запятой, но может быть представлена как целочисленное
значение, поскольку дробные числа в нем равны нулю. Это позволяет
использовать переменную first и константу second вместе в
сравнениях. Это было бы невозможно, например, для постоянного
значения 200.01, потому что значение с плавающей запятой не может
быть представлено как целое число без отбрасывания дробных цифр и
создания другого значения. Для этого требуется явное преобразование,
как описано далее в этой главе. Код в листинге 5-9 выдает следующий
результат при компиляции и выполнении:
false
true
true
true
false
false

ВЫПОЛНЕНИЕ ТЕРНАРНЫХ СРАВНЕНИЙ
В Go нет тернарного оператора, а это значит, что подобные
выражения использовать нельзя:
...
max := first > second ? first : second
...
Вместо этого один из операторов сравнения, описанных в
таблице 5-5, используется с оператором if, например:
...
var max int
if (first > second) {
max = first
} else {
max = second
}
...
Этот синтаксис менее лаконичен, но, как и многие функции Go,
вы быстро привыкнете работать без троичных выражений.

Сравнение указателей
Указатели можно сравнить, чтобы увидеть, указывают ли они на одну
и ту же ячейку памяти, как показано в листинге 5-10.
package main
import (
"fmt"
//
"math"
)
func main() {
first := 100
second := &first
third := &first

alpha := 100
beta := &alpha

}

fmt.Println(second == third)
fmt.Println(second == beta)

Листинг 5-10 Сравнение указателей в файле main.go в папке operations

Оператор равенства Go (==) используется для сравнения ячеек
памяти. В листинге 5-10 указатели с именами second и third
указывают на одно и то же место и равны. Указатель с именем beta
указывает на другое место в памяти. Код в листинге 5-10 выдает
следующий результат при компиляции и выполнении:
true
false
Важно понимать, что сравниваются области памяти, а не значения,
которые они хранят. Если вы хотите сравнить значения, вы должны
следовать указателям, как показано в листинге 5-11.
package main
import (
"fmt"
//
"math"
)
func main() {
first := 100
second := &first
third := &first
alpha := 100
beta := &alpha
fmt.Println(*second == *third)
fmt.Println(*second == *beta)
}

Листинг 5-11 Следующие указатели в сравнении в файле main.go в папке operations

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

Понимание логических операторов
Логические операторы сравнивают bool значения, как описано в
таблице 5-6. Результаты, полученные этими операторами, могут быть
присвоены переменным или использованы как часть выражения
управления потоком, которое я описываю в главе 6.
Таблица 5-6 Логические операторы
Оператор Описание
||

Этот оператор возвращает true (истину), если любой из операндов true. Если
первый операнд true, то второй операнд не будет оцениваться.

&&

Этот оператор возвращает true, если оба операнда true. Если первый операнд
false, то второй операнд не будет оцениваться.

!

Этот оператор используется с одним операндом. Он возвращает true, если
операнд false, и false, если операнд true.

В листинге 5-12 показаны логические операторы, используемые
для получения значений, присваиваемых переменным.
package main
import (
"fmt"
//
"math"
)
func main() {
maxMph := 50
passengerCapacity := 4
airbags := true

familyCar := passengerCapacity > 2 && airbags
sportsCar := maxMph > 100 || passengerCapacity == 2
canCategorize := !familyCar && !sportsCar
fmt.Println(familyCar)
fmt.Println(sportsCar)
fmt.Println(canCategorize)
}
Листинг 5-12 Использование логических операторов в файле main.go в папке operations

С логическими операторами можно использовать только
логические значения, и Go не будет пытаться преобразовать значение,
чтобы получить истинное или ложное значение. Если операнд для
логического оператора является выражением, то он оценивается для
получения логического результата, который используется при
сравнении. Код в листинге 5-12 выдает следующий результат при
компиляции и выполнении:
true
false
false
Go сокращает процесс оценки, когда используются логические
операторы, а это означает, что для получения результата оценивается
наименьшее количество значений. В случае оператора && оценка
останавливается, когда встречается ложное значение. В случае ||
оператор, оценка останавливается, когда встречается истинное
значение. В обоих случаях никакое последующее значение не может
изменить результат операции, поэтому дополнительные вычисления не
требуются.

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

инструкция, которая применяет оператор сложения к значениям
разных типов. (Вы можете обнаружить, что ваш редактор кода
автоматически исправляет код в листинге 5-13, и вам, возможно,
придется отменить исправление, чтобы код в редакторе соответствовал
листингу, чтобы увидеть ошибку компилятора.)
package main
import (
"fmt"
//
"math"
)
func main() {
kayak := 275
soccerBall := 19.50
total := kayak + soccerBall
fmt.Println(total)
}
Листинг 5-13 Смешивание типов в операции в файле main.go в папке operations

Литеральные
значения,
используемые
для
определения
переменных kayak и soccerBall, приводят к значению int и значению
float64, которые затем используются в операции сложения для
установки значения переменной total. Когда код будет
скомпилирован, будет сообщено о следующей ошибке:
.\main.go:13:20: invalid operation:
(mismatched types int and float64)

kayak

+

soccerBall

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

Выполнение явных преобразований типов

Явное преобразование преобразует значение для изменения его типа,
как показано в листинге 5-14.
package main
import (
"fmt"
//
"math"
)
func main() {
kayak := 275
soccerBall := 19.50
total := float64(kayak) + soccerBall
fmt.Println(total)
}
Листинг 5-14 Использование явного преобразования в файле main.go в папке operations

Синтаксис для явных преобразований — T(x), где T — это целевой
тип, а x — это значение или выражение для преобразования. В
листинге 5-14 я использовал явное преобразование для получения
значения float64 из переменной kayak, как показано на рисунке 5-1.

Рисунок 5-1 Явное преобразование типа

Преобразование в значение float64 означает, что типы в операции
сложения согласованы. Код в листинге 5-14 выдает следующий
результат при компиляции и выполнении:
294.5

Понимание ограничений явных преобразований

Явные преобразования можно использовать только в том случае, если
значение может быть представлено в целевом типе. Это означает, что
вы можете выполнять преобразование между числовыми типами и
между строками и рунами, но другие комбинации, такие как
преобразование значений int в значения bool, не поддерживаются.
Следует соблюдать осторожность при выборе значений для
преобразования, поскольку явные преобразования могут привести к
потере точности числовых значений или вызвать переполнение, как
показано в листинге 5-15.
package main
import (
"fmt"
//
"math"
)
func main() {
kayak := 275
soccerBall := 19.50
total := kayak + int(soccerBall)
fmt.Println(total)
fmt.Println(int8(total))
}
Листинг 5-15 Преобразование числовых типов в файле main.go в папке operations

Этот листинг преобразует значение float64 в int для операции
сложения и, отдельно, преобразует int в int8 (это тип для целого
числа сознаком, выделяющего 8 бит памяти, как описано в главе 4).
Код выдает следующий результат при компиляции и выполнении:
294
38
При преобразовании из числа с плавающей запятой в целое
дробная часть значения отбрасывается, так что число с плавающей

запятой 19.50 становится int со значением 19. Отброшенная дробь
является причиной того, что значение переменной total равно 294
вместо 294.5 произведено в предыдущем разделе.
Значение int8, используемое во втором явном преобразовании,
слишком мало для представления значения int 294, поэтому
происходит переполнение переменной, как описано в предыдущем
разделе «Понимание арифметического переполнения».

Преобразование значений с плавающей запятой в целые
числа
Как показано в предыдущем примере, явные преобразования могут
привести к неожиданным результатам, особенно при преобразовании
значений с плавающей запятой в целые числа. Самый безопасный
подход — преобразовать в другом направлении, представляя целые
числа и значения с плавающей запятой, но если это невозможно, то
math пакет предоставляет набор полезных функций, которые можно
использовать для выполнения преобразований контролируемым
образом, как описано в таблице 5-7.
Таблица 5-7 Функции в пакете math для преобразования числовых типов
Функция

Описание

Ceil(value)

Эта функция возвращает наименьшее целое число, большее
указанного значения с плавающей запятой. Например, наименьшее
целое число, большее 27.1, равно 28.

Floor(value)

Эта функция возвращает наибольшее целое число, которое меньше
указанного значения с плавающей запятой. Например, наибольшее
целое число, меньшее 27.1, равно 28.

Round(value)

Эта функция округляет указанное значение с плавающей запятой до
ближайшего целого числа.

RoundToEven(value) Эта функция округляет указанное значение с плавающей запятой до
ближайшего четного целого числа.

Функции, описанные в таблице, возвращают значения float64,
которые затем могут быть явно преобразованы в тип int, как показано
в листинге 5-16.
package main

import (
"fmt"
"math"
)
func main() {
kayak := 275
soccerBall := 19.50
total := kayak + int(math.Round(soccerBall))
}

fmt.Println(total)
Листинг 5-16 Округление значения в файле main.go в папке operations

Функция math.Round округляет значение soccerBall с 19.5 до 20,
которое затем явно преобразуется в целое число и используется в
операции сложения. Код в листинге 5-16 выдает следующий результат
при компиляции и выполнении:
295

Парсинг из строк
Стандартная
библиотека
Go
включает
пакет
strconv,
предоставляющий функции для преобразования string значений в
другие базовые типы данных. Таблица 5-8 описывает функции,
которые анализируют строки в другие типы данных.
Таблица 5-8 Функции для преобразования строк в другие типы данных
Функция

Описание

ParseBool(str)

Эта функция преобразует строку в логическое значение. Распознаваемые
строковые значения: "true", "false", "TRUE", "FALSE", "True", "False",
"T", "F", "0" и "1".

ParseFloat(str, Эта функция анализирует строку в значение с плавающей запятой
size)
указанного размера, как описано в разделе «Анализ чисел с плавающей
запятой».

Функция

Описание

ParseInt(str,
base, size)

Эта функция анализирует строку в int64 с указанным основанием и
размером. Допустимые базовые значения: 2 для двоичного, 8 для
восьмеричного, 16 для шестнадцатеричного и 10, как описано в разделе
«Синтаксический анализ целых чисел».

ParseUint(str,
base, size)

Эта функция преобразует строку в целое число без знака с указанным
основанием и размером.

Atoi(str)

Эта функция преобразует строку в целое число с основанием 10 и
эквивалентна вызову функции ParseInt(str, 10, 0), как описано в
разделе «Использование удобной целочисленной функции».

В листинге 5-17 показано использование функции ParseBool для
преобразования строк в логические значения.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "true"
val2 := "false"
val3 := "not true"
bool1, b1err := strconv.ParseBool(val1)
bool2, b2err := strconv.ParseBool(val2)
bool3, b3err := strconv.ParseBool(val3)

}

fmt.Println("Bool 1", bool1, b1err)
fmt.Println("Bool 2", bool2, b2err)
fmt.Println("Bool 3", bool3, b3err)
Листинг 5-17 Разбор строк в файле main.go в папке operations

Как я объясню в главе 6, функции Go могут выдавать несколько
результирующих значений. Функции, описанные в таблице 5-8,
возвращают два значения результата: проанализированный результат и
ошибку, как показано на рисунке 5-8.

Рисунок 5-2 Разбор строки

Возможно, вы привыкли к языкам, которые сообщают о проблемах,
генерируя исключение, которое можно перехватить и обработать с
помощью специального ключевого слова, такого как catch. Go
работает, присваивая ошибку второму результату, полученному
функциями в Таблице 5-8. Если результат ошибки равен нулю, то
строка успешно проанализирована. Если результат ошибки не nil, то
синтаксический анализ завершился неудачно. Вы можете увидеть
примеры успешного и неудачного синтаксического анализа,
скомпилировав и выполнив код в листинге 5-17, который дает
следующий результат:
Bool 1 true
Bool 2 false
Bool 3 false strconv.ParseBool: parsing "not true": invalid
syntax
Первые две строки разбираются на значения true и false, и
результат ошибки для обоих вызовов функции равен nil. Третья
строка отсутствует в списке распознаваемых значений, описанном в
таблице 5-8, и ее нельзя проанализировать. Для этой операции
результат ошибки предоставляет подробные сведения о проблеме.
Необходимо соблюдать осторожность, проверяя результат ошибки,
потому что другой результат по умолчанию будет равен нулю, когда
строка не может быть проанализирована. Если вы не проверите
результат ошибки, вы не сможете отличить ложное значение, которое
было правильно проанализировано из строки, и нулевое значение,
которое было использовано из-за сбоя синтаксического анализа.
Проверка на наличие ошибки обычно выполняется с использованием
ключевых слов if/else, как показано в листинге 5-18. Я описываю
ключевое слово if и связанные с ним функции в главе 6.

package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "0"
bool1, b1err := strconv.ParseBool(val1)
if b1err == nil {
fmt.Println("Parsed value:", bool1)
} else {
fmt.Println("Cannot parse", val1)
}
}
Листинг 5-18 Проверка на наличие ошибки в файле main.go в папке operations

Блок if/else позволяет отличить нулевое значение от успешной
обработки строки, которая анализируется до значения false. Как я
объясняю в главе 6, операторы Go if могут определять оператор
инициализации, что позволяет вызывать функцию преобразования и
проверять ее результаты в одном операторе, как показано в листинге 519.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "0"
if bool1, b1err := strconv.ParseBool(val1); b1err == nil
{

}

fmt.Println("Parsed value:", bool1)
} else {
fmt.Println("Cannot parse", val1)
}

Листинг 5-19 Проверка ошибки в отдельном операторе в файле main.go в папке
operations

Листинг 5-18 и Листинг 5-19 выдают следующий результат, когда
проект компилируется и выполняется:
Parsed value: false

Разбор целых чисел
Функции ParseInt и ParseUint требуют основания числа,
представленного строкой, и размера типа данных, который будет
использоваться для представления проанализированного значения, как
показано в листинге 5-20.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "100"
int1, int1err := strconv.ParseInt(val1, 0, 8)
if int1err == nil {
fmt.Println("Parsed value:", int1)
} else {
fmt.Println("Cannot parse", val1)
}
}
Листинг 5-20 Разбор целого числа в файле main.go в папке operations

Первым аргументом функции ParseInt является строка для
анализа. Второй аргумент — это основание для числа или ноль, чтобы
функция могла определить основание по префиксу строки. Последний
аргумент — это размер типа данных, которому будет присвоено
проанализированное значение. В этом примере я оставил функцию
определения основания и указал размер 8.
Скомпилируйте и выполните код из листинга 5-20, и вы получите
следующий
вывод,
показывающий
проанализированное
целочисленное значение:
Parsed value: 100
Вы могли бы ожидать, что указание размера изменит тип,
используемый для результата, но это не так, и функция всегда
возвращает int64. Размер указывает только размер данных, в который
должно поместиться проанализированное значение. Если строковое
значение содержит числовое значение, которое не может быть
представлено в пределах указанного размера, то это значение не будет
проанализировано. В листинге 5-21 я изменил строковое значение,
чтобы оно содержало большее значение.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "500"
int1, int1err := strconv.ParseInt(val1, 0, 8)
if int1err == nil {
fmt.Println("Parsed value:", int1)
} else {
fmt.Println("Cannot parse", val1, int1err)
}
}

Листинг 5-21 Увеличение значения в файле main.go в папке operations

Строка "500" может быть преобразована в целое число, но она
слишком велика для представления в виде 8-битного значения, размер
которого определяется аргументом ParseInt. Когда код компилируется
и выполняется, вывод показывает ошибку, возвращаемую функцией:
Cannot parse 500 strconv.ParseInt: parsing "500": value out
of range
Это может показаться непрямым подходом, но он позволяет Go
поддерживать свои правила типов, гарантируя при этом, что вы можете
безопасно выполнять явное преобразование результата, если он
успешно проанализирован, как показано в листинге 5-22.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "100"
int1, int1err := strconv.ParseInt(val1, 0, 8)
if int1err == nil {
smallInt := int8(int1)
fmt.Println("Parsed value:", smallInt)
} else {
fmt.Println("Cannot parse", val1, int1err)
}
}
Листинг 5-22 Явное преобразование результата в файле main.go в папке operations

Указание размера 8 при вызове функции ParseInt позволяет мне
выполнить явное преобразование в тип int8 без возможности
переполнения. Код в листинге 5-22 выдает следующий результат при
компиляции и выполнении:

Parsed value: 100

Разбор двоичных, восьмеричных и шестнадцатеричных
целых чисел
Аргумент base, полученный функциями Parse, позволяет
анализировать недесятичные числовые строки, как показано в
листинге 5-23.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "100"
int1, int1err := strconv.ParseInt(val1, 2, 8)

}

if int1err == nil {
smallInt := int8(int1)
fmt.Println("Parsed value:", smallInt)
} else {
fmt.Println("Cannot parse", val1, int1err)
}

Листинг 5-23 Анализ двоичного значения в файле main.go в папке operations

Строковое значение "100" может быть преобразовано в десятичное
значение 100, но оно также может представлять двоичное значение 4.
Используя второй аргумент функции ParseInt, я могу указать
основание 2, что означает, что строка будет интерпретироваться как
двоичное значение. Скомпилируйте и выполните код, и вы увидите
десятичное представление числа, проанализированного из двоичной
строки:
Parsed value: 4

Вы можете оставить функции Parse для определения базы
значения с помощью префикса, как показано в листинге 5-24.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "0b1100100"
int1, int1err := strconv.ParseInt(val1, 0, 8)
if int1err == nil {
smallInt := int8(int1)
fmt.Println("Parsed value:", smallInt)
} else {
fmt.Println("Cannot parse", val1, int1err)
}
}
Листинг 5-24 Использование префикса в файле main.go в папке operations

Функции, описанные в таблице 5-8, могут определять базу
анализируемого значения на основе его префикса. Таблица 5-9
описывает набор поддерживаемых префиксов.
Таблица 5-9 Базовые префиксы для числовых строк
Префикс Описание
0b

Этот префикс обозначает двоичное значение, например 0b1100100.

0o

Этот префикс обозначает восьмеричное значение, например 0o144.

0x

Этот префикс обозначает шестнадцатеричное значение, например 0x64.

Строка в листинге 5-24 имеет префикс 0b, обозначающий двоичное
значение. Когда код компилируется и выполняется, создается
следующий вывод:
Parsed value: 100

Использование удобной целочисленной функции
Для многих проектов наиболее распространенной задачей
синтаксического анализа является создание значений int из строк,
содержащих десятичные числа, как показано в листинге 5-25.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "100"
int1, int1err := strconv.ParseInt(val1, 10, 0)
if int1err == nil {
var intResult int = int(int1)
fmt.Println("Parsed value:", intResult)
} else {
fmt.Println("Cannot parse", val1, int1err)
}
}
Листинг 5-25 Выполнение общей задачи синтаксического анализа в файле main.go в папке
operations

Это настолько распространенная задача, что пакет strconv
предоставляет функцию Atoi, которая выполняет синтаксический
анализ и явное преобразование за один шаг, как показано в листинге 526.
package main
import (
"fmt"
"strconv"
)
func main() {

val1 := "100"
int1, int1err := strconv.Atoi(val1)

}

if int1err == nil {
var intResult int = int1
fmt.Println("Parsed value:", intResult)
} else {
fmt.Println("Cannot parse", val1, int1err)
}
Листинг 5-26 Использование функции удобства в файле main.go в папке operations

Функция Atoi принимает только значение для анализа и не
поддерживает анализ недесятичных значений. Тип результата — int
вместо int64, создаваемого функцией ParseInt. Код в листингах 5-25
и 5-26 выдает следующий результат при компиляции и выполнении:
Parsed value: 100

Разбор чисел с плавающей запятой
Функция ParseFloat используется для анализа строк, содержащих
числа с плавающей запятой, как показано в листинге 5-27.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "48.95"
float1, float1err := strconv.ParseFloat(val1, 64)
if float1err == nil {
fmt.Println("Parsed value:", float1)
} else {
fmt.Println("Cannot parse", val1, float1err)

}
}
Листинг 5-27 Анализ значений с плавающей запятой в файле main.go в папке operations

Первым аргументом функции ParseFloat является анализируемое
значение. Второй аргумент определяет размер результата. Результатом
функции ParseFloat является значение float64, но если указано 32,
то результат можно явно преобразовать в значение float32.
Функция ParseFloat может анализировать значения, выраженные
с помощью экспоненты, как показано в листинге 5-28.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := "4.895e+01"
float1, float1err := strconv.ParseFloat(val1, 64)

}

if float1err == nil {
fmt.Println("Parsed value:", float1)
} else {
fmt.Println("Cannot parse", val1, float1err)
}
Листинг 5-28 Разбор значения с экспонентой в файле main.go в папке operations

Листинги 5-27 и 5-28 дают одинаковый результат при компиляции
и выполнении:
Parsed value: 48.95

Форматирование значений как строк
Стандартная библиотека Go также предоставляет функции для
преобразования основных значений данных в строки, которые можно

использовать напрямую или составлять с другими строками. Пакет
strconv предоставляет функции, описанные в таблице 5-10.
Таблица 5-10 Функции strconv для преобразования значений в строки
Функция

Описание

FormatBool(val)

Эта функция возвращает строку true или false в зависимости от
значения указанного bool значения.

FormatInt(val,
base)

Эта функция возвращает строковое представление указанного
значения int64, выраженное в указанной системе счисления.

FormatUint(val,
base)

Эта функция возвращает строковое представление указанного
значения uint64, выраженное в указанной базе.

FormatFloat(val,
Эта функция возвращает строковое представление указанного
format, precision, значения float64, выраженное с использованием указанного формата,
size)
точности и размера.
Itoa(val)

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

Форматирование логических значений
Функция FormatBool принимает bool значение и возвращает
строковое представление, как показано в листинге 5-29. Это самая
простая из функций, описанных в таблице 5-10, поскольку она
возвращает только строки true и false.
package main
import (
"fmt"
"strconv"
)
func main() {
val1 := true
val2 := false
str1 := strconv.FormatBool(val1)
str2 := strconv.FormatBool(val2)
fmt.Println("Formatted value 1: " + str1)
fmt.Println("Formatted value 2: " + str2)

}
Листинг 5-29 Форматирование логического значения в файле main.go в папке operations

Обратите внимание, что я могу использовать оператор + для
объединения результата функции FormatBool с литеральной строкой,
чтобы в функцию fmt.Println передавался только один аргумент. Код
в листинге 5-29 выдает следующий результат при компиляции и
выполнении:
Formatted value 1: true
Formatted value 2: false

Форматирование целочисленных значений
Функции FormatInt и FormatUint форматируют целочисленные
значения как строки, как показано в листинге 5-30.
package main
import (
"fmt"
"strconv"
)
func main() {
val := 275
base10String := strconv.FormatInt(int64(val), 10)
base2String := strconv.FormatInt(int64(val), 2)
fmt.Println("Base 10: " + base10String)
fmt.Println("Base 2: " + base2String)
}
Листинг 5-30 Форматирование целого числа в файле main.go в папке operations

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

Base 10: 275
Base 2: 100010011

Использование удобной целочисленной функции
Целочисленные
значения
чаще
всего
представляются
с
использованием типа int и преобразуются в строки с основанием 10.
Пакет strconv предоставляет функцию Itoa, которая представляет
собой более удобный способ выполнения этого конкретного
преобразования, как показано в листинге 5-31.
package main
import (
"fmt"
"strconv"
)
func main() {
val := 275
base10String := strconv.Itoa(val)
base2String := strconv.FormatInt(int64(val), 2)
fmt.Println("Base 10: " + base10String)
fmt.Println("Base 2: " + base2String)
}
Листинг 5-31 Использование функции удобства в файле main.go в папке operations

Функция Itoa принимает значение int, которое явно
преобразуется в int64 и передается функции ParseInt. Код в листинге
5-31 выводит следующий результат:
Base 10: 275
Base 2: 100010011

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

разные форматы. В листинге 5-32 показана базовая операция
форматирования с использованием функции FormatFloat.
package main
import (
"fmt"
"strconv"
)
func main() {
val := 49.95
Fstring := strconv.FormatFloat(val, 'f', 2, 64)
Estring := strconv.FormatFloat(val, 'e', -1, 64)
fmt.Println("Format F: " + Fstring)
fmt.Println("Format E: " + Estring)
}
Листинг 5-32 Преобразование числа с плавающей запятой в файле main.go в папке operations

Первым
аргументом
функции
FormatFloat
является
обрабатываемое значение. Второй аргумент — это byte значение,
указывающее формат строки. Байт обычно выражается как
литеральное значение руны, и в таблице 5-11 описаны наиболее часто
используемые форматы руны. (Как отмечалось в главе 4, тип byte
является псевдонимом для uint8 и часто для удобства выражается с
помощью руны.)
Таблица 5-11 Обычно используемые параметры формата для форматирования строк с
плавающей запятой
Функция Описание
f

Значение с плавающей запятой будет выражено в форме ±ddd.ddd без
экспоненты, например 49.95.

e, E

Значение с плавающей запятой будет выражено в форме ±ddd.ddde±dd, например,
4.995e+01 или 4.995E+01. Регистр буквы, обозначающей показатель степени,
определяется регистром руны, используемой в качестве аргумента
форматирования.

Функция Описание
g, G

Значение с плавающей запятой будет выражено в формате e/E для больших
показателей степени или в формате f для меньших значений.

Третий аргумент функции FormatFloat указывает количество
цифр, которые будут следовать за десятичной точкой. Специальное
значение -1 можно использовать для выбора наименьшего количества
цифр, которое создаст строку, которую можно будет разобрать обратно
в то же значение с плавающей запятой без потери точности.
Последний аргумент определяет, округляется ли значение с
плавающей запятой, чтобы его можно было выразить как значение
float32 или float64, используя значение 32 или 64.
Эти аргументы означают, что этот оператор форматирует значение,
назначенное переменной с именем val, используя параметр формата f,
с двумя десятичными знаками и округляет так, чтобы значение могло
быть представлено с использованием типа float64:
...
Fstring := strconv.FormatFloat(val, 'f', 2, 64)
...
Эффект заключается в форматировании значения в строку, которую
можно использовать для представления денежной суммы. Код в
листинге 5-32 выдает следующий результат при компиляции и
выполнении:
Format F: 49.95
Format E: 4.995e+01

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

6. Управление потоком выполнения
В этой главе я описываю возможности Go для управления потоком
выполнения. Go поддерживает ключевые слова, общие для других
языков программирования, такие как if, for, switch и т. д., но каждое
из них имеет некоторые необычные и инновационные функции.
Таблица 6-1 помещает функции управления потоком Go в контекст.
Таблица 6-1 Помещение управления потоком в контекст
Вопрос

Ответ

Что это?

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

Почему они
полезны?

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

Как это
Go поддерживает ключевые слова управления потоком, в том числе if, for и
используется? switch, каждое из которых по-разному управляет потоком выполнения.
Есть ли
подводные
камни или
ограничения?

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

Есть ли
Нет. Управление потоком — это фундаментальная функция языка.
альтернативы?

Таблица 6-2 суммирует главу.
Таблица 6-2 Краткое содержание главы
Проблема

Решение

Листинг

Условно выполнять операторы

Используйте оператор if с необязательными
предложениями else if и else и оператором
инициализации

4–10

Повторно выполнить операторы

Используйте цикл for с необязательными
операторами инициализации и завершения

11–13

Прервать цикл

Используйте ключевое слово continue или
break

14

Проблема

Решение

Листинг

Перечислить последовательность Используйте цикл for с ключевым словом range 15–18
значений
Выполнение сложных сравнений Используйте оператор switch с необязательным
для условного выполнения
оператором инициализации
операторов

19–21,
23–26

Заставить один оператор case
переходить в следующий
оператор case

Используйте ключевое слово fallthrough

22

Укажите место, в которое
должно перейти выполнение

Использовать метку

27

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем flowcontrol.
Перейдите в папку управления потоком и выполните команду,
показанную в листинге 6-1, чтобы инициализировать проект.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init flowcontrol
Листинг 6-1 Инициализация проекта

Добавьте файл с именем main.go в папку flowcontrol с
содержимым, показанным в листинге 6-2.
package main
import "fmt"
func main() {
kayakPrice := 275.00
fmt.Println("Price:", kayakPrice)

}
Листинг 6-2 Содержимое файла main.go в папке flowcontrol

Используйте командную строку для запуска команды, показанной в
листинге 6-3, в папке flowcontrol.
go run .
Листинг 6-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Price: 275

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

Рисунок 6-1 Поток исполнения

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

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

Использование операторов if
Оператор if используется для выполнения группы операторов только
тогда, когда указанное выражение возвращает логическое значение
true при его оценке, как показано в листинге 6-4.
package main
import "fmt"
func main() {
kayakPrice := 275.00

}

if kayakPrice > 100 {
fmt.Println("Price is greater than 100")
}

Листинг 6-4 Использование инструкции if в файле main.go в папке flowcontrol

За ключевым словом if следует выражение, а затем группа
операторов, которые должны быть выполнены, заключенные в
фигурные скобки, как показано на рисунке 6-2.

Рисунок 6-2 Анатомия оператора if

Выражение в листинге 6-4 использует оператор > для сравнения
значения переменной kayakPrice с литеральным постоянным
значением 100. Выражение оценивается как true, что означает, что
выражение, содержащееся в фигурных скобках, выполняется, что
приводит к следующему результату:
Price is greater than 100
Я обычно заключаю выражение в круглые скобки, как показано в
листинге 6-5. Go не требует круглых скобок, но я использую их по
привычке.
package main
import "fmt"
func main() {
kayakPrice := 275.00

}

if (kayakPrice > 100) {
fmt.Println("Price is greater than 100")
}
Листинг 6-5 Использование скобок в файле main.go в папке flowcontrol

ОГРАНИЧЕНИЯ ПО СИНТАКСИСУ УПРАВЛЕНИЯ
ПОТОКОМ
Go менее гибок, чем другие языки, когда речь идет о синтаксисе
операторов if и других операторов управления потоком. Во-первых,
фигурные скобки нельзя опускать, даже если в блоке кода есть
только один оператор, то есть такой синтаксис недопустим:
...
if (kayakPrice > 100)
fmt.Println("Price is greater than 100")
...
Во-вторых, открывающая фигурная скобка должна стоять в той
же строке, что и ключевое слово управления потоком, и не может

появляться в следующей строке, что означает, что этот синтаксис
также не разрешен::
...
if (kayakPrice > 100)
{
fmt.Println("Price is greater than 100")
}
...
В-третьих, если вы хотите разбить длинное выражение на
несколько строк, вы не можете разбить строку после значения или
имени переменной:
...
if (kayakPrice > 100
&& kayakPrice < 500) {
fmt.Println("Price is greater than 100 and less than
500")
}
...
Компилятор Go сообщит об ошибке для всех этих операторов, и
проблема заключается в том, как процесс сборки пытается вставить
точки с запятой в исходный код. Изменить такое поведение
невозможно, и по этой причине некоторые примеры в этой книге
имеют странный формат: некоторые операторы кода содержат
больше символов, чем может быть отображено в одной строке на
печатной странице, и мне пришлось тщательно разделить операторы,
чтобы избежать этой проблемы.

Использование ключевого слова else
Ключевое слово else можно использовать для создания
дополнительных предложений в операторе if, как показано в листинге
6-6.
package main
import "fmt"

func main() {
kayakPrice := 275.00

}

if (kayakPrice > 500) {
fmt.Println("Price is greater than 500")
} else if (kayakPrice < 300) {
fmt.Println("Price is less than 300")
}

Листинг 6-6 Использование ключевого слова else в файле main.go в папке flowcontrol

Когда ключевое слово else сочетается с ключевым словом if,
операторы кода в фигурных скобках выполняются только тогда, когда
выражение true, а выражение в предыдущем предложении false, как
показано на рисунке 6-3.

Рисунок 6-3 Предложение else/if в операторе if

В листинге 6-6 выражение, используемое в предложении if, дает
ложный результат, поэтому выполнение переходит к выражению
else/if, которое дает истинный результат. Код в листинге 6-6 выдает
следующий результат при компиляции и выполнении:
Price is less than 300
Комбинация else/if может быть повторена для создания
последовательности предложений, как показано в листинге 6-7, каждое
из которых будет выполняться только тогда, когда все предыдущие
выражения были false.

package main
import "fmt"
func main() {
kayakPrice := 275.00
if (kayakPrice > 500) {
fmt.Println("Price is greater than 500")
} else if (kayakPrice < 100) {
fmt.Println("Price is less than 100")
} else if (kayakPrice > 200 && kayakPrice < 300) {
fmt.Println("Price is between 200 and 300")
}
}
Листинг 6-7 Определение нескольких предложений else/if в файле main.go в папке
flowcontrol

Выполнение проходит через оператор if, оценивая выражения до
тех пор, пока не будет получено истинное значение или пока не
останется вычисляемых выражений. Код в листинге 6-7 выдает
следующий результат при компиляции и выполнении:
Price is between 200 and 300
Ключевое слово else можно также использовать для создания
резервного предложения, операторы которого будут выполняться только
в том случае, если все выражения if и else/if в операторе дадут
ложные результаты, как показано в листинге 6-8.
package main
import "fmt"
func main() {
kayakPrice := 275.00
if (kayakPrice > 500) {
fmt.Println("Price is greater than 500")
} else if (kayakPrice < 100) {

fmt.Println("Price is less than 100")
} else {
fmt.Println("Price not matched
expressions")
}
}

by

earlier

Листинг 6-8 Создание резервного предложения в файле main.go в папке flowcontrol

Предложение резервного варианта должно быть определено в конце
оператора и указывается с помощью ключевого слова else без
выражения, как показано на рисунке 6-4.

Рисунок 6-4 Резервное предложение в операторе if

Код в листинге 6-8 выдает следующий результат при компиляции и
выполнении:
Price not matched by earlier expressions

Понимание области действия оператора if
Каждое предложение в операторе if имеет свою собственную область
видимости, что означает, что доступ к переменным возможен только в
пределах предложения, в котором они определены. Это также означает,
что вы можете использовать одно и то же имя переменной для разных
целей в отдельных предложениях, как показано в листинге 6-9.
package main
import "fmt"

func main() {
kayakPrice := 275.00

}

if (kayakPrice > 500) {
scopedVar := 500
fmt.Println("Price is greater than", scopedVar)
} else if (kayakPrice < 100) {
scopedVar := "Price is less than 100"
fmt.Println(scopedVar)
} else {
scopedVar := false
fmt.Println("Matched: ", scopedVar)
}

Листинг 6-9 Использование области видимости в файле main.go в папке flowcontrol

Каждое предложение в операторе if определяет переменную с
именем scopedVar, и каждая из них имеет свой тип. Каждая
переменная является локальной для своего предложения, что означает,
что к ней нельзя получить доступ в других предложениях или вне
оператора if. Код в листинге 6-9 выдает следующий результат при
компиляции и выполнении:
Matched: false

Использование оператора инициализации с оператором if
Go позволяет оператору if использовать оператор инициализации,
который выполняется перед вычислением выражения оператора if.
Оператор инициализации ограничен простым оператором Go, что
означает, в общих чертах, что оператор может определять новую
переменную, присваивать новое значение существующей переменной
или вызывать функцию.
Чаще всего эта функция используется для инициализации
переменной, которая впоследствии используется в выражении, как
показано в листинге 6-10.
package main
import (

)

"fmt"
"strconv"

func main() {
priceString := "275"
if kayakPrice, err := strconv.Atoi(priceString); err ==
nil {
fmt.Println("Price:", kayakPrice)
} else {
fmt.Println("Error:", err)
}
}
Листинг 6-10 Использование оператора инициализации в файле main.go в папке
flowcontrol

За ключевым словом if следует оператор инициализации, затем
точка с запятой и вычисляемое выражение, как показано на рисунке 6-5.

Рисунок 6-5 Использование оператора инициализации

Оператор инициализации в листинге 6-10 вызывает функцию
strconv.Atoi, описанную в главе 5, для преобразования строки в
значение типа int. Функция возвращает два значения, которые
присваиваются переменным с именами kayakPrice и err:
...
if kayakPrice, err := strconv.Atoi(priceString); err == nil {
...
Областью действия переменных, определенных оператором
инициализации, является весь оператор if, включая выражение.
Переменная err используется в выражении оператора if, чтобы
определить, была ли строка проанализирована без ошибок:
...

if kayakPrice, err := strconv.Atoi(priceString); err == nil {
...
Переменные также можно использовать в предложении if и любых
предложениях else/if и else:
...
if kayakPrice, err := strconv.Atoi(priceString); err == nil {
fmt.Println("Price:", kayakPrice)
} else {
fmt.Println("Error:", err)
}
...
Код в листинге 6-10 выдает следующий результат при компиляции и
выполнении:
Price: 275
ИСПОЛЬЗОВАНИЕ СКОБОК С ПРЕДСТАВИТЕЛЯМИ
ИНИЦИАЛИЗАЦИИ
Как я объяснял ранее, я обычно использую круглые скобки для
заключения выражений в операторах if. Это по-прежнему возможно
при использовании оператора инициализации, но вы должны
убедиться, что круглые скобки применяются только к выражению,
например:
...
if kayakPrice, err := strconv.Atoi(priceString); (err ==
nil) {
...
Круглые скобки нельзя применять к инструкции инициализации
или заключать обе части инструкции.

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

повторяться бесконечно, если их не прервет ключевое слово break, как
показано в листинге 6-11. (Ключевое слово return также может
использоваться для завершения цикла.)
package main
import (
"fmt"
//"strconv"
)
func main() {

}

counter := 0
for {
fmt.Println("Counter:", counter)
counter++
if (counter > 3) {
break
}
}

Листинг 6-11 Использование базового цикла в файле main.go в папке flowcontrol

За ключевым словом for следуют инструкции для повторения,
заключенные в фигурные скобки, как показано на рисунке 6-6. Для
большинства циклов одним из операторов будет ключевое слово break,
завершающее цикл.

Рисунок 6-6 Базовый цикл for

Ключевое слово break в листинге 6-11 содержится внутри
оператора if, что означает, что цикл не прерывается до тех пор, пока
выражение оператора if не даст истинное значение. Код в листинге 611 выдает следующий результат при компиляции и выполнении:
Counter:
Counter:
Counter:
Counter:

0
1
2
3

Включение условия в цикл
Цикл, показанный в предыдущем разделе, представляет собой обычное
требование, которое должно повторяться до тех пор, пока не будет
достигнуто условие. Это настолько распространенное требование, что
условие может быть включено в синтаксис цикла, как показано в
листинге 6-12.
package main
import (
"fmt"
//"strconv"
)

func main() {

}

counter := 0
for (counter 3) {
//
break
// }
}

Листинг 6-12 Использование условия цикла в файле main.go в папке flowcontrol

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

Рисунок 6-7 Условие цикла for

Операторы, заключенные в фигурные скобки, будут выполняться
повторно, пока условие оценивается как true. В этом примере условие
возвращает true, пока значение переменной counter меньше или равно
3, а код выдает следующие результаты при компиляции и выполнении:
Counter:
Counter:
Counter:
Counter:

0
1
2
3

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

Подсказка
Как и в случае с оператором if, круглые скобки могут быть
применены к условию оператора for, но не к операторам
инициализации или пост-операторам.
package main
import (
"fmt"
//"strconv"
)
func main() {

}

for counter := 0; counter 3) {
break
}
}

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

Продолжение цикла
Ключевое слово continue можно использовать для прекращения
выполнения операторов цикла for для текущего значения и перехода к
следующей итерации, как показано в листинге 6-14.
package main
import (
"fmt"
//"strconv"
)
func main() {

}

for counter := 0; counter = 3 && counter < 7:
fmt.Println(counter, "is >= 3 && < 7")
default:
fmt.Println(counter, "is >= 7")
}
}

Листинг 6-26 Использование выражений в операторе switch в файле main.go в папке
flowcontrol

Когда значение сравнения опущено, каждый оператор case
указывается с условием. При выполнении оператора switch каждое
условие оценивается до тех пор, пока одно из них не даст true
результат или пока не будет достигнуто необязательное предложение
default. Листинг 6-26 производит следующий вывод, когда проект
компилируется и выполняется:
Zero
1 is
2 is
3 is
4 is
5 is
6 is

value
< 3
< 3
>= 3 &&
>= 3 &&
>= 3 &&
>= 3 &&

<
<
<
<

7
7
7
7

7 is >= 7
8 is >= 7
9 is >= 7

Использование операторов меток
Операторы меток позволяют выполнять переход к другой точке,
обеспечивая большую гибкость, чем другие функции управления
потоком. В листинге 6-27 показано использование оператора метки.
package main
import (
"fmt"
//"strconv"
)
func main() {

}

counter := 0
target: fmt.Println("Counter", counter)
counter++
if (counter < 5) {
goto target
}

Листинг 6-27 Использование оператора Label в файле main.go в папке flowcontrol

Метки определяются именем, за которым следует двоеточие, а затем
обычный оператор кода, как показано на рисунке 6-14. Ключевое слово
goto используется для перехода к метке.

Рисунок 6-14 Маркировка заявления

Подсказка
Существуют ограничения на то, когда вы можете перейти к метке,
например, невозможность перехода к оператору case из-за пределов
охватывающего его оператора switch.
Имя, присвоенное метке в этом примере, — target. Когда выполнение
достигает ключевого слова goto, оно переходит к оператору с
указанной меткой. Эффект представляет собой базовый цикл, который
вызывает увеличение значения переменной counter, пока оно меньше
5. При компиляции и выполнении листинга 6-27 выводится следующий
результат:
Counter
Counter
Counter
Counter
Counter

0
1
2
3
4

Резюме
В этой главе я описал функции управления потоком Go. Я объяснил, как
условно выполнять операторы с операторами if и switch и как
многократно выполнять операторы с помощью цикла for. Как показано
в этой главе, в Go меньше ключевых слов управления потоком, чем в

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

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

Ответ

Кто они
такие?

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

Почему они
полезны?

Эти классы коллекций являются удобным способом отслеживать связанные
значения данных.

Как они
Каждый тип коллекции можно использовать с литеральным синтаксисом или
используются? с помощью функции make.
Есть ли
подводные
камни или
ограничения?

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

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

Таблица 7-2 суммирует главу.
Таблица 7-2 Краткое содержание главы
Проблема

Решение

Листинг

Хранить фиксированное
количество значений

Использовать массив

4–8

Сравнить массивы

Используйте операторы сравнения

9

Проблема

Решение

Листинг

Перечислить массив

Используйте цикл for с ключевым словом range

10, 11

Хранить переменное
количество значений

Используйте срез

12–13,
16, 17,
23

Добавить элемент в срез

Используйте функцию append

14–15,
18, 20–
22

Создать срез из существующего Используйте диапазон
массива или выберите элементы
из среза

19, 24

Скопировать элементы в срез

Используйте функцию copy

25, 29

Удалить элементы из среза

Используйте функцию append с диапазонами,
которые пропускают элементы для удаления

30

Перечислить срез

Используйте цикл for с ключевым словом range

31

Сортировка элементов в срезе

Используйте пакет sort

32

Сравнить срезы

Используйте пакет reflect

33, 34

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

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

35

Хранить пары ключ-значение

Используйте карты

36–40

Удалить пару ключ-значение с
карты

Используйте функцию delete

41

Перечислить содержимое карты Используйте цикл for с ключевым словом range

42, 43

Чтение байтовых значений или
символов из строки

Используйте строку как массив или выполните
явное преобразование к типу []rune

44–48

Перечислить символы в строке

Используйте цикл for с ключевым словом range

49

Перечислить байты в строке

Выполните явное преобразование в тип []byte и
используйте цикл for с ключевым словом range.

50

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем collections.
Перейдите в папку collections и выполните команду, показанную в
листинге 7-1, чтобы инициализировать проект.
go mod init collections

Листинг 7-1 Инициализация проекта

Добавьте файл с именем main.go в папку collections с
содержимым, показанным в листинге 7-2.
Подсказка
Выможете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
package main
import "fmt"
func main() {
}

fmt.Println("Hello, Collections")
Листинг 7-2 Содержимое файла main.go в папке collections

Используйте командную строку для запуска команды, показанной в
листинге 7-3, в папке collections.
go run .
Листинг 7-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату::
Hello, Collections

Работа с массивами
Массивы Go имеют фиксированную длину и содержат элементы одного
типа, доступ к которым осуществляется по индексу, как показано в
листинге 7-4.
package main

import "fmt"
func main() {
var names [3]string
names[0] = "Kayak"
names[1] = "Lifejacket"
names[2] = "Paddle"
}

fmt.Println(names)

Листинг 7-4 Определение и использование массивов в файле main.go в папке collections

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

Рисунок 7-1 Определение массива

Массив создается и заполняется нулевым значением для типа
элемента. В этом примере массив names будет заполнен пустой строкой
(""), которая является нулевым значением для строкового типа. Доступ
к элементам массива осуществляется с использованием нотации
индекса с отсчетом от нуля, как показано на рисунке 7-2.

Рисунок 7-2 Доступ к элементу массива

Последний оператор в листинге 7-4 передает массив fmt.Println,
который создает строковое представление массива и записывает его в
консоль, производя следующий вывод после компиляции и выполнения
кода:
[Kayak Lifejacket Paddle]

Использование литерального синтаксиса массива
Массивы могут быть определены и заполнены в одном операторе с
использованием литерального синтаксиса, показанного в листинге 7-5.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
fmt.Println(names)
}
Листинг 7-5 Использование литерального синтаксиса массива в файле main.go в папке
collections

За типом массива следуют фигурные скобки, содержащие элементы,
которые будут заполнять массив, как показано на рисунке 7-3.

Рисунок 7-3 Синтаксис литерального массива

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

которой не указано значение, будет присвоено нулевое значение для
типа массива.
Код в листинге 7-5 выдает следующий результат при компиляции и
выполнении:
[Kayak Lifejacket Paddle]
СОЗДАНИЕ МНОГОМЕРНЫХ МАССИВОВ
Массивы Go являются одномерными, но их можно комбинировать
для создания многомерных массивов, например:
...
var coords [3][3]int
...
Этот оператор создает массив, емкость которого равна 3 и
базовый тип которого является массивом int, также с емкостью 3,
создавая массив значений int 3×3. Отдельные значения указываются
с использованием двух позиций индекса, например:
...
coords[1][2] = 10
...
Синтаксис немного неудобен, особенно для массивов с большим
количеством измерений, но он функционален и соответствует
подходу Go к массивам.

Понимание типов массивов
Тип массива — это комбинация его размера и базового типа. Вот
оператор из листинга 7-5, определяющий массив:
...
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
...

Тип переменной name — [3]string, что означает массив с базовым
типом string и емкостью 3. Каждая комбинация базового типа и
емкости является отдельным типом, как показано в листинге 7-6.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
var otherArray [4]string = names
}

fmt.Println(names)
Листинг 7-6 Работа с типами массивов в файле main.go в папке collections

Базовые типы двух массивов в этом примере одинаковы, но
компилятор сообщит об ошибке, даже если емкость otherArray
достаточна для размещения элементов из массива names. Вот ошибка,
которую выдает компилятор:
.\main.go:9:9: cannot use names (type [3]string) as type
[4]string in assignment
ПОЗВОЛЯЕМ КОМПИЛЯТОРУ ОПРЕДЕЛЯТЬ ДЛИНУ
МАССИВА
При использовании литерального синтаксиса компилятор может
вывести длину массива из списка элементов, например:
...
names := [...]string { "Kayak", "Lifejacket", "Paddle" }
...
Явная длина заменяется тремя точками (...), что указывает
компилятору определять длину массива из литеральных значений.
Тип переменной names по-прежнему [3]string, и единственное
отличие состоит в том, что вы можете добавлять или удалять
литеральные значения, не обновляя при этом явно указанную длину.

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

Понимание значений массива
Как я объяснял в главе 4, Go по умолчанию работает со значениями, а
не со ссылками. Это поведение распространяется на массивы, что
означает, что присваивание массива новой переменной копирует массив
и копирует содержащиеся в нем значения, как показано в листинге 7-7.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
otherArray := names
names[0] = "Canoe"

}

fmt.Println("names:", names)
fmt.Println("otherArray:", otherArray)

Листинг 7-7 Присвоение массива новой переменной в файле main.go в папке collections

В этом примере я присваиваю массив names новой переменной с
именем otherArray, а затем изменяю значение нулевого индекса
массива names перед записью обоих массивов. При компиляции и
выполнении код выдает следующий вывод, показывающий, что массив
и его содержимое были скопированы:
names: [Canoe Lifejacket Paddle]
otherArray: [Kayak Lifejacket Paddle]
Указатель можно использовать для создания ссылки на массив, как
показано в листинге 7-8.
package main

import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
otherArray := &names
names[0] = "Canoe"

}

fmt.Println("names:", names)
fmt.Println("otherArray:", *otherArray)
Листинг 7-8 Использование указателя на массив в файле main.go в папке collections

Тип переменной otherArray — *[3]string, обозначающий
указатель на массив, способный хранить три строковых значения.
Указатель массива работает так же, как и любой другой указатель, и для
доступа к содержимому массива необходимо следовать. Код в листинге
7-8 выдает следующий результат при компиляции и выполнении:
names: [Canoe Lifejacket Paddle]
otherArray: [Canoe Lifejacket Paddle]
Вы также можете создавать массивы, содержащие указатели, что
означает, что значения в массиве не копируются при копировании
массива. И, как я показал в главе 4, вы можете создавать указатели на
определенные позиции в массиве, которые обеспечат доступ к
значению в этом месте, даже если содержимое массива изменилось.

Сравнение массивов
Операторы сравнения == и != можно применять к массивам, как
показано в листинге 7-9.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }

}

moreNames := [3]string { "Kayak", "Lifejacket", "Paddle"
same := names == moreNames

}

fmt.Println("comparison:", same)

Листинг 7-9 Сравнение массивов в файле main.go в папке collections

Массивы равны, если они одного типа и содержат одинаковые
элементы в одном и том же порядке. Массивы names и moreNames
равны, потому что оба они являются массивами [3]string и содержат
одни и те же строковые значения. Код в листинге 7-9 выдает
следующий результат:
comparison: true

Перечисление массива
Массивы перечисляются с использованием ключевых слов for и range,
как показано в листинге 7-10.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
for index, value := range names {
fmt.Println("Index:", index, "Value:", value)
}
}
Листинг 7-10 Перечисление массива в файле main.go в папке collections

Я подробно описал циклы for в главе 6, но при использовании с
ключевым словом range ключевое слово for перечисляет содержимое
массива, создавая два значения для каждого элемента по мере
перечисления массива, как показано на рисунке 7-4.

Рисунок 7-4 Перечисление массива

Первое значение, присвоенное переменной index в листинге 7-10,
соответствует местоположению массива, которое перечисляется.
Второе значение, которое присваивается переменной с именем value в
листинге 7-10, присваивается элементу в текущем местоположении.
Листинг производит следующий вывод при компиляции и выполнении:
Index: 0 Value: Kayak
Index: 1 Value: Lifejacket
Index: 2 Value: Paddle
Go не позволяет определять переменные и не использовать их. Если
вам не нужны ни индекс, ни значение, вы можете использовать символ
подчеркивания (символ _) вместо имени переменной, как показано в
листинге 7-11.
package main
import "fmt"
func main() {
names := [3]string { "Kayak", "Lifejacket", "Paddle" }
for _, value := range names {
fmt.Println("Value:", value)
}
}
Листинг 7-11 Не использование текущего индекса в файле main.go в папке collections

Подчеркивание известно как пустой идентификатор и
используется, когда функция возвращает значения, которые
впоследствии не используются и для которых не следует назначать имя.
Код в листинге 7-11 отбрасывает текущий индекс по мере перечисления
массива и выдает следующий результат:
Value: Kayak
Value: Lifejacket
Value: Paddle

Работа со срезами
Лучше всего рассматривать срезы как массив переменной длины,
потому что они полезны, когда вы не знаете, сколько значений вам
нужно сохранить, или когда число меняется со временем. Один из
способов определить срез — использовать встроенную функцию make,
как показано в листинге 7-12.
package main
import "fmt"
func main() {
names := make([]string, 3)
names[0] = "Kayak"
names[1] = "Lifejacket"
names[2] = "Paddle"
fmt.Println(names)
}
Листинг 7-12 Определение среза в файле main.go в папке collections

Функция make принимает аргументы, определяющие тип и длину
среза, как показано на рисунке 7-5.

Рисунок 7-5 Создание среза

Тип среза в этом примере — []string, что означает срез,
содержащий строковые значения. Длина не является частью типа среза,
потому что размер срезов может варьироваться, как я продемонстрирую
позже в этом разделе. Срезы также можно создавать с использованием
литерального синтаксиса, как показано в листинге 7-13.
package main
import "fmt"
func main() {
names := []string {"Kayak", "Lifejacket", "Paddle"}
}

fmt.Println(names)

Листинг 7-13 Использование литерального синтаксиса в файле main.go в папке
collections

Синтаксис литерала среза подобен тому, который используется для
массивов, а начальная длина среза выводится из количества
литеральных значений, как показано на рисунке 7-6.

Рисунок 7-6 Использование синтаксиса литерала среза

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

которые могут быть сохранены в массиве. В этом примере и длина, и
емкость равны 3, как показано на рисунке 7-7.

Рисунок 7-7 Срез и его базовый массив

Срезы поддерживают нотацию индекса в стиле массива, которая
обеспечивает доступ к элементам базового массива. Хотя рисунке 7-7
представляет собой более реалистичное представление среза, на
рисунке 7-8 показано, как срез отображается в свой массив.

Рисунок 7-8 Срез и его базовый массив

Сопоставление между этим срезом и его массивом простое, но
срезы не всегда имеют такое прямое сопоставление со своим массивом,
как демонстрируют последующие примеры. Код в листинге 7-12 и
листинге 7-13 выдает следующий результат при компиляции и
выполнении:
[Kayak Lifejacket Paddle]

Добавление элементов в срез
Одним из ключевых преимуществ срезов является то, что их можно
расширять для размещения дополнительных элементов, как показано в
листинге 7-14.

package main
import "fmt"
func main() {
names := []string {"Kayak", "Lifejacket", "Paddle"}
names = append(names, "Hat", "Gloves")
}

fmt.Println(names)

Листинг 7-14 Добавление элементов к срезу в файле main.go в папке collections

Встроенная функция append принимает срез и один или несколько
элементов для добавления к срезу, разделенных запятыми, как показано
на рисунке 7-9.

Рисунок 7-9 Добавление элементов в срез

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

Рисунок 7-10 Результат добавления элементов в срез

Код в листинге 7-14 выдает после компиляции и выполнения
следующий вывод, показывающий добавление двух новых элементов в
срез:
[Kayak Lifejacket Paddle Hat Gloves]
Исходный срез и его базовый массив все еще существуют и могут
использоваться, как показано в листинге 7-15.
package main
import "fmt"
func main() {
names := []string {"Kayak", "Lifejacket", "Paddle"}
appendedNames := append(names, "Hat", "Gloves")
names[0] = "Canoe"

}

fmt.Println("names:", names)
fmt.Println("appendedNames:", appendedNames)
Листинг 7-15 Добавление элементов к срезу в файле main.go в папке collections

В этом примере результат функции append присваивается другой
переменной, в результате чего получается два среза, один из которых
был создан из другого. Каждый срез имеет базовый массив, и срезы
независимы. Код в листинге 7-15 выдает следующий результат при
компиляции и выполнении, показывающий, что изменение значения с
использованием одного среза не влияет на другой срез:
names: [Canoe Lifejacket Paddle]
appendedNames: [Kayak Lifejacket Paddle Hat Gloves]

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

можете указать дополнительную емкость при использовании функции
make, как показано в листинге 7-16.
package main
import "fmt"
func main() {
names := make([]string, 3, 6)
names[0] = "Kayak"
names[1] = "Lifejacket"
names[2] = "Paddle"

}

fmt.Println("len:", len(names))
fmt.Println("cap:", cap(names))

Листинг 7-16 Выделение дополнительной емкости в файле main.go в папке collections

Как отмечалось ранее, срезы имеют длину и емкость. Длина среза
— это количество значений, которые он может содержать в данный
момент, а емкость — это количество элементов, которые могут быть
сохранены в базовом массиве, прежде чем размер среза должен быть
изменен и создан новый массив. Емкость всегда будет не меньше
длины, но может быть больше, если с помощью функции make была
выделена дополнительная емкость. Вызов функции make в листинге 716 создает срез длиной 3 и емкостью 6, как показано на рисунке 7-11.

Рисунок 7-11 Выделение дополнительной емкости

Подсказка
Вы также можете использовать функции len и cap для стандартных
массивов фиксированной длины. Обе функции будут возвращать
длину массива, так что для массива типа [3]string, например, обе

функции вернут 3. См. пример в разделе «Использование функции
копирования»..
Встроенные функции len и cap возвращают длину и емкость среза. Код
в листинге 7-16 выдает следующий результат при компиляции и
выполнении:
len: 3
cap: 6
В результате базовый массив для среза имеет пространство для
роста, как показано на рисунке 7-12.

Рисунок 7-12 Срез, базовый массив которого имеет дополнительную емкость

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

import "fmt"
func main() {
names := make([]string, 3, 6)
names[0] = "Kayak"
names[1] = "Lifejacket"
names[2] = "Paddle"
appendedNames := append(names, "Hat", "Gloves")
names[0] = "Canoe"

}

fmt.Println("names:",names)
fmt.Println("appendedNames:", appendedNames)

Листинг 7-17 Добавление элементов в срез в файле main.go в папке collections

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

Рисунок 7-13 Несколько срезов, поддерживаемых одним массивом

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

можно увидеть в выводе кода в листинге 7-17:
names: [Canoe Lifejacket Paddle]
appendedNames: [Canoe Lifejacket Paddle Hat Gloves]

Добавление одного среза к другому
Функцию append можно использовать для добавления одного среза к
другому, как показано в листинге 7-18.
package main
import "fmt"
func main() {
names := make([]string, 3, 6)
names[0] = "Kayak"
names[1] = "Lifejacket"
names[2] = "Paddle"
moreNames := []string { "Hat Gloves"}
appendedNames := append(names, moreNames...)
}

fmt.Println("appendedNames:", appendedNames)

Листинг 7-18 Добавление среза в файл main.go в папку collections

За вторым аргументом следуют три точки (...), которые
необходимы, поскольку встроенная функция append определяет
переменный параметр, который я описываю в главе 8. Для этой главы
достаточно знать, что вы можете добавлять содержимое одного среза в
другой срез, пока используются три точки. (Если вы опустите три
точки, компилятор Go сообщит об ошибке, потому что он решит, что вы
пытаетесь добавить второй срез как одно значение к первому срезу, и
знает, что типы не совпадают.) Код в листинге 7-18 производит
следующий вывод при компиляции и выполнении:
appendedNames: [Kayak Lifejacket Paddle Hat Gloves]

Создание срезов из существующих массивов
Срезы можно создавать с использованием существующих массивов, что
основано на поведении, описанном в предыдущих примерах, и
подчеркивает природу срезов как представлений массивов. В листинге
7-19 определяется массив, который используется для создания срезов.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
someNames := products[1:3]
allNames := products[:]

}

fmt.Println("someNames:", someNames)
fmt.Println("allNames", allNames)

Листинг 7-19 Создание срезов из существующего массива в файле main.go в папке collections

Переменной
products
назначается
стандартный
массив
фиксированной длины, содержащий строковые значения. Массив
используется для создания срезов с использованием диапазона, в
котором указаны низкие и высокие значения, как показано на рисунке
7-14.

Рисунок 7-14 Использование диапазона для создания среза из существующего массива

Диапазоны выражены в квадратных скобках, где низкие и высокие
значения разделены двоеточием. Первый индекс в срезе
устанавливается как наименьшее значение, а длина является
результатом наибольшего значения минус наименьшее значение. Это
означает, что диапазон [1:3] создает диапазон, нулевой индекс
которого отображается в индекс 1 массива, а длина равна 2. Как
показывает этот пример, срезы не обязательно выравнивать с началом
резервного массива.
Начальный индекс и счетчик можно не указывать в диапазоне,
чтобы включить все элементы из источника, как показано на рисунке 715. (Вы также можете опустить только одно из значений, как показано в
последующих примерах.)

Рисунок 7-15 Диапазон, включающий все элементы

Код в листинге 7-19 создает два среза, каждый из которых
поддерживается одним и тем же массивом. Срез someNames имеет
частичное представление массива, тогда как срез allNames
представляет собой представление всего массива, как показано на
рисунке 7-16.

Рисунок 7-16 Создание срезов из существующих массивов

Код в листинге 7-19 выдает следующий результат при компиляции и
выполнении:

someNames: [Lifejacket Paddle]
allNames [Kayak Lifejacket Paddle Hat]

Добавление элементов при использовании существующих
массивов для срезов
Связь между срезом и существующим массивом может создавать
разные результаты при добавлении элементов.
Как показано в предыдущем примере, можно сместить срез так,
чтобы его первая позиция индекса не находилась в начале массива и
чтобы его конечный индекс не указывал на последний элемент массива.
В листинге 7-19 индекс 0 для среза someNames отображается в индекс 1
массива. До сих пор емкость срезов согласовывалась с длиной базового
массива, но это уже не так, поскольку эффект смещения заключается в
уменьшении объема массива, который может использоваться срезом. В
листинге 7-20 добавлены операторы, записывающие длину и емкость
двух срезов.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
someNames := products[1:3]
allNames := products[:]
fmt.Println("someNames:", someNames)
fmt.Println("someNames len:", len(someNames), "cap:",
cap(someNames))
fmt.Println("allNames", allNames)
fmt.Println("allNames len", len(allNames), "cap:",
cap(allNames))
}
Листинг 7-20 Отображение длины и емкости среза в файле main.go в папке collections

Код в листинге 7-20 выдает следующий вывод при компиляции и
выполнении, подтверждая эффект среза смещения:

someNames: [Lifejacket Paddle]
someNames len: 2 cap: 3
allNames [Kayak Lifejacket Paddle Hat]
allNames len 4 cap: 4
Листинг 7-21 добавляет элемент к срезу someNames.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
someNames := products[1:3]
allNames := products[:]
someNames = append(someNames, "Gloves")
fmt.Println("someNames:", someNames)
fmt.Println("someNames len:", len(someNames), "cap:",
cap(someNames))
fmt.Println("allNames", allNames)
fmt.Println("allNames len", len(allNames), "cap:",
cap(allNames))
}
Листинг 7-21 Добавление элемента к срезу в файле main.go в папке collections

Этот срез может вместить новый элемент без изменения размера, но
расположение массива, которое будет использоваться для хранения
элемента, уже включено в срез allNames, а это означает, что операция
append расширяет срез someNames и изменяет одно из значений,
которые можно получить через срез allNames, как показано на рисунке
7-17.

Рисунок 7-17 Добавление элемента в срез

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

Если вы увязли в срезах и не получили ожидаемых результатов,
спросите себя, к какой категории относится каждый из ваших срезов
и не обрабатываете ли вы срез непоследовательно или создаете
срезы из разных категорий из одного и того же исходного массива.
Если вы используете срез в качестве фиксированного представления
массива, вы можете ожидать, что несколько срезов дадут вам
согласованное представление этого массива, и любые новые значения,
которые вы назначите, будут отражены всеми срезами, которые
отображаются в измененный элемент.
Этот результат подтверждается выводом, полученным при
компиляции и выполнении кода в листинге 7-21:
someNames: [Lifejacket Paddle Gloves]
someNames len: 3 cap: 3
allNames [Kayak Lifejacket Paddle Gloves]
allNames len 4 cap: 4
Добавление значения Gloves к срезу someNames изменяет значение,
возвращаемое allNames[3], поскольку срезы используют один и тот же
массив.
Выходные данные также показывают, что длина и емкость срезов
одинаковы, что означает, что больше нет места для расширения среза
без создания большего резервного массива. Чтобы подтвердить это
поведение, в листинге 7-22 к срезу someNames добавляется еще один
элемент.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
someNames := products[1:3]
allNames := products[:]
someNames = append(someNames, "Gloves")

someNames = append(someNames, "Boots")
fmt.Println("someNames:", someNames)
fmt.Println("someNames len:", len(someNames), "cap:",
cap(someNames))
fmt.Println("allNames", allNames)
fmt.Println("allNames len", len(allNames), "cap:",
cap(allNames))
}
Листинг 7-22 Добавление еще одного элемента в файл main.go в папке collections

Первый вызов функции append расширяет срез someNames в
существующем базовом массиве. При повторном вызове функции
append дополнительной емкости не остается, поэтому создается новый
массив, содержимое копируется, а два среза поддерживаются разными
массивами, как показано на рисунке 7-18.

Рисунок 7-18 Изменение размера среза путем добавления элемента

Процесс изменения размера копирует только те элементы массива,
которые отображаются срезом, что приводит к повторному
выравниванию индексов среза и массива. Код в листинге 7-22 выдает
следующий результат при компиляции и выполнении:
someNames: [Lifejacket Paddle Gloves Boots]
someNames len: 4 cap: 6
allNames [Kayak Lifejacket Paddle Gloves]
allNames len 4 cap: 4

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

будут дублироваться, как показано в листинге 7-23.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
someNames := products[1:3:3]
allNames := products[:]
someNames = append(someNames, "Gloves")
//someNames = append(someNames, "Boots")
fmt.Println("someNames:", someNames)
fmt.Println("someNames len:", len(someNames), "cap:",
cap(someNames))
fmt.Println("allNames", allNames)
fmt.Println("allNames len", len(allNames), "cap:",
cap(allNames))
}
Листинг 7-23 Указание емкости среза в файле main.go в папке collections

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

Рисунок 7-19 Указание емкости в диапазоне

Максимальное значение не определяет максимальную емкость
напрямую. Вместо этого максимальная емкость определяется путем
вычитания нижнего значения из максимального значения. В примере
максимальное значение равно 3, а минимальное значение равно 1, что
означает, что емкость будет ограничена до 2. В результате операция
append приводит к изменению размера среза и выделению
собственного массива, вместо расширения в существующем массиве,
что можно увидеть в выводе кода в листинге 7-23:
someNames: [Lifejacket Paddle Gloves]
someNames len: 3 cap: 4
allNames [Kayak Lifejacket Paddle Hat]
allNames len 4 cap: 4
Изменение размера среза означает, что значение Gloves,
добавляемое к срезу someNames, не становится одним из значений,
сопоставленных срезом allNames.

Создание срезов из других срезов
Срезы также можно создавать из других срезов, хотя взаимосвязь
между срезами не сохраняется при изменении их размера. Чтобы
продемонстрировать, что это значит, в листинге 7-24 создается один
срез из другого.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
allNames := products[1:]
someNames := allNames[1:3]
allNames = append(allNames, "Gloves")
allNames[1] = "Canoe"
fmt.Println("someNames:", someNames)
fmt.Println("allNames", allNames)

}
Листинг 7-24 Создание среза из среза в файле main.go в папке collections

Диапазон, используемый для создания среза
применяется к allNames, который также является срезом:

someNames,

...
someNames := allNames[1:3]
...
Этот диапазон создает срез, который отображается на второй и
третий элементы среза allNames. Срез allNames был создан с
собственным диапазоном:
...
allNames := products[1:]
...
Диапазон создает срез, который отображается на все элементы
исходного массива, кроме первого. Эффекты диапазонов суммируются,
что означает, что срез someNames будет отображен на вторую и третью
позиции в массиве, как показано на рисунке 7-20.

Рисунок 7-20 Создание среза из среза

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

для определения отображений для срезов, поддерживаемых одним и
тем же массивом, как показано на рисунке 7-21.

Рисунок 7-21 Фактическое расположение срезов

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

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

Использование функции копирования для обеспечения
разделения массива срезов
Функцию copy можно использовать для дублирования существующего
среза, выбирая некоторые или все элементы, но гарантируя, что новый
срез поддерживается собственным массивом, как показано в листинге
7-25.
package main
import "fmt"
func main() {

products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
allNames := products[1:]
someNames := make([]string, 2)
copy(someNames, allNames)

}

fmt.Println("someNames:", someNames)
fmt.Println("allNames", allNames)

Листинг 7-25 Дублирование среза в файле main.go в папке collections

Функция copy принимает два аргумента: срез назначения и срез
источника, как показано на рисунке 7-22.

Рисунок 7-22 Использование встроенной функции копирования

Функция копирует элементы в целевой срез. Срезы не обязательно
должны иметь одинаковую длину, потому что функция copy будет
копировать элементы только до тех пор, пока не будет достигнут конец
целевого или исходного среза. Размер целевого среза не изменяется,
даже если в существующем резервном массиве есть доступная емкость,
а это означает, что вы должны убедиться, что его длина достаточна для
размещения количества элементов, которые вы хотите скопировать.
Эффект оператора copy в листинге 7-25 заключается в том, что
элементы копируются из среза allNames до тех пор, пока не будет
исчерпана длина среза someNames. Листинг производит следующий
вывод при компиляции и выполнении:
someNames: [Lifejacket Paddle]
allNames [Lifejacket Paddle Hat]
Длина среза someNames равна 2, что означает, что два элемента
копируются из среза allNames. Даже если бы срез someNames имел
дополнительную емкость, никакие другие элементы не были бы

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

Понимание ловушки неинициализированных срезов
Как я объяснял в предыдущем разделе, функция copy не изменяет
размер целевого среза. Распространенной ошибкой является попытка
скопировать элементы в срез, который не был инициализирован, как
показано в листинге 7-26.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
allNames := products[1:]
var someNames []string
copy(someNames, allNames)

}

fmt.Println("someNames:", someNames)
fmt.Println("allNames", allNames)

Листинг 7-26 Копирование элементов в неинициализированный срез в файле main.go в папке
collections

Я заменил оператор, который инициализирует срез someNames,
функцией make и заменил его оператором, который определяет
переменную someNames без ее инициализации. Этот код компилируется
и выполняется без ошибок, но дает следующие результаты:
someNames: []
allNames [Lifejacket Paddle Hat]
Никакие элементы не были скопированы в целевой срез. Это
происходит потому, что неинициализированные срезы имеют нулевую
длину и нулевую емкость. Функция copy останавливает копирование,
когда достигается длина конечной длины, и, поскольку длина равна

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

Указание диапазонов при копировании срезов
Детальный контроль над копируемыми элементами может быть
достигнут с помощью диапазонов, как показано в листинге 7-27.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
allNames := products[1:]
someNames := []string { "Boots", "Canoe"}
copy(someNames[1:], allNames[2:3])

}

fmt.Println("someNames:", someNames)
fmt.Println("allNames", allNames)

Листинг 7-27 Использование диапазонов при копировании элементов в файле main.go в папке
collections

Диапазон, примененный к целевому срезу, означает, что копируемые
элементы будут начинаться с позиции 1. Диапазон, примененный к
исходному срезу, означает, что копирование начнется с элемента в
позиции 2, и будет скопирован один элемент. Код в листинге 7-27
выдает следующий результат при компиляции и выполнении:
someNames: [Boots Hat]
allNames [Lifejacket Paddle Hat]

Копирование срезов разного размера
Поведение, которое приводит к проблеме, описанной в разделе
«Понимание ловушки неинициализированных срезов», позволяет

копировать срезы разных размеров, если вы помните об их
инициализации. Если целевой срез больше исходного, то копирование
будет продолжаться до тех пор, пока не будет скопирован последний
элемент в источнике, как показано в листинге 7-28.
package main
import "fmt"
func main() {
products := []string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
replacementProducts := []string { "Canoe", "Boots"}
copy(products, replacementProducts)
}

fmt.Println("products:", products)

Листинг 7-28 Копирование меньшего исходного среза в файл main.go в папке collections

Исходный срез содержит только два элемента, и диапазон не
используется. В результате функция copy начинает копирование
элементов из среза replaceProducts в срез products и
останавливается, когда достигается конец среза replaceProducts.
Остальные элементы в срезе продуктов не затрагиваются операцией
копирования, как показывают выходные данные примера:
products: [Canoe Boots Paddle Hat]
Если целевой срез меньше исходного, то копирование продолжается
до тех пор, пока все элементы в целевом срезе не будут заменены, как
показано в листинге 7-29.
package main
import "fmt"
func main() {

products := []string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
replacementProducts := []string { "Canoe", "Boots"}
copy(products[0:1], replacementProducts)
}

fmt.Println("products:", products)

Листинг 7-29 Копирование исходного среза большего размера в файл main.go в папке
collections

Диапазон, используемый для назначения, создает срез длиной один,
что означает, что из исходного массива будет скопирован только один
элемент, как показано в выводе примера:
products: [Canoe Lifejacket Paddle Hat]

Удаление элементов среза
Встроенной функции для удаления элементов среза нет, но эту
операцию можно выполнить с помощью диапазонов и функции
добавления, как показано в листинге 7-30.
package main
import "fmt"
func main() {
products := [4]string { "Kayak", "Lifejacket", "Paddle",
"Hat"}

}

deleted := append(products[:2], products[3:]...)
fmt.Println("Deleted:", deleted)

Листинг 7-30 Удаление элементов среза в файле main.go в папке collections

Чтобы удалить значение, метод append используется для
объединения двух диапазонов, содержащих все элементы среза, кроме
того, который больше не требуется. Листинг 7-30 дает следующий
результат при компиляции и выполнении:

Deleted: [Kayak Lifejacket Hat]

Перечисление срезов
Срезы нумеруются так же, как и массивы, с ключевыми словами for и
range, как показано в листинге 7-31.
package main
import "fmt"
func main() {
products := []string { "Kayak", "Lifejacket", "Paddle",
"Hat"}

}

for index, value := range products[2:] {
fmt.Println("Index:", index, "Value:", value)
}

Листинг 7-31 Перечисление среза в файле main.go в папке collections

Я описываю различные способы использования цикла for в
листинге 7-31, но в сочетании с ключевым словом range ключевое
слово for может перечислять срез, создавая переменные индекса и
значения для каждого элемента. Код в листинге 7-31 выдает следующий
результат:
Index: 0 Value: Paddle
Index: 1 Value: Hat

Сортировка срезов
Встроенной поддержки сортировки срезов нет, но стандартная
библиотека включает пакет sort, определяющий функции для
сортировки различных типов срезов. Пакет sort подробно описан в
главе 18, но в листинге 7-32 показан простой пример, обеспечивающий
некоторый контекст в этой главе.
package main
import (

)

"fmt"
"sort"

func main() {
products := []string { "Kayak", "Lifejacket", "Paddle",
"Hat"}
sort.Strings(products)

}

for index, value := range products {
fmt.Println("Index:", index, "Value:", value)
}

Листинг 7-32 Сортировка среза в файле main.go в папке collections

Функция Strings сортирует значения в []string на месте, получая
следующие результаты при компиляции и выполнении примера:
Index:
Index:
Index:
Index:

0
1
2
3

Value:
Value:
Value:
Value:

Hat
Kayak
Lifejacket
Paddle

Как объясняется в главе 18, пакет sort включает функции для
сортировки срезов, содержащих целые числа и строки, а также
поддержку сортировки пользовательских типов данных.

Сравнение срезов
Go ограничивает использование оператора сравнения, поэтому срезы
можно сравнивать только с нулевым значением. Сравнение двух срезов
приводит к ошибке, как показано в листинге 7-33.
package main
import (
"fmt"
//"sort"
)

func main() {
p1 := []string { "Kayak", "Lifejacket", "Paddle", "Hat"}
p2 := p1
}

fmt.Println("Equal:", p1 == p2)

Листинг 7-33 Сравнение срезов в файле main.go в папке collections

При компиляции этого кода возникает следующая ошибка:
.\main.go:13:30: invalid operation: p1 == p2 (slice can only
be compared to nil)
Однако есть один способ сравнения срезов. Стандартная библиотека
включает пакет с именем reflect, который включает в себя удобную
функцию DeepEqual. Пакет reflect описан в главах 27–29 и содержит
расширенные
функции
(именно
поэтому
для
описания
предоставляемых им функций требуется три главы). Функцию
DeepEqual можно использовать для сравнения более широкого
диапазона типов данных, чем оператор равенства, включая срезы, как
показано в листинге 7-34.
package main
import (
"fmt"
"reflect"
)
func main() {
p1 := []string { "Kayak", "Lifejacket", "Paddle", "Hat"}
p2 := p1
}

fmt.Println("Equal:", reflect.DeepEqual(p1, p2))
Листинг 7-34 Сравнение срезов удобной функцией в файле main.go в папке collections

Функция DeepEqual удобна, но вы должны прочитать главы,
описывающие пакет reflect, чтобы понять, как он работает, прежде

чем использовать его в своих проектах. Листинг производит
следующий вывод при компиляции и выполнении:
Equal: true

Получение массива, лежащего в основе среза
Если у вас есть срез, но вам нужен массив (обычно потому, что функция
требует его в качестве аргумента), вы можете выполнить явное
преобразование среза, как показано в листинге 7-35.
package main
import (
"fmt"
//"reflect"
)
func main() {
p1 := []string { "Kayak", "Lifejacket", "Paddle", "Hat"}
arrayPtr := (*[3]string)(p1)
array := *arrayPtr
fmt.Println(array)
}
Листинг 7-35 Получение массива в файле main.go в папке collections

Я выполнил эту задачу в два этапа. Первый шаг — выполнить явное
преобразование типа среза []string в *[3]string. Следует соблюдать
осторожность при указании типа массива, поскольку произойдет
ошибка, если количество элементов, требуемых массивом, превысит
длину среза. Длина массива может быть меньше длины среза, и в этом
случае массив не будет содержать все значения среза. В этом примере в
срезе четыре значения, и я указал тип массива, который может хранить
три значения, а это означает, что массив будет содержать только первые
три значения среза.
На втором шаге я следую за указателем, чтобы получить значение
массива, которое затем записывается. Код в листинге 7-35 выдает
следующий результат при компиляции и выполнении:

[Kayak Lifejacket Paddle]

Работа с картами
Карты — это встроенная структура данных, которая связывает значения
данных с ключами. В отличие от массивов, где значения связаны с
последовательными целочисленными ячейками, карты могут
использовать другие типы данных в качестве ключей, как показано в
листинге 7-36.
package main
import "fmt"
func main() {
products := make(map[string]float64, 10)
products["Kayak"] = 279
products["Lifejacket"] = 48.95

}

fmt.Println("Map size:", len(products))
fmt.Println("Price:", products["Kayak"])
fmt.Println("Price:", products["Hat"])

Листинг 7-36 Использование карты в файле main.go в папке collections

Карты создаются с помощью встроенной функции make, как и
срезы. Тип карты указывается с помощью ключевого слова map, за
которым следует тип ключа в квадратных скобках, за которым следует
тип значения, как показано на рисунке 7-23. Последний аргумент
функции make указывает начальную емкость карты. Карты, как и срезы,
изменяются автоматически, и аргумент размера может быть опущен.

Рисунок 7-23 Определение карты

Оператор в листинге 7-36 будет хранить значения float64, которые
индексируются string ключами. Значения хранятся на карте с
использованием синтаксиса в стиле массива, с указанием ключа вместо
местоположения, например:
...
products["Kayak"] = 279
...
Этот оператор сохраняет значение float64 с помощью ключа Kayak.
Значения считываются с карты с использованием того же синтаксиса:
...
fmt.Println("Price:", products["Kayak"])
...
Если карта содержит указанный ключ, возвращается значение,
связанное с ключом. Нулевое значение для типа значения карты
возвращается, если карта не содержит ключ. Количество элементов,
хранящихся на карте, получается с помощью встроенной функции len,
например:
...
fmt.Println("Map size:", len(products))
...
Код в листинге 7-36 выдает следующий результат при компиляции и
выполнении:
Map size: 2
Price: 279
Price: 0

Использование литерального синтаксиса карты
Срезы также могут быть определены с использованием литерального
синтаксиса, как показано в листинге 7-37.
package main
import "fmt"

func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
}
fmt.Println("Map size:", len(products))
fmt.Println("Price:", products["Kayak"])
fmt.Println("Price:", products["Hat"])
}
Листинг 7-37 Использование литерального синтаксиса карты в файле main.go в папке
collections

Литеральный синтаксис указывает начальное содержимоекарты
между фигурными скобками. Каждая запись карты указывается с
помощью ключа, двоеточия, значения и запятой, как показано на
рисунке 7-24.

Рисунок 7-24 Литеральный синтаксис карты

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

Ключи, используемые в литеральном синтаксисе, должны быть
уникальными, и компилятор сообщит об ошибке, если одно и то же имя
используется для двух литеральных записей. Листинг 7-37 дает
следующий результат при компиляции и выполнении:
Map size: 2
Price: 279
Price: 0

Проверка элементов в карте
Как отмечалось ранее, карты возвращают нулевое значение для типа
значения, когда выполняются чтения, для которых нет ключа. Это
может затруднить различение сохраненного значения, которое
оказывается нулевым значением, и несуществующего ключа, как
показано в листинге 7-38.
package main
import "fmt"
func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}
}

fmt.Println("Hat:", products["Hat"])

Листинг 7-38 Чтение значений карты в файле main.go в папке collections

Проблема с этим кодом заключается в том, что products["Hat"]
возвращает ноль, но неизвестно, связано ли это с тем, что ноль является
сохраненным значением, или с тем, что с ключом Hat не связано
никакого значения. Чтобы решить эту проблему, карты создают два
значения при чтении значения, как показано в листинге 7-39.
package main

import "fmt"
func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}
value, ok := products["Hat"]

}

if (ok) {
fmt.Println("Stored value:", value)
} else {
fmt.Println("No stored value")
}
Листинг 7-39 Определение наличия значения на карте в файле main.go в папке collections

Это известно как метод «запятая ок», когда значения присваиваются
двум переменным при чтении значения из карты:
...
value, ok := products["Hat"]
...
Первое значение — это либо значение, связанное с указанным
ключом, либо нулевое значение, если ключ отсутствует. Второе
значение — это логическое значение, которое равно true, если карта
содержит указанный ключ, и false в противном случае. Второе
значение обычно присваивается переменной с именем ok, откуда и
возникает термин «запятая ok».
Этот метод можно упростить с помощью оператора инициализации,
как показано в листинге 7-40.
package main
import "fmt"
func main() {

products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}

}

if value, ok := products["Hat"]; ok {
fmt.Println("Stored value:", value)
} else {
fmt.Println("No stored value")
}

Листинг 7-40 Использование оператора инициализации в файле main.go в папке
collections

Код в листингах 7-39 и 7-39 выдает после компиляции и
выполнения следующий вывод, показывающий, что ключ Hat
использовался для сохранения значения 0 в карте:
Stored value: 0

Удаление объектов с карты
Элементы удаляются с карты с помощью встроенной функции
удаления, как показано в листинге 7-41.
package main
import "fmt"
func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}
delete(products, "Hat")
if value, ok := products["Hat"]; ok {
fmt.Println("Stored value:", value)
} else {

}

fmt.Println("No stored value")

}
Листинг 7-41 Удаление с карты в файле main.go в папке collections

Аргументами функции delete являются карта и ключ для удаления.
Об ошибке не будет сообщено, если указанный ключ не содержится в
карте. Код в листинге 7-41 выдает следующий результат при
компиляции и выполнении, подтверждая, что ключ Hat больше не
находится в карте:
No stored value

Перечисление содержимого карты
Карты перечисляются с использованием ключевых слов for и range,
как показано в листинге 7-42.
package main
import "fmt"
func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}

}

for key, value := range products {
fmt.Println("Key:", key, "Value:", value)
}

Листинг 7-42 Перечисление карты в файле main.go в папке collections

Когда ключевые слова for и range используются с картой, двум
переменным присваиваются ключи и значения по мере перечисления
содержимого карты. Код в листинге 7-42 выдает следующий результат
при компиляции и выполнении (хотя они могут отображаться в другом
порядке, как я объясню в следующем разделе):

Key: Kayak Value: 279
Key: Lifejacket Value: 48.95
Key: Hat Value: 0

Перечисление карты по порядку
Вы можете увидеть результаты из листинга 7-42 в другом порядке,
потому что нет никаких гарантий, что содержимое карты будет
пронумеровано в каком-либо конкретном порядке. Если вы хотите
получить значения на карте по порядку, то лучший подход —
перечислить карту и создать срез, содержащий ключи, отсортировать
срез, а затем пронумеровать срез для чтения значений с карты, как
показано на Листинг 7-43.
package main
import (
"fmt"
"sort"
)
func main() {
products := map[string]float64 {
"Kayak" : 279,
"Lifejacket": 48.95,
"Hat": 0,
}

}

keys := make([]string, 0, len(products))
for key, _ := range products {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Println("Key:", key, "Value:", products[key])
}

Листинг 7-43 Перечисление карты в ключевом порядке в файле main.go в папке collections

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

ключа:
Key: Hat Value: 0
Key: Kayak Value: 279
Key: Lifejacket Value: 48.95

Понимание двойной природы строк
В главе 4 я описал строки как последовательности символов. Это
правда, но есть сложности, потому что строки Go имеют раздвоение
личности в зависимости от того, как вы их используете.
Go обрабатывает строки как массивы байтов и поддерживает
нотацию индекса массива и диапазона среза, как показано в листинге 744.
package main
import (
"fmt"
"strconv"
)
func main() {
var price string = "$48.95"
var currency byte = price[0]
var amountString string = price[1:]
amount, parseErr := strconv.ParseFloat(amountString, 64)

}

fmt.Println("Currency:", currency)
if (parseErr == nil) {
fmt.Println("Amount:", amount)
} else {
fmt.Println("Parse Error:", parseErr)
}

Листинг 7-44 Индексирование и создание среза строки в файле main.go в папке
collections

Я использовал полный синтаксис объявления переменных, чтобы
подчеркнуть тип каждой переменной. Когда используется нотация
индекса, результатом является byte из указанного места в строке:
...
var currency byte = price[0]
...
Этот оператор выбирает byte в нулевой позиции и присваивает его
переменной с именем currency. Когда строка нарезается, срез также
описывается с использованием байтов, но результатом является string:
...
var amountString string = price[1:]
...
Диапазон выбирает все, кроме байта в нулевом местоположении, и
присваивает укороченную строку переменной с именем amountString.
Этот код выдает следующий результат при компиляции и выполнении с
помощью команды, показанной в листинге 7-44:
Currency: 36
Amount: 48.95
Как я объяснял в главе 4, тип byte является псевдонимом для uint8,
поэтому значение currency отображается в виде числа: Go понятия не
имеет, что числовое значение 36 должно выражаться знаком доллара.
На рисунке 7-25 строка представлена как массив байтов и показано, как
они индексируются и нарезаются.

Рисунок 7-25 Строка как массив байтов

При разрезании строки получается другая строка, но для
интерпретации byte как символа, который он представляет, требуется

явное преобразование, как показано в листинге 7-45.
package main
import (
"fmt"
"strconv"
)
func main() {
var price string = "$48.95"
var currency string = string(price[0])
var amountString string = price[1:]
amount, parseErr := strconv.ParseFloat(amountString, 64)

}

fmt.Println("Currency:", currency)
if (parseErr == nil) {
fmt.Println("Amount:", amount)
} else {
fmt.Println("Parse Error:", parseErr)
}
Листинг 7-45 Преобразование результата в файл main.go в папку collections

Скомпилируйте и выполните код, и вы увидите следующие
результаты:
Currency: $
Amount: 48.95
Похоже, что это работает, но в нем есть ловушка, которую можно
увидеть, если изменить символ валюты, как показано в листинге 7-46.
(Если вы не живете в той части мира, где на клавиатуре есть символ
валюты евро, удерживайте нажатой клавишу Alt и нажмите 0128 на
цифровой клавиатуре.)
package main
import (
"fmt"

)

"strconv"

func main() {
var price string = "€48.95"
var currency string = string(price[0])
var amountString string = price[1:]
amount, parseErr := strconv.ParseFloat(amountString, 64)

}

fmt.Println("Currency:", currency)
if (parseErr == nil) {
fmt.Println("Amount:", amount)
} else {
fmt.Println("Parse Error:", parseErr)
}
Листинг 7-46 Изменение символа валюты в файле main.go в папке collections

Скомпилируйте и выполните код, и вы увидите вывод, подобный
следующему:
Currency: â
Parse Error: strconv.ParseFloat:
invalid syntax

parsing

"\x82\xac48.95":

Проблема в том, что нотации массива и диапазона выбирают байты,
но не все символы выражаются одним байтом. Новый символ валюты
хранится в трех байтах, как показано на рисунке 7-26.

Рисунок 7-26 Изменение символа валюты

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

остальная часть строки. Вы можете подтвердить, что изменение
символа валюты увеличило размер массива, используя функцию len,
как показано в листинге 7-47.
package main
import (
"fmt"
"strconv"
)
func main() {
var price string = "€48.95"
var currency string = string(price[0])
var amountString string = price[1:]
amount, parseErr := strconv.ParseFloat(amountString, 64)

}

fmt.Println("Length:", len(price))
fmt.Println("Currency:", currency)
if (parseErr == nil) {
fmt.Println("Amount:", amount)
} else {
fmt.Println("Parse Error:", parseErr)
}
Листинг 7-47 Получение длины строки в файле main.go в папке collections

Функция len обрабатывает строку как массив байтов, и код в
листинге 7-47 выдает следующий результат при компиляции и
выполнении:
Length: 8
Currency: â
Parse Error: strconv.ParseFloat:
invalid syntax

parsing

"\x82\xac48.95":

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

Преобразование строки в руны
Тип rune представляет собой кодовую точку Unicode, которая по сути
является одним символом. Чтобы избежать нарезки строк в середине
символов, можно выполнить явное преобразование в срез рун, как
показано в листинге 7-48.
Подсказка
Юникод невероятно сложен, чего и следовало ожидать от любого
стандарта, целью которого является описание нескольких систем
письма, которые развивались на протяжении тысячелетий. В этой
книге я не описываю Unicode и для простоты рассматриваю значения
rune как одиночные символы, чего достаточно для большинства
проектов разработки. Я достаточно описываю Unicode, чтобы
объяснить, как работают функции Go.
package main
import (
"fmt"
"strconv"
)
func main() {
var price []rune = []rune("€48.95")
var currency string = string(price[0])
var amountString string = string(price[1:])
amount, parseErr := strconv.ParseFloat(amountString, 64)

}

fmt.Println("Length:", len(price))
fmt.Println("Currency:", currency)
if (parseErr == nil) {
fmt.Println("Amount:", amount)
} else {
fmt.Println("Parse Error:", parseErr)
}

Листинг 7-48 Преобразование в руны в файле main.go в папке collections

Я применяю явное преобразование к литеральной строке и
присваиваю срез переменной price. При работе со срезом рун
отдельные байты группируются в символы, которые они представляют,
без ссылки на количество байтов, которое требуется для каждого
символа, как показано на рисунке 7-27.

Рисунок 7-27 Срез руны

Как объяснялось в главе 4, тип rune является псевдонимом для
int32, что означает, что при печати значения руны будет отображаться
числовое значение, используемое для представления символа. Это
означает, что, как и в предыдущем примере с байтами, я должен
выполнить явное преобразование одной руны в строку, например:
...
var currency string = string(price[0])
...
Но, в отличие от предыдущих примеров, я также должен выполнить
явное преобразование создаваемого среза, например::
...
var amountString string = string(price[1:])
...
Результатом среза является []rune; иными словами, разрезание
среза руны дает другой срез руны. Код в листинге 7-48 выдает
следующий результат при компиляции и выполнении:
Length: 6
Currency: €
Amount: 48.95
Функция len возвращает 6, поскольку массив содержит символы, а
не байты. И, конечно же, остальная часть вывода соответствует

ожиданиям, потому что нет потерянных байтов, которые могли бы
повлиять на результат.
ПОНИМАНИЕ ПОЧЕМУ И БАЙТЫ, И РУНЫ ПОЛЕЗНЫ
Подход, который Go использует для строк, может показаться
странным, но у него есть свое применение. Байты важны, когда вы
заботитесь о хранении строк, и вам нужно знать, сколько места
нужно выделить. Символы важны, когда вы имеете дело с
содержимым строк, например, при вставке нового символа в
существующую строку.
Обе грани строк важны. Однако важно понимать, нужно ли вам
иметь дело с байтами или символами для той или иной операции.
У вас может возникнуть соблазн работать только с байтами, что
будет работать до тех пор, пока вы используете только те символы,
которые представлены одним байтом, что обычно означает ASCII.
Сначала это может сработать, но почти всегда заканчивается плохо,
особенно когда ваш код обрабатывает символы, введенные
пользователем с набором символов, отличным от ASCII, или
обрабатывает файл, содержащий данные, отличные от ASCII. Для
небольшого объема дополнительной работы проще и безопаснее
признать, что Unicode действительно существует, и полагаться на Go
для преобразования байтов в символы.

Перечисление строк
Цикл for можно использовать для перечисления содержимого строки.
Эта функция показывает некоторые умные аспекты того, как Go
работает с отображением байтов в руны. В листинге 7-49 перечисляется
строка.
package main
import (
"fmt"
//"strconv"
)
func main() {

var price = "€48.95"

}

for index, char := range price {
fmt.Println(index, char, string(char))
}

Листинг 7-49 Перечисление строки в файле main.go в папке collections

В этом примере я использовал строку, содержащую символ валюты
евро, что демонстрирует, что Go обрабатывает строки как
последовательность рун при использовании с циклом for.
Скомпилируйте и выполните код из листинга 7-49, и вы получите
следующий вывод:
0
3
4
5
6
7

8364 €
52 4
56 8
46 .
57 9
53 5

Цикл for обрабатывает строку как массив элементов. Записанные
значения представляют собой индекс текущего элемента, числовое
значение этого элемента и числовой элемент, преобразованный в
строку.
Обратите внимание, что значения индекса не являются
последовательными.
Цикл
for
обрабатывает
строку
как
последовательность
символов,
полученную
из
базовой
последовательности байтов. Значения индекса соответствуют первому
байту, из которого состоит каждый символ, как показано на рисунке 7-2.
Второе значение индекса равно 3, например, потому что первый символ
в строке состоит из байтов в позициях 0, 1 и 2.
Если вы хотите перечислить базовые байты без их преобразования в
символы, вы можете выполнить явное преобразование в байтовый срез,
как показано в листинге 7-50.
package main
import (
"fmt"

)

//"strconv"

func main() {
var price = "€48.95"

}

for index, char := range []byte(price) {
fmt.Println(index, char)
}
Листинг 7-50 Перечисление байтов в строке в файле main.go в папке collections

Скомпилируйте и выполните этот код с помощью команды,
показанной в листинге 7-50, и вы увидите следующий вывод:
0
1
2
3
4
5
6
7

226
130
172
52
56
46
57
53

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

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

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

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

Ответ

Кто они
такие?

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

Почему они
полезны?

Функции позволяют определить свойства один раз и использовать их
многократно.

Как они
Функции вызываются по имени и могут быть снабжены значениями данных,
используются? с которыми можно работать, используя параметры. Результат выполнения
операторов в функции может быть получен как результат функции.
Есть ли
подводные
камни или
ограничения?

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

Есть ли
Нет, функции — это основная особенность языка Go.
альтернативы?

Таблица 8-2 суммирует главу.
Таблица 8-2 Краткое содержание главы
Проблема

Решение

Листинг

Групповые операторы, чтобы их можно было
выполнять по мере необходимости

Определите функцию

4

Проблема

Решение

Листинг

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

Определить параметры
функции

5–8

Разрешить функции принимать переменное количество Определить переменный
аргументов
параметр

9–13

Использовать ссылки на значения, определенные вне
функции

Определите параметры,
которые принимают
указатели

14, 15

Производить вывод из операторов, определенных в
функции

Определите один или
несколько результатов

16–22

Игнорировать результат, полученный функцией

Используйте пустой
идентификатор

23

Запланировать вызов функции, когда текущая
выполняемая функция будет завершена

Используйте ключевое
слово defer

24

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем functions.
Перейдите в папку functions и выполните команду, показанную в
листинге 8-1, чтобы инициализировать проект.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init functions
Листинг 8-1 Инициализация проекта

Добавьте файл с именем main.go в папку functions с содержимым,
показанным в листинге 8-2.
package main
import "fmt"

func main() {
}

fmt.Println("Hello, Functions")
Листинг 8-2 Содержимое файла main.go в папке functions.

Используйте командную строку для запуска команды, показанной в
листинге 8-3, в папке functions.
go run .
Листинг 8-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Hello, Functions

Определение простой функции
Функции — это группы операторов, которые можно использовать и
повторно использовать как одно действие. Для начала в листинге 8-4
определяется простая функция.
package main
import "fmt"
func printPrice() {
kayakPrice := 275.00
kayakTax := kayakPrice * 0.2
fmt.Println("Price:", kayakPrice, "Tax:", kayakTax)
}
func main() {
fmt.Println("About to call function")
printPrice()
fmt.Println("Function complete")
}
Листинг 8-4 Определение функции в файле main.go в папке functions

Функции определяются ключевым словом func, за которым следует
имя функции, круглые скобки и блок кода, заключенный в фигурные
скобки, как показано на рисунке 8-1.

Рисунок 8-1 Анатомия функции

Теперь в файле кода main.go есть две функции. Новая функция
называется printPrice и содержит операторы, определяющие две
переменные и вызывающие функцию Println из пакета fmt. Основная
функция — это точка входа в приложение, где начинается и
заканчивается выполнение. Функции Go должны быть определены с
помощью фигурных скобок, а открывающая фигурная скобка должна
быть определена в той же строке, что и ключевое слово func и имя
функции. Условные обозначения, принятые в других языках, такие как
опускание фигурных скобок или размещение фигурной скобки на
следующей строке, не допускаются.
Примечание
Обратите внимание, что функция printPrice определена вместе с
существующей основной функцией в файле main.go. Go
поддерживает определение функций внутри других функций, но
требуется другой синтаксис, как описано в главе 9.
Основная функция вызывает функцию printPrice, что делается с
помощью оператора, указывающего имя функции, за которым следуют
круглые скобки, как показано на рисунке 8-2.

Рисунок 8-2 Вызов функции

При вызове функции выполняются операторы, содержащиеся в
блоке кода функции. Когда все операторы были вызваны, выполнение
продолжается с оператора, следующего за оператором, вызвавшим
функцию. Это можно увидеть в выводе кода в листинге 8-4, когда он
скомпилирован и выполнен:
About to call function
Price: 275 Tax: 55
Function complete

Определение и использование параметров
функции
Параметры позволяют функции получать значения данных при ее
вызове, что позволяет изменить ее поведение. Листинг 8-5 изменяет
функцию printPrice, определенную в предыдущем разделе, так что
она определяет параметры.
package main
import "fmt"
func printPrice(product string, price float64, taxRate
float64) {
taxAmount := price * taxRate
fmt.Println(product, "price:", price, "Tax:", taxAmount)
}
func main() {
printPrice("Kayak", 275, 0.2)
printPrice("Lifejacket", 48.95, 0.2)
printPrice("Soccer Ball", 19.50, 0.15)
}

Листинг 8-5 Определение параметров функции в файле main.go в папке functions.

Параметры определяются именем, за которым следует тип.
Несколько параметров разделяются запятыми, как показано на рисунке
8-3.

Рисунок 8-3 Определение параметров функции

В листинге 8-5 к функции printPrice добавлены три параметра:
строка с именем product, переменная float64 именованная price и
именованная переменная float64 с именем taxRate. В блоке кода
функции доступ к значению, присвоенному параметру, осуществляется
по его имени, как показано на рисунке 8-4.

Рисунок 8-4 Доступ к параметру внутри блока кода

Значения параметров передаются в качестве аргументов при вызове
функции, что означает, что каждый раз при вызове функции могут быть
предоставлены разные значения. Аргументы указываются между
круглыми скобками, которые следуют за именем функции, через
запятую и в том же порядке, в котором были определены параметры,
как показано на рисунке 8-5.

Рисунок 8-5 Вызов функции с аргументами

Значения, используемые в качестве аргументов, должны
соответствовать типам параметров, определенных функцией. Код в
листинге 8-5 выдает следующий результат при компиляции и
выполнении:
Kayak price: 275 Tax: 55
Lifejacket price: 48.95 Tax: 9.790000000000001
Soccer Ball price: 19.5 Tax: 2.925
Значение, отображаемое для продукта Lifejacket, содержит
значение длинной дроби, которое обычно округляется для сумм в
валюте. Я объясню, как форматировать числовые значения в виде
строк, в главе 17.
Примечание
Go не поддерживает необязательные параметры или значения по
умолчанию для параметров.

Пропуск типов параметров
Тип можно не указывать, если смежные параметры имеют одинаковый
тип, как показано в листинге 8-6.
package main
import "fmt"
func printPrice(product string, price, taxRate float64) {
taxAmount := price * taxRate
fmt.Println(product, "price:", price, "Tax:", taxAmount)
}
func main() {
printPrice("Kayak", 275, 0.2)
printPrice("Lifejacket", 48.95, 0.2)
printPrice("Soccer Ball", 19.50, 0.15)
}
Листинг 8-6 Пропуск типа данных параметра в файле main.go в папке functions

Оба параметра price и taxRate имеют тип float64, и, поскольку
они являются смежными, тип данных применяется только к последнему
параметру этого типа. Пропуск типа данных параметра не меняет
параметр или его тип. Код в листинге 8-6 выдает следующий результат:
Kayak price: 275 Tax: 55
Lifejacket price: 48.95 Tax: 9.790000000000001
Soccer Ball price: 19.5 Tax: 2.925

Пропуск имен параметров
Символ подчеркивания (символ _) может использоваться для
параметров, определенных функцией, но не используемых в операторах
кода функции, как показано в листинге 8-7.
package main
import "fmt"
func printPrice(product string, price, _ float64) {
taxAmount := price * 0.25
fmt.Println(product, "price:", price, "Tax:", taxAmount)
}
func main() {
printPrice("Kayak", 275, 0.2)
printPrice("Lifejacket", 48.95, 0.2)
printPrice("Soccer Ball", 19.50, 0.15)
}
Листинг 8-7 Отсутствие имени параметра в файле main.go в папке functions.

Знак подчеркивания известен как пустой идентификатор, а
результат — это параметр, значение которого должно быть
предоставлено при вызове функции, но значение которого недоступно
внутри блока кода функции. Это может показаться странным, но это
может быть полезным способом указать, что параметр не используется
внутри функции, что может возникнуть при реализации методов,
требуемых интерфейсом. Код в листинге 8-7 выдает следующий
результат при компиляции и выполнении:
Kayak price: 275 Tax: 68.75

Lifejacket price: 48.95 Tax: 12.2375
Soccer Ball price: 19.5 Tax: 4.875
Функции также могут опускать имена во всех своих параметрах, как
показано в листинге 8-8.
package main
import "fmt"
func printPrice(string, float64, float64) {
// taxAmount := price * 0.25
fmt.Println("No parameters")
}
func main() {
printPrice("Kayak", 275, 0.2)
printPrice("Lifejacket", 48.95, 0.2)
printPrice("Soccer Ball", 19.50, 0.15)
}
Листинг 8-8 Пропуск всех имен параметров в файле main.go в папке functions

Параметры без имен не могут быть доступны внутри функции, и эта
функция в основном используется в сочетании с интерфейсами,
описанными в главе 11, или при определении типов функций,
описанных в главе 9. Листинг 8-8 дает следующий результат при
компиляции и выполнении:
No parameters
No parameters
No parameters

Определение вариационных параметров
Вариативный параметр принимает переменное количество значений,
что может упростить использование функций. Чтобы понять проблему,
которую решают вариативные параметры, полезно рассмотреть
альтернативу, показанную в листинге 8-9.
package main
import "fmt"

func printSuppliers(product string, suppliers []string ) {
for _, supplier := range suppliers {
fmt.Println("Product:", product, "Supplier:",
supplier)
}
}
func main() {
printSuppliers("Kayak", []string {"Acme Kayaks", "Bob's
Boats", "Crazy Canoes"})
printSuppliers("Lifejacket", []string {"Sail Safe Co"})
}
Листинг 8-9 Определение функции в файле main.go в папке functions

Второй параметр, определенный функцией printSuppliers,
принимает переменное количество поставщиков, используя string
срез. Это работает, но может быть неудобным, поскольку требует
построения срезов, даже если требуется только одна строка, например:
...
printSuppliers("Lifejacket", []string {"Sail Safe Co"})
...
Переменные параметры позволяют функции более элегантно
получать переменное число аргументов, как показано в листинге 8-10.
package main
import "fmt"
func printSuppliers(product string, suppliers ...string ) {
for _, supplier := range suppliers {
fmt.Println("Product:", product, "Supplier:",
supplier)
}
}
func main() {
printSuppliers("Kayak", "Acme Kayaks", "Bob's Boats",
"Crazy Canoes")
printSuppliers("Lifejacket", "Sail Safe Co")
}

Листинг 8-10 Определение вариативного параметра в файле main.go в папке functions

Вариативный параметр определяется многоточием (три точки), за
которым следует тип, как показано на рисунке 8-6.

Рисунок 8-6 Вариативный параметр

Вариативный параметр должен быть последним параметром,
определенным функцией, и может использоваться только один тип,
например строковый тип в этом примере. При вызове функции можно
указать переменное количество строковых аргументов без
необходимости создания среза:
...
printSuppliers("Kayak", "Acme Kayaks", "Bob's Boats", "Crazy
Canoes")
...
Тип вариативного параметра не меняется, а предоставленные
значения по-прежнему содержатся в срезе. Для листинга 8-10 это
означает, что тип параметра suppliers остается []string. Код в
листингах 8-9 и 8-10 выдает следующий результат при компиляции и
выполнении:
Product:
Product:
Product:
Product:

Kayak Supplier: Acme Kayaks
Kayak Supplier: Bob's Boats
Kayak Supplier: Crazy Canoes
Lifejacket Supplier: Sail Safe Co

Работа без аргументов для вариационного параметра
Go позволяет полностью опустить аргументы для переменных
параметров, что может привести к неожиданным результатам, как
показано в листинге 8-11.
package main
import "fmt"

func printSuppliers(product string, suppliers ...string ) {
for _, supplier := range suppliers {
fmt.Println("Product:", product, "Supplier:",
supplier)
}
}
func main() {
printSuppliers("Kayak", "Acme Kayaks", "Bob's Boats",
"Crazy Canoes")
printSuppliers("Lifejacket", "Sail Safe Co")
printSuppliers("Soccer Ball")
}
Листинг 8-11 Пропуск аргументов в файле main.go в папке functions

Новый вызов функции printSuppliers не предоставляет никаких
аргументов для параметра suppliers. Когда это происходит, Go
использует nil в качестве значения параметра, что может вызвать
проблемы с кодом, предполагающим, что в срезе будет хотя бы одно
значение. Скомпилируйте и запустите код из листинга 8-11; вы
получите следующий вывод:
Product:
Product:
Product:
Product:

Kayak Supplier: Acme Kayaks
Kayak Supplier: Bob's Boats
Kayak Supplier: Crazy Canoes
Lifejacket Supplier: Sail Safe Co

Для продукта Soccer Ball нет выходных данных, поскольку срезы
nil имеют нулевую длину, поэтому цикл for никогда не выполняется.
Листинг 8-12 устраняет эту проблему, проверяя эту проблему.
package main
import "fmt"
func printSuppliers(product string, suppliers ...string ) {
if (len(suppliers) == 0) {
fmt.Println("Product:", product, "Supplier: (none)")
} else {
for _, supplier := range suppliers {

fmt.Println("Product:", product, "Supplier:",
supplier)
}
}
}
func main() {
printSuppliers("Kayak", "Acme Kayaks", "Bob's Boats",
"Crazy Canoes")
printSuppliers("Lifejacket", "Sail Safe Co")
printSuppliers("Soccer Ball")
}
Листинг 8-12 Проверка наличия пустых срезов в файле main.go в папке functions

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

Kayak Supplier: Acme Kayaks
Kayak Supplier: Bob's Boats
Kayak Supplier: Crazy Canoes
Lifejacket Supplier: Sail Safe Co
Soccer Ball Supplier: (none)

Использование срезов в качестве значений переменных
параметров
Вариативные параметры позволяют вызывать функцию без создания
срезов, но это бесполезно, если у вас уже есть срез, который вы хотите
использовать. В этих ситуациях после последнего аргумента,
переданного в функцию с многоточием, можно будет использовать срез,
как показано в листинге 8-13.
package main
import "fmt"
func printSuppliers(product string, suppliers ...string ) {
if (len(suppliers) == 0) {

fmt.Println("Product:", product, "Supplier: (none)")
} else {
for _, supplier := range suppliers {
fmt.Println("Product:", product, "Supplier:",
supplier)
}
}
}
func main() {
names := []string {"Acme Kayaks", "Bob's Boats", "Crazy
Canoes"}

}

printSuppliers("Kayak", names...)
printSuppliers("Lifejacket", "Sail Safe Co")
printSuppliers("Soccer Ball")

Листинг 8-13 Использование среза в качестве аргумента в файле main.go в папке functions

Этот метод позволяет избежать необходимости распаковывать срез
на отдельные значения, чтобы их можно было снова объединить в срез
для вариативного параметра. Скомпилируйте и выполните код из
листинга 8-13, и вы получите следующий вывод:
Product:
Product:
Product:
Product:
Product:

Kayak Supplier: Acme Kayaks
Kayak Supplier: Bob's Boats
Kayak Supplier: Crazy Canoes
Lifejacket Supplier: Sail Safe Co
Soccer Ball Supplier: (none)

Использование указателей в качестве параметров функций
По умолчанию Go копирует значения, используемые в качестве
аргументов, поэтому изменения ограничиваются функцией, как
показано в листинге 8-14.
package main
import "fmt"
func swapValues(first, second int) {

}

fmt.Println("Before swap:", first, second)
temp := first
first = second
second = temp
fmt.Println("After swap:", first, second)

func main() {

}

val1, val2 := 10, 20
fmt.Println("Before calling function", val1, val2)
swapValues(val1, val2)
fmt.Println("After calling function", val1, val2)

Листинг 8-14 Изменение значения параметра в файле main.go в папке functions

Функция swapValues получает два значения int, записывает их,
меняет местами и снова записывает. Значения, переданные функции,
записываются до и после вызова функции. Вывод из листинга 8-14
показывает, что изменения, внесенные в значения в функции
swpValues, не влияют на переменные, определенные в функции main:
Before calling function 10 20
Before swap: 10 20
After swap: 20 10
After calling function 10 20
Go позволяет функциям получать указатели, что меняет это
поведение, как показано в листинге 8-15.
package main
import "fmt"
func swapValues(first, second *int) {
fmt.Println("Before swap:", *first, *second)
temp := *first
*first = *second
*second = temp
fmt.Println("After swap:", *first, *second)
}

func main() {

}

val1, val2 := 10, 20
fmt.Println("Before calling function", val1, val2)
swapValues(&val1, &val2)
fmt.Println("After calling function", val1, val2)
Листинг 8-15 Определение функции с указателями в файле main.go в папке functions

Функция swapValues по-прежнему меняет местами два значения, но
делает это с помощью указателя, что означает, что изменения вносятся
в области памяти, которые также используются функцией main, что
можно увидеть в выводе кода:
Before calling function 10 20
Before swap: 10 20
After swap: 20 10
After calling function 20 10
Существуют лучшие способы выполнения таких задач, как замена
значений, включая использование нескольких результатов функций, как
описано в следующем разделе, но этот пример демонстрирует, что
функции могут работать со значениями напрямую или косвенно через
указатели.

Определение и использование результатов
функции
Функции определяют результаты, которые позволяют функциям
предоставлять своим вызывающим объектам выходные данные
операций, как показано в листинге 8-16.
package main
import "fmt"
func calcTax(price float64) float64 {
return price + (price * 0.2)
}

func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
priceWithTax := calcTax(price)
fmt.Println("Product: ", product,
priceWithTax)
}
}

"Price:",

Листинг 8-16 Создание результата функции в файле main.go в папке functions

Функция объявляет свой результат, используя тип данных,
следующий за параметром, как показано на рисунке 8-7.

Рисунок 8-7 Определение результата функции

Функция calcTax выдает результат float64, который создается
оператором return, как показано на рисунке 8-8.

Рисунок 8-8 Возврат результата функцией

При вызове функции результат может быть присвоен переменной,
как показано на рисинке 8-9.

Рисунок 8-9 Использование результата функции

Результаты функции можно использовать непосредственно в
выражениях. В листинге 8-17 переменная опущена, а функция calcTax
вызывается напрямую для получения аргумента для функции
fmt.PrintLn.
package main
import "fmt"
func calcTax(price float64) float64 {
return price + (price * 0.2)
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
fmt.Println("Product: ", product,
calcTax(price))
}
}

"Price:",

Листинг 8-17 Использование результата функции непосредственно в файле main.go в
папке functions

Go использует результат, полученный функцией calcTax, без
необходимости определять промежуточную переменную. Код в
листингах 8-16 и 8-17 выдает следующий результат:

Product: Kayak Price: 330
Product: Lifejacket Price: 58.74

Возврат функцией нескольких результатов
Необычной особенностью функций Go является возможность
получения более одного результата, как показано в листинге8-18.
package main
import "fmt"
func swapValues(first, second int) (int, int) {
return second, first
}
func main() {
val1, val2 := 10, 20
fmt.Println("Before calling function", val1, val2)
val1, val2 = swapValues(val1, val2)
fmt.Println("After calling function", val1, val2)
}
Листинг 8-18 Создание нескольких результатов в файле main.go в папке functions

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

с

Рисунок 8-10 Определение нескольких результатов

Когда функция определяет несколько результатов, значения для
каждого результата предоставляются с ключевым словом return,
разделенным запятыми, как показано на рисунке 8-11.

Рисунок 8-11 Возврат нескольких результатов

Функция swapValues использует ключевое слово return для
получения двух результатов типа int, которые она получает через свои
параметры. Эти результаты могут быть присвоены переменным в
операторе, вызывающем функцию, также через запятую, как показано
на рисунке 8-12.

Рисунок 8-12 Получение нескольких результатов

Код в листинге 8-18 выдает следующий результат при компиляции и
выполнении:
Before calling function 10 20
After calling function 20 10

Использование нескольких результатов вместо нескольких
значений
Поначалу результаты нескольких функций могут показаться
странными, но их можно использовать, чтобы избежать источника
ошибок, характерного для других языков, который заключается в
придании разных значений одному результату на основе возвращаемого
значения. В листинге 8-19 показана проблема, вызванная приданием
дополнительного значения одному результату..
package main
import "fmt"

func calcTax(price float64) float64 {
if (price > 100) {
return price * 0.2
}
return -1
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
tax := calcTax(price)
if (tax != -1) {
fmt.Println("Product: ", product, "Tax:", tax)
} else {
fmt.Println("Product: ", product, "No tax due")

}

}

}

Листинг 8-19 Использование одного результата в файле main.go в папке functions

Функция calcTax использует результат float64 для передачи двух
результатов. Для значений больше 100 в результате будет указана сумма
налога к уплате. Для значений менее 100 результат будет означать, что
налог не взимается. Компиляция и выполнение кода из листинга 8-19
приводит к следующему результату:
Product: Kayak Tax: 55
Product: Lifejacket No tax due
Придание нескольких значений одному результату может стать
проблемой по мере развития проектов. Налоговый орган может начать
возвращать налог на определенные покупки, что делает значение -1
двусмысленным, поскольку может указывать на то, что налог не
уплачивается или что должен быть выдан возврат в размере 1 доллара.

Есть много способов разрешить этот тип неоднозначности, но
использование результатов нескольких функций является элегантным
решением, хотя и может занять некоторое время, чтобы привыкнуть к
нему. В листинге 8-20 я изменил функцию calcTax, чтобы она выдавала
несколько результатов.
package main
import "fmt"
func calcTax(price float64) (float64, bool) {
if (price > 100) {
return price * 0.2, true
}
return 0, false
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
taxAmount, taxDue := calcTax(price)
if (taxDue) {
fmt.Println("Product: ", product, "Tax:",
taxAmount)
} else {
fmt.Println("Product: ", product, "No tax due")
}

}

}
Листинг 8-20 Использование нескольких результатов в файле main.go в папке functions.

Дополнительный результат, возвращаемый методом calcTax,
представляет собой логическое значение, указывающее, подлежит ли
уплате налог, отделяя эту информацию от другого результата. В
листинге 8-20 два результата получены в отдельном операторе, но

множественные результаты хорошо подходят для поддержки оператора
if оператора инициализации, как показано в листинге 8-21. (Подробнее
об этой функции см. в главе 12.)
package main
import "fmt"
func calcTax(price float64) (float64, bool) {
if (price > 100) {
return price * 0.2, true
}
return 0, false
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
if taxAmount, taxDue := calcTax(price); taxDue {
fmt.Println("Product: ", product, "Tax:",
taxAmount)
} else {
fmt.Println("Product: ", product, "No tax due")

}

}

}

Листинг 8-21 Использование оператора инициализации в файле main.go в папке functions

Два результата получаются путем вызова функции calcTax в
операторе инициализации, а результат bool затем используется в
качестве выражения оператора. Код в листингах 8-20 и 8-21 выдает
следующий результат:
Product: Kayak Tax: 55
Product: Lifejacket No tax due

Использование именованных результатов
Результатам функции можно давать имена, которым можно присваивать
значения во время выполнения функции. Когда выполнение достигает
ключевого слова return, возвращаются текущие значения,
присвоенные результатам, как показано в листинге 8-22.
package main
import "fmt"
func calcTax(price float64) (float64, bool) {
if (price > 100) {
return price * 0.2, true
}
return 0, false
}
func calcTotalPrice(products map[string]float64,
minSpend float64) (total, tax float64) {
total = minSpend
for _, price := range products {
if taxAmount, due := calcTax(price); due {
total += taxAmount;
tax += taxAmount
} else {
total += price
}
}
return
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
total1, tax1 := calcTotalPrice(products, 10)
fmt.Println("Total 1:", total1, "Tax 1:", tax1)
total2, tax2 := calcTotalPrice(nil, 10)
fmt.Println("Total 2:", total2, "Tax 2:", tax2)

}
Листинг 8-22 Использование именованных результатов в файле main.go в папке functions

Именованные результаты определяются как комбинация имени и
типа результата, как показано на рисунке 8-13.

Рисунок 8-13 Именованные результаты

Функция calcTotalPrice определяет результаты с именами total и
tax. Оба являются значениями float64, что означает, что я могу
опустить тип данных в первом имени. Внутри функции результаты
можно использовать как обычные переменные:
...
total = minSpend
for _, price := range products {
if taxAmount, due := calcTax(price); due {
total += taxAmount;
tax += taxAmount
} else {
total += price
}
}
...
Ключевое слово return используется само по себе, позволяя
возвращать текущие значения, присвоенные именованным результатам.
Код в листинге 8-22 выдает следующий результат:
Total 1: 113.95 Tax 1: 55
Total 2: 10 Tax 2: 0

Использование пустого идентификатора для сброса
результатов

Go требует использования всех объявленных переменных, что может
быть неудобно, когда функция возвращает значения, которые вам не
нужны. Чтобы избежать ошибок компилятора, можно использовать
пустой идентификатор (символ _) для обозначения результатов, которые
не будут использоваться, как показано в листинге 8-23.
package main
import "fmt"
func calcTotalPrice(products map[string]float64) (count int,
total float64) {
count = len(products)
for _, price := range products {
total += price
}
return
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

_, total := calcTotalPrice(products)
fmt.Println("Total:", total)

Листинг 8-23 Сброс результатов функции в файле main.go в папке functions

Функция calcTotalPrice возвращает два результата, из которых
используется только один. Пустой идентификатор используется для
нежелательного значения, что позволяет избежать ошибки
компилятора. Код в листинге 8-23 выдает следующий результат:
Total: 323.95

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

Ключевое слово defer используется для планирования вызовафункции,
который будет выполнен непосредственно перед возвратом из текущей
функции, как показано в листинге 8-24.
package main
import "fmt"
func calcTotalPrice(products map[string]float64) (count int,
total float64) {
fmt.Println("Function started")
defer fmt.Println("First defer call")
count = len(products)
for _, price := range products {
total += price
}
defer fmt.Println("Second defer call")
fmt.Println("Function about to return")
return
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

_, total := calcTotalPrice(products)
fmt.Println("Total:", total)

Листинг 8-24 Использование ключевого слова defer в файле main.go в папке functions

Ключевое слово defer используется перед вызовом функции, как
показано на рисунке 8-14.

Рисунок 8-14 Ключевое слово defer

Ключевое слово defer в основном используется для вызова
функций, освобождающих ресурсы, таких как закрытие открытых
файлов (описано в главе 22) или соединений HTTP (главы 24 и 25). Без
ключевого слова defer оператор, освобождающий ресурс, должен
появиться в конце функции, а это может быть много операторов после
создания и использования ресурса. Ключевое слово defer позволяет
сгруппировать операторы, которые создают, используют и освобождают
ресурс вместе.
Ключевое слово defer можно использовать с любым вызовом
функции, как показано в листинге 8-24, и одна функция может
использовать ключевое слово defer несколько раз. Непосредственно
перед возвратом функции Go выполнит вызовы, запланированные с
помощью ключевого слова defer, в том порядке, в котором они были
определены. Код в листинге 8-24 планирует вызовы функции
fmt.Println и при компиляции и выполнении выдает следующий
вывод:
Function started
Function about to return
Second defer call
First defer call
Total: 323.95

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

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

Ответ

Кто они
такие?

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

Почему они
полезны?

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

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

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

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

Таблица 9-2 суммирует главу.
Таблица 9-2 Краткое содержание главы
Проблема

Решение

Листинг

Описать функции с определенной комбинацией
параметров и результатов

Используйте функциональный
тип

4–7

Упростить повторяющееся выражение
функционального типа

Использовать псевдоним
функционального типа

8

Проблема

Решение

Листинг

Определить функцию, относящуюся к области
кода

Используйте литеральный
синтаксис функции

9–12

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

13–18

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем functionTypes.
Перейдите в папку functionTypes и выполните команду, показанную в
листинге 9-1, чтобы инициализировать проект.
go mod init functionTypes
Листинг 9-1 Инициализация проекта

Добавьте файл с именем main.go в папку functionTypes с
содержимым, показанным в листинге 9-2.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
package main
import "fmt"
func main() {
}

fmt.Println("Hello, Function Types")
Листинг 9-2 Содержимое файла main.go в папке functionTypes

Используйте командную строку для запуска команды, показанной в
листинге 9-3, в папке functionTypes.

go run .
Листинг 9-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Hello, Function Types

Понимание типов функций
Функции в Go имеют тип данных, что означает, что они могут быть
назначены переменным и использоваться в качестве параметров
функции, аргументов и результатов. В листинге 9-4 показано простое
использование типа данных функции.
package main
import "fmt"
func calcWithTax(price float64) float64 {
return price + (price * 0.2)
}
func calcWithoutTax(price float64) float64 {
return price
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
var calcFunc func(float64) float64
if (price > 100) {
calcFunc = calcWithTax
} else {
calcFunc = calcWithoutTax
}

totalPrice := calcFunc(price)
fmt.Println("Product:",
totalPrice)
}
}

product,

"Price:",

Листинг 9-4 Использование типа данных функции в файле main.go в папке functionTypes

Этот пример содержит две функции, каждая из которых определяет
параметр float64 и возвращает результат float64. Цикл for в
основной функции выбирает одну из этих функций и использует ее для
расчета общей цены продукта. Первый оператор в цикле определяет
переменную, как показано на рисунок 9-1.

Рисунок 9-1 Определение переменной функционального типа

Типы функций указываются с помощью ключевого слова func, за
которым в скобках следуют типы параметров, а затем типы результатов.
Это известно как сигнатура функции. Если результатов несколько, то
типы результатов также заключаются в круглые скобки. Тип функции в
листинге 9-4 описывает функцию, которая принимает аргумент float64
и возвращает результат float64.
Переменной calcFunc, определенной в листинге 9-4, может быть
присвоено любое значение, соответствующее ее типу, что означает
любую функцию с правильным количеством и типом аргументов и
результатов. Чтобы назначить определенную функцию переменной,
используется имя функции, как показано на рисунке 9-2.

Рисунок 9-2 Назначение функции переменной

Как только функция была назначена переменной, ее можно вызвать,
как если бы имя переменной было именем функции. В примере это
означает, что функция, назначенная переменной calcFunc, может быть
вызвана, как показано на рисунке 9-3.

Рисунок 9-3 Вызов функции через переменную

В результате будет вызвана любая функция, назначенная функции
totalPrice. Если значение price больше 100, то переменной
totalPrice присваивается функция calcWithTax, и именно эта
функция будет выполняться. Если price меньше или равна 100, то
переменной totalPrice присваивается функция calcWithoutTax, и
вместо нее будет выполняться эта функция. Код в листинге 9-4 выдает
следующий результат при компиляции и выполнении (хотя вы можете
увидеть результаты в другом порядке, как описано в главе 7):
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

Понимание сравнения функций и нулевого типа
Операторы сравнения Go нельзя использовать для сравнения функций,
но их можно использовать для определения того, была ли функция
присвоена переменной, как показано в листинге 9-5.
package main
import "fmt"
func calcWithTax(price float64) float64 {
return price + (price * 0.2)
}
func calcWithoutTax(price float64) float64 {
return price
}

func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
var calcFunc func(float64) float64
fmt.Println("Function assigned:", calcFunc == nil)
if (price > 100) {
calcFunc = calcWithTax
} else {
calcFunc = calcWithoutTax
}
fmt.Println("Function assigned:", calcFunc == nil)
totalPrice := calcFunc(price)
fmt.Println("Product:", product, "Price:",
totalPrice)
}
}
Листинг 9-5 Проверка назначения в файле main.go в папке functionTypes

Нулевое значение для типов функций равно nil, а новые операторы
в листинге 9-5 используют оператор равенства, чтобы определить,
присвоена ли функция переменной calcFunc. Код в листинге 9-5
выдает следующий результат:
Function
Function
Product:
Function
Function
Product:

assigned: true
assigned: false
Kayak Price: 330
assigned: true
assigned: false
Lifejacket Price: 48.95

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

Некоторым описаниям в следующих разделах может быть трудно
следовать, потому что слово функция требуется очень часто. Я
предлагаю обратить пристальное внимание на примеры кода,
которые помогут разобраться в тексте.
package main
import "fmt"
func calcWithTax(price float64) float64 {
return price + (price * 0.2)
}
func calcWithoutTax(price float64) float64 {
return price
}
func printPrice(product string, price float64, calculator
func(float64) float64 ) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

for product, price := range products {
if (price > 100) {
printPrice(product, price, calcWithTax)
} else {
printPrice(product, price, calcWithoutTax)
}
}

Листинг 9-6 Использование функций в качестве аргументов в файле main.go в папке
functionTypes

Функция printPrice определяет три параметра, первые два из
которых получают значения типа string и float64. Третий параметр,
названный calculator, получает функцию, которая получает значение
float64 и выдает результат float64, как показано на рисунке 9-4.

Рисунок 9-4 Параметр функции

В функции printPrice параметр calculator используется так же,
как и любая другая функция:
...
fmt.Println("Product:", product, "Price:", calculator(price))
...
Важно то, что функция printPrice не знает — и не заботится о том,
получает ли она функцию calcWithTax или calcWithoutTax через
параметр calculator. Все, что знает функция printPrice, это то, что
она сможет вызвать функцию calculator с аргументом float64 и
получить результат float64, потому что это функциональный тип
параметра.
Выбор используемой функции осуществляется оператором if в
main функции, а имя используется для передачи одной функции в
качестве аргумента другой функции, например:
...
printPrice(product, price, calcWithTax)
...
Код в листинге 9-6 выдает следующий результат при компиляции и
выполнении:
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

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

листинге 9-7.
package main
import "fmt"
func calcWithTax(price float64) float64 {
return price + (price * 0.2)
}
func calcWithoutTax(price float64) float64 {
return price
}
func printPrice(product string, price float64, calculator
func(float64) float64 ) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func selectCalculator(price float64) func(float64) float64 {
if (price > 100) {
return calcWithTax
}
return calcWithoutTax
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

for product, price := range products {
printPrice(product, price, selectCalculator(price))
}

Листинг 9-7 Создание результата функции в файле main.go в папке functionTypes

Функция selectCalculator получает значение
возвращает функцию, как показано на рисунке 9-5.

float64

и

Рисунок 9-5 Результат типа функции

Результатом selectCalculator является функция, которая
принимает значение float64 и выдает результат float64. Вызывающие
selectCalculator не знают, получат ли они функцию calcWithTax или
calcWithoutTax, только то, что они получат функцию с указанной
сигнатурой. Код в листинге 9-7 выдает следующий результат при
компиляции и выполнении:
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

Создание псевдонимов функциональных типов
Как показали предыдущие примеры, использование типов функций
может быть многословным и повторяющимся, что приводит к созданию
кода, который трудно читать и поддерживать. Go поддерживает
псевдонимы типов, которые можно использовать для присвоения имени
сигнатуре функции, чтобы типы параметров и результатов не
определялись каждый раз при использовании типа функции, как
показано в листинге 9-8.
package main
import "fmt"
type calcFunc func(float64) float64
func calcWithTax(price float64) float64 {
return price + (price * 0.2)
}
func calcWithoutTax(price float64) float64 {
return price
}

func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func selectCalculator(price float64) calcFunc {
if (price > 100) {
return calcWithTax
}
return calcWithoutTax
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

for product, price := range products {
printPrice(product, price, selectCalculator(price))
}

Листинг 9-8 Использование псевдонима типа в файле main.go в папке functionTypes

Псевдоним создается с помощью ключевого слова type, за которым
следует имя псевдонима, а затем тип, как показано на рисунке 9-6.

Рисунок 9-6 Псевдоним типа

Примечание
Ключевое слово type также используется
пользовательских типов, как описано в главе 10.

для

создания

Псевдоним в листинге 9-8 присваивает имя calcFunc типу функции,
которая принимает аргумент float64 и выдает результат float64.
Псевдоним можно использовать вместо типа функции, например:
...
func selectCalculator(price float64) calcFunc {
...
Вам не обязательно использовать псевдонимы для типов функций,
но они могут упростить код и облегчить идентификацию использования
конкретной сигнатуры функции. Код в листинге 9-8 выдает следующий
результат:
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

Использование литерального синтаксиса
функции
Синтаксис литерала функции позволяет определять функции так, чтобы
они были специфичны для области кода, как показано в листинге 9-9.
package main
import "fmt"
type calcFunc func(float64) float64
// func calcWithTax(price float64) float64 {
//
return price + (price * 0.2)
// }
// func calcWithoutTax(price float64) float64 {
//
return price
// }
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}

func selectCalculator(price float64) calcFunc {
if (price > 100) {
var withTax calcFunc = func (price float64) float64 {
return price + (price * 0.2)
}
return withTax
}
withoutTax := func (price float64) float64 {
return price
}
return withoutTax
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}

}

for product, price := range products {
printPrice(product, price, selectCalculator(price))
}

Листинг 9-9 Использование литерального синтаксиса в файле main.go в папке functionTypes

Литеральный синтаксис не включает имя, поэтому за ключевым
словом func следуют параметры, тип результата и блок кода, как
показано на рисунке 9-7. Поскольку имя опущено, функции,
определенные таким образом, называются анонимными функциями.

Рисунок 9-7 Синтаксис литерала функции

Примечание

Go не поддерживает стрелочные функции, где функции более
лаконично выражаются с помощью оператора => без ключевого
слова func и блока кода, заключенного в фигурные скобки. В Go
функции всегда должны определяться ключевым словом и телом.
Литеральный синтаксис создает функцию, которую можно
использовать как любое другое значение, включая назначение функции
переменной, что я и сделал в листинге 9-9. Тип литерала функции
определяется сигнатурой функции, что означает, что количество и типы
параметров функции должны соответствовать типу переменной,
например:
...
var withTax calcFunc = func (price float64) float64 {
return price + (price * 0.2)
}
...
Эта литеральная функция имеет сигнатуру, соответствующую
псевдониму типа calcFunc, с одним параметром float64 и одним
результатом float64. Литеральные функции также можно использовать
с коротким синтаксисом объявления переменных:
...
withoutTax := func (price float64) float64 {
return price
}
...
Компилятор Go определит тип переменной, используя сигнатуру
функции, что означает, что тип переменной WithoutTax является
func(float64) float64. Код в листинге 9-9 выдает следующий
результат при компиляции и выполнении:
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

Понимание области действия функциональной переменной

Функции обрабатываются так же, как и любые другие значения, но
доступ к функции, добавляющей налог, возможен только через
переменную withTax, которая, в свою очередь, доступна только в
кодовом блоке оператора if, как показано в листинге 9-10.
...
func selectCalculator(price float64) calcFunc {
if (price > 100) {
var withTax calcFunc = func (price float64) float64 {
return price + (price * 0.2)
}
return withTax
} else if (price < 10) {
return withTax
}
withoutTax := func (price float64) float64 {
return price
}
return withoutTax
}
...
Листинг 9-10 Использование функции вне ее области действия в файле main.go в папке
functionTypes

Оператор в предложении else/if пытается получить доступ к
функции, назначенной переменной withTax. Доступ к переменной
недоступен, поскольку она находится в другом блоке кода, поэтому
компилятор выдает следующую ошибку:
# command-line-arguments
.\main.go:18:16: undefined: withTax

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

import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func selectCalculator(price float64) calcFunc {
if (price > 100) {
return func (price float64) float64 {
return price + (price * 0.2)
}
}
return func (price float64) float64 {
return price
}
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
printPrice(product, price, selectCalculator(price))
}
}
Листинг 9-11 Использование функций непосредственно в файле main.go в папке
functionTypes

Ключевое слово return применяется непосредственно к функции,
не присваивая функцию переменной. Код в листинге 9-11 выдает
следующий результат:
Product: Kayak Price: 330
Product: Lifejacket Price: 48.95

Литеральные функции также можно использовать в качестве
аргументов для других функций, как показано в листинге 9-12.
package main
import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func main() {
products := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
for product, price := range products {
printPrice(product, price, func (price float64)
float64 {
return price + (price * 0.2)
})
}
}
Листинг 9-12 Использование литерального аргумента функции в файле main.go в папке
functionTypes

Последний аргумент функции printPrice выражается с
использованием литерального синтаксиса и без присвоения функции
переменной. Код в листинге 9-12 выдает следующий результат:
Product: Kayak Price: 330
Product: Lifejacket Price: 58.74

Понимание замыкания функции

Функции, определенные с использованием литерального синтаксиса,
могут ссылаться на переменные из окружающего кода — функция,
известная как замыкание. Эту функцию может быть трудно понять,
поэтому я начну с примера, который не зависит от замыкания,
показанного в листинге 9-13, а затем объясню, как его можно улучшить.
package main
import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func main() {
watersportsProducts := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
soccerProducts := map[string] float64 {
"Soccer Ball": 19.50,
"Stadium": 79500,
}
calc := func(price float64) float64 {
if (price > 100) {
return price + (price * 0.2)
}
return price;
}
for product, price := range watersportsProducts {
printPrice(product, price, calc)
}
calc = func(price float64) float64 {
if (price > 50) {

return price + (price * 0.1)
}
return price

}
for product, price := range soccerProducts {
printPrice(product, price, calc)
}
}
Листинг 9-13 Использование нескольких функций в файле main.go в папке functionTypes

Две карты содержат названия и цены товаров в категориях водного
спорта и футбола. Карты перечисляются циклами for, которые
вызывают функцию printPrice для каждого элемента карты. Одним из
аргументов, требуемых функцией printPrice, является функция
calcFunc, которая вычисляет общую цену продукта, включая налоги.
Для каждой категории продуктов требуется свой порог необлагаемого
налогом дохода и налоговая ставка, как описано в Таблице 9-3.
Таблица 9-3 Пороги категорий продуктов и налоговые ставки
Категория

Порог Ставка

Водный спорт 100

20%

Футбол

10%

50

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

кода с изменениями для каждой категории. Это легко сделать с
помощью функции замыкания, как показано в листинге 9-14.
package main
import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
func priceCalcFactory(threshold, rate float64) calcFunc {
return func(price float64) float64 {
if (price > threshold) {
return price + (price * rate)
}
return price
}
}
func main() {
watersportsProducts := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
soccerProducts := map[string] float64 {
"Soccer Ball": 19.50,
"Stadium": 79500,
}
waterCalc := priceCalcFactory(100, 0.2);
soccerCalc := priceCalcFactory(50, 0.1)
for product, price := range watersportsProducts {
printPrice(product, price, waterCalc)
}

for product, price := range soccerProducts {
printPrice(product, price, soccerCalc)
}
}
Листинг 9-14 Использование замыкания функции в файле main.go в папке functionTypes

Ключевым дополнением является функция priceCalcFactory,
которую я буду называть в этом разделе фабричной функцией, чтобы
отличать ее от других частей кода. Работа фабричной функции
заключается в создании функций калькулятора для определенной
комбинации порога и налоговой ставки. Эта задача описывается
сигнатурой функции, как показано на рисунке 9-8.

Рисунок 9-8 Сигнатура заводской функции

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

Рисунок 9-9 Общий код

Функция замыкания является связующим звеном между фабричной
функцией и функцией калькулятора. Функция калькулятора использует
две переменные для получения результата, например:
...
return func(price float64) float64 {
if (price > threshold) {
return price + (price * rate)
}
return price
}
...
Значения threshold и rate берутся из заводских параметров
функции, например:
...
func priceCalcFactory(threshold, rate float64) calcFunc {
...
Функция замыкания позволяет функции получать доступ к
переменным и параметрам в окружающем коде. В этом случае функция
калькулятора опирается на параметры заводской функции. Когда
вызывается функция калькулятора, значения параметров используются
для получения результата, как показано на рисунке 9-10.

Рисунок 9-10 Замыкание функции

Говорят, что функция замыкается на источниках требуемых
значений, так что функция калькулятора закрывается на параметрах
threshold и rate фабричной функции.
Результатом является фабричная функция, которая создает функции
калькулятора, настроенные для налогового порога и ставки категории
продукта. Код, необходимый для расчета цен, был объединен, поэтому
изменения будут применяться ко всем категориям. Листинг 9-13 и
Листинг 9-14 выдают следующий результат:
Product:
Product:
Product:
Product:

Kayak Price: 330
Lifejacket Price: 48.95
Soccer Ball Price: 19.5
Stadium Price: 87450

Понимание оценки замыкания
Переменные, по которым замыкается функция, оцениваются каждый
раз, когда функция вызывается, а это означает, что изменения,
сделанные вне функции, могут повлиять на результаты, которые она
производит, как показано в листинге 9-15.
package main
import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {

fmt.Println("Product:",
calculator(price))
}

product,

"Price:",

var prizeGiveaway = false
func priceCalcFactory(threshold, rate float64) calcFunc {
return func(price float64) float64 {
if (prizeGiveaway) {
return 0
} else if (price > threshold) {
return price + (price * rate)
}
return price
}
}
func main() {
watersportsProducts := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
soccerProducts := map[string] float64 {
"Soccer Ball": 19.50,
"Stadium": 79500,
}
prizeGiveaway = false
waterCalc := priceCalcFactory(100, 0.2);
prizeGiveaway = true
soccerCalc := priceCalcFactory(50, 0.1)
for product, price := range watersportsProducts {
printPrice(product, price, waterCalc)
}

}

for product, price := range soccerProducts {
printPrice(product, price, soccerCalc)
}

Листинг 9-15 Изменение замкнутого значения в файле main.go в папке functionTypes

Функция калькулятора замыкается на переменной PrizeGiveaway, в
результате чего цены падают до нуля. Перед созданием функции для
категории водных видов спорта переменной PrizeGiveaway
присваивается значение false, а перед созданием функции для
категории футбола — значение true.
Но, поскольку замыкания оцениваются при вызове функции,
используется текущее значение переменной PrizeGiveaway, а не
значение на момент создания функции. Как следствие, цены для обеих
категорий сбрасываются до нуля, и код выдает следующий результат:
Product:
Product:
Product:
Product:

Lifejacket Price: 0
Kayak Price: 0
Soccer Ball Price: 0
Stadium Price: 0

Принудительная ранняя оценка
Оценка замыканий при вызове функции может быть полезна, но если
вы хотите использовать значение, которое было текущим на момент
создания функции, скопируйте это значение, как показано в листинге 916.
...
func priceCalcFactory(threshold, rate float64) calcFunc {
fixedPrizeGiveway := prizeGiveaway
return func(price float64) float64 {
if (fixedPrizeGiveway) {
return 0
} else if (price > threshold) {
return price + (price * rate)
}
return price
}
}
...
Листинг 9-16 Принудительная оценка в файле main.go в папке functionTypes

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

можно добиться, добавив параметр в фабричную функцию, поскольку
по умолчанию параметры функции передаются по значению. Листинг
9-17 добавляет параметр к фабричной функции.
package main
import "fmt"
type calcFunc func(float64) float64
func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
var prizeGiveaway = false
func priceCalcFactory(threshold, rate float64,
bool) calcFunc {
return func(price float64) float64 {
if (zeroPrices) {
return 0
} else if (price > threshold) {
return price + (price * rate)
}
return price
}
}
func main() {
watersportsProducts := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
soccerProducts := map[string] float64 {
"Soccer Ball": 19.50,
"Stadium": 79500,
}
prizeGiveaway = false

zeroPrices

waterCalc := priceCalcFactory(100, 0.2, prizeGiveaway);
prizeGiveaway = true
soccerCalc := priceCalcFactory(50, 0.1, prizeGiveaway)
for product, price := range watersportsProducts {
printPrice(product, price, waterCalc)
}

}

for product, price := range soccerProducts {
printPrice(product, price, soccerCalc)
}
Листинг 9-17 Добавление параметра в файл main.go в папку functionTypes

В листинге 9-16 и листинге 9-17 функции калькулятора не
затрагиваются при изменении переменной PrizeGiveaway и получении
следующего вывода:
Product:
Product:
Product:
Product:

Kayak Price: 330
Lifejacket Price: 48.95
Stadium Price: 0
Soccer Ball Price: 0

Замыкание по указателю для предотвращения ранней
оценки
Большинство проблем с замыканием вызвано изменениями,
внесенными в переменные после создания функции, которые можно
решить с помощью методов, описанных в предыдущем разделе. Иногда
вы можете столкнуться с противоположной проблемой, которая
заключается в необходимости избегать ранней оценки, чтобы
гарантировать, что текущее значение используется функцией. В этих
ситуациях использование указателя предотвратит копирование
значений, как показано в листинге 9-18.
package main
import "fmt"
type calcFunc func(float64) float64

func printPrice(product string, price float64, calculator
calcFunc) {
fmt.Println("Product:",
product,
"Price:",
calculator(price))
}
var prizeGiveaway = false
func priceCalcFactory(threshold, rate float64,
*bool) calcFunc {
return func(price float64) float64 {
if (*zeroPrices) {
return 0
} else if (price > threshold) {
return price + (price * rate)
}
return price
}
}

zeroPrices

func main() {
watersportsProducts := map[string]float64 {
"Kayak" : 275,
"Lifejacket": 48.95,
}
soccerProducts := map[string] float64 {
"Soccer Ball": 19.50,
"Stadium": 79500,
}
prizeGiveaway = false
waterCalc := priceCalcFactory(100, 0.2, &prizeGiveaway);
prizeGiveaway = true
soccerCalc := priceCalcFactory(50, 0.1, &prizeGiveaway)
for product, price := range watersportsProducts {
printPrice(product, price, waterCalc)
}
for product, price := range soccerProducts {
printPrice(product, price, soccerCalc)

}

}

Листинг 9-18 Замыкание указателя в файле main.go в папке functionTypes

В этом примере фабричная функция определяет параметр, который
получает указатель на bool значение, на котором функция калькулятора
закрывается. Указатель следует, когда вызывается функция
калькулятора, что гарантирует использование текущего значения. Код в
листинге 9-18 выводит следующий результат:
Product:
Product:
Product:
Product:

Kayak Price: 0
Lifejacket Price: 0
Soccer Ball Price: 0
Stadium Price: 0

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

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

Ответ

Кто они такие? Структуры — это типы данных, состоящие из полей.
Почему они
полезны?

Структуры позволяют определять пользовательские типы данных.

Как они
Ключевые слова type и struct используются для определения типа, позволяя
используются? указывать имена полей и типы.
Есть ли
подводные
камни или
ограничения?

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

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

Таблица 10-2 суммирует главу.
Таблица 10-2 Краткое содержание главы
Проблема

Решение

Листинг

Определить пользовательский тип Определите тип структуры
данных

4, 24

Создать структурное значение

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

5–7, 15

Определить поле структуры, тип
которого является другой
структурой

Определите встроенное поле

8, 9

Проблема

Решение

Листинг

Сравнить значения структуры

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

10, 11

Преобразовать типы структур

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

Определить структуру без
присвоения имени

Определите анонимную структуру

13–14

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

Используйте указатель

16–21,
25–29

Согласованное создание
структурных значений

Определить функцию-конструктор

22, 23

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем structs.
Перейдите в папку structs и выполните команду, показанную в
листинге 10-1, чтобы инициализировать проект.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init structs
Листинг 10-1 Инициализация проекта

Добавьте файл с именем main.go в папку structs с содержимым,
показанным в листинге 10-2.
package main
import "fmt"

func main() {
}

fmt.Println("Hello, Structs")
Листинг 10-2 Содержимое файла main.go в папке structs

Используйте командную строку для запуска команды, показанной в
листинге 10-3, в папке structs.
go run .
Листинг 10-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Hello, Structs

Определение и использование структуры
Пользовательские типы данных определяются с помощью функции
структур Go, которая демонстрируется в листинге 10-4.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
kayak := Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
fmt.Println(kayak.name, kayak.category, kayak.price)
kayak.price = 300

}

fmt.Println("Changed price:", kayak.price)

Листинг 10-4 Создание пользовательского типа данных в файле main.go в папке structs

Пользовательские типы данных известны в Go как структурные
типы и определяются с помощью ключевого слова type, имени и
ключевого слова struct. Скобки окружают ряд полей, каждое из
которых определяется именем и типом. Поля одного типа могут быть
объявлены вместе, как показано на рисунке 10-1, и все поля должны
иметь разные имена.

Рисунок 10-1 Определение типа структуры

Этот тип структуры называется Product и имеет три поля: поля
name и category содержат строковые значения, а поле price содержит
значение float64. Поля name и category имеют одинаковый тип и
могут быть определены вместе.
ГДЕ GO КЛАССЫ?
Go не делает различий между структурами и классами, как это
делают другие языки. Все пользовательские типы данных
определяются как структуры, и решение о передаче их по ссылке или
по значению принимается в зависимости от того, используется ли
указатель. Как я объяснял в главе 4, это дает тот же эффект, что и
наличие отдельных категорий типов, но с дополнительной
гибкостью, позволяя делать выбор каждый раз, когда используется
значение. Однако это требует большего усердия от программиста,
который должен продумать последствия своего выбора во время

кодирования. Ни один из подходов не лучше, и результаты по
существу одинаковы.

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

Рисунок 10-2 Создание значения структуры

Значение, созданное в листинге 10-4, представляет собой Product,
поле name которого имеет значение Kayak, поле category —
Watersports, а поле price — 275. Значение структуры присваивается
переменной с именем kayak.
Go привередлив к синтаксису и выдаст ошибку, если за конечным
значением поля не следует ни запятая, ни закрывающая фигурная
скобка. Обычно я предпочитаю конечные запятые, которые позволяют
поставить закрывающую фигурную скобку на следующую строку в
файле кода, как я сделал с синтаксисом литерала карты в главе 7.
Примечание
Go не позволяет использовать структуры с ключевым словом const,
и компилятор сообщит об ошибке, если вы попытаетесь определить

константную структуру. Для создания констант можно использовать
только типы данных, описанные в главе 9.

Использование значения структуры
Доступ к полям значения структуры осуществляется через имя,
присвоенное переменной, так что доступ к значению поля name
значения структуры, присвоенного переменной kayak, осуществляется
с помощью kayak.name, как показано на рисунок 10-3.

Рисунок 10-3 Доступ к полям структуры

Новые значения могут быть присвоены полю структуры с
использованием того же синтаксиса, как показано на рисунке 10-4.

Рисунок 10-4 Изменение поля структуры

Этот оператор присваивает значение 300 полю price значения
структуры Product, присвоенного переменной kayak. Код в листинге
10-4 выдает следующий результат при компиляции и выполнении:
Kayak Watersports 275
Changed price: 300
ПОНИМАНИЕ ТЕГОВ СТРУКТУРЫ

Тип структуры можно определить с помощью тегов, которые
предоставляют дополнительную информацию о том, как следует
обрабатывать поле. Теги структуры — это просто строки, которые
интерпретируются кодом, обрабатывающим значения структуры, с
использованием функций, предоставляемых пакетом reflect. См. в
главе 21 пример того, как можно использовать теги структур для
изменения способа кодирования структур в данных JSON, и см. в
главе 28 сведения о том, как самостоятельно получить доступ к
тегам структур.

Частичное присвоение значений структуры
При создании значения структуры не обязательно указывать значения
для всех полей, как показано в листинге 10-5.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
kayak := Product {
name: "Kayak",
category: "Watersports",
}
fmt.Println(kayak.name, kayak.category, kayak.price)
kayak.price = 300
fmt.Println("Changed price:", kayak.price)
}
Листинг 10-5 Назначение некоторых полей в файле main.go в папке structs

Для поля price структуры, назначенной переменной kayak,
начальное значение не указано. Если поле не указано, используется
нулевое значение для типа поля. В листинге 10-5 тип нуля для поля

price равен 0, потому что тип поля — float64; код выдает следующий
результат при компиляции и выполнении:
Kayak Watersports 0
Changed price: 300
Как видно из выходных данных, пропуск начального значения не
препятствует тому, чтобы значение впоследствии было присвоено
полю.
Нулевые типы назначаются всем полям, если вы определяете
переменную структурного типа, но не присваиваете ей значение, как
показано в листинге 10-6.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
kayak := Product {
name: "Kayak",
category: "Watersports",
}
fmt.Println(kayak.name, kayak.category, kayak.price)
kayak.price = 300
fmt.Println("Changed price:", kayak.price)
var lifejacket Product
fmt.Println("Name is zero value:", lifejacket.name == "")
fmt.Println("Category
is
zero
value:",
lifejacket.category == "")
fmt.Println("Price is zero value:", lifejacket.price ==
0)
}
Листинг 10-6 Неназначенная переменная в файле main.go в папке structs

Тип переменной lifejacket — Product, но ее полям не
присваиваются значения. Значение всех полей lifejacket равно нулю
для их типа, что подтверждается выходными данными из листинга 106:
Kayak Watersports 0
Changed price: 300
Name is zero value: true
Category is zero value: true
Price is zero value: true
ИСПОЛЬЗОВАНИЕ ФУНКЦИИ NEW ДЛЯ СОЗДАНИЯ
СТРУКТУРНЫХ ЗНАЧЕНИЙ
Вы можете увидеть код, который использует встроенную функцию
new для создания значений структуры, например:
...
var lifejacket = new(Product)
...
Результатом является указатель на значение структуры, поля
которого инициализируются нулевым значением их типа. Это
эквивалентно этому утверждению:
...
var lifejacket = &Product{}
...
Эти подходы взаимозаменяемы, и выбор между ними является
вопросом предпочтения.

Использование позиций полей для создания значений
структуры
Значения структуры могут быть определены без использования имен,
если типы значений соответствуют порядку, в котором поля
определяются типом структуры, как показано в листинге 10-7.
package main

import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
var kayak = Product { "Kayak", "Watersports", 275.00 }

}

fmt.Println("Name:", kayak.name)
fmt.Println("Category:", kayak.category)
fmt.Println("Price:", kayak.price)

Листинг 10-7 Пропуск имен полей в файле main.go в папке structs

Литеральный синтаксис, используемый для определения значения
структуры, содержит только значения, которые присваиваются полям
структуры в том порядке, в котором они указаны. Код в листинге 10-7
выводит следующий результат:
Name: Kayak
Category: Watersports
Price: 275

Определение встроенных полей
Если поле определено без имени, оно известно как встроенное поле, и
доступ к нему осуществляется с использованием имени его типа, как
показано в листинге 10-8.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}

type StockLevel struct {
Product
count int
}
stockItem := StockLevel {
Product: Product { "Kayak", "Watersports", 275.00 },
count: 100,
}

}

fmt.Println("Name:", stockItem.Product.name)
fmt.Println("Count:", stockItem.count)

Листинг 10-8 Определение встроенных полей в файле main.go в папке structs

Тип структуры StockLevel имеет два поля. Первое поле встроено и
определяется только с использованием типа, который является типом
структуры Product, как показано на рисунке 10-5

Рисунок 10-5 Определение встроенного поля

Доступ к встроенным полям осуществляется с использованием
имени типа поля, поэтому эта функция наиболее полезна для полей, тип
которых является структурой. В этом случае встроенное поле
определяется с типом Product, что означает, что оно назначается и
читается с использованием Product в качестве имени поля, например:
...
stockItem := StockLevel {
Product: Product { "Kayak", "Watersports", 275.00 },
count: 100,
}
...
fmt.Println(fmt.Sprint("Name: ", stockItem.Product.name))
...

Код в листинге 10-8 выдает следующий результат при компиляции и
выполнении:
Name: Kayak
Count: 100
Как отмечалось ранее, имена полей должны быть уникальными для
типа структуры, что означает, что вы можете определить только одно
встроенное поле для определенного типа. Если вам нужно определить
два поля одного типа, вам нужно будет присвоить имя одному из них,
как показано в листинге 10-9.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
type StockLevel struct {
Product
Alternate Product
count int
}
stockItem := StockLevel {
Product: Product { "Kayak", "Watersports", 275.00 },
Alternate: Product{"Lifejacket", "Watersports", 48.95
},
}

}

count: 100,

fmt.Println("Name:", stockItem.Product.name)
fmt.Println("Alt Name:", stockItem.Alternate.name)
Листинг 10-9 Определение дополнительного поля в файле main.go в папке structs

Тип StockLevel имеет два поля типа Product, но только одно из них
может быть встроенным полем. Для второго поля я присвоил имя, через
которое осуществляется доступ к полю. Код в листинге 10-9 выдает
следующийрезультат при компиляции и выполнении:
Name: Kayak
Alt Name: Lifejacket

Сравнение значений структуры
Значения структур сопоставимы, если можно сравнить все их поля. В
листинге 10-10 создается несколько значений структуры и применяется
оператор сравнения, чтобы определить, равны ли они.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
p1 := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
p2 := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
p3 := Product { name: "Kayak", category: "Boats", price:
275.00 }

}

fmt.Println("p1 == p2:", p1 == p2)
fmt.Println("p1 == p3:", p1 == p3)

Листинг 10-10 Сравнение значений структуры в файле main.go в папке structs

Значения структуры p1 и p2 равны, потому что все их поля равны.
Значения структуры p1 и p3 не равны, потому что значения,
присвоенные их полям category, различны. Скомпилируйте и
запустите проект, и вы увидите следующие результаты:

p1 == p2: true
p1 == p3: false
Структуры нельзя сравнивать, если тип структуры определяет поля
с несравнимыми типами, например срезы, как показано в листинге 1011.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
otherNames []string
}
p1 := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
p2 := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
p3 := Product { name: "Kayak", category: "Boats", price:
275.00 }

}

fmt.Println("p1 == p2:", p1 == p2)
fmt.Println("p1 == p3:", p1 == p3)
Листинг 10-11 Добавление несравнимого поля в файл main.go в папку structs

Как объяснялось в главе 7, оператор сравнения Go нельзя применять
к срезам, что означает невозможность сравнения значений Product.
При компиляции этот код выдает следующие ошибки:
.\main.go:17:33: invalid operation: p1
containing []string cannot be compared)
.\main.go:18:33: invalid operation: p1
containing []string cannot be compared)

Преобразование между типами структур

==

p2

(struct

==

p3

(struct

Тип структуры можно преобразовать в любой другой тип структуры с
теми же полями, что означает, что все поля имеют одинаковые имена и
типы и определяются в одном и том же порядке, как показано в
листинге 10-12.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
//otherNames []string
}
type Item struct {
name string
category string
price float64
}
prod := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
item := Item { name: "Kayak", category: "Watersports",
price: 275.00 }
fmt.Println("prod == item:", prod == Product(item))
}
Листинг 10-12 Преобразование типа структуры в файле main.go в папке structs

Значения, созданные из типов структур Product и Item, можно
сравнивать, поскольку они определяют одни и те же поля в одном и том
же порядке. Скомпилировать и выполнить проект; вы увидите
следующий вывод:
prod == item: true

Определение анонимных типов структур

Анонимные типы структур определяются без использования имени, как
показано в листинге 10-13.
package main
import "fmt"
func writeName(val struct {
name, category string
price float64}) {
fmt.Println("Name:", val.name)
}
func main() {
type Product struct {
name, category string
price float64
//otherNames []string
}
type Item struct {
name string
category string
price float64
}
prod := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
item := Item { name: "Stadium", category: "Soccer",
price: 75000 }

}

writeName(prod)
writeName(item)

Листинг 10-13 Определение анонимного типа структуры в файле main.go в папке structs

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

Name: Kayak
Name: Stadium
Я не нахожу эту функцию особенно полезной, поскольку она
показана в листинге 10-13, но есть вариант, который я использую:
определение анонимной структуры и присвоение ей значения за один
шаг. Это полезно при вызове кода, который проверяет типы, которые он
получает
во
время
выполнения,
используя
возможности,
предоставляемые пакетом reflect, который я описываю в главах 27–
29. Пакет reflect содержит расширенные функции, но он используется
другими частями стандартной библиотеки, такими как встроенная
поддержка кодирования данных JSON. Я подробно объясню функции
JSON в главе 21, но в этой главе листинг 10-14 демонстрирует
использование анонимной структуры для выбора полей, которые
должны быть включены в строку JSON.
package main
import (
"fmt"
"encoding/json"
"strings"
)
func main() {
type Product struct {
name, category string
price float64
}
prod := Product { name: "Kayak", category: "Watersports",
price: 275.00 }
var builder strings.Builder
json.NewEncoder(&builder).Encode(struct {
ProductName string
ProductPrice float64
}{
ProductName: prod.name,
ProductPrice: prod.price,

}

})
fmt.Println(builder.String())

Листинг 10-14 Присвоение значения анонимной структуре в файле main.go в папке
structs

Не беспокойтесь о пакетах encoding/json и strings, которые
описаны в последующих главах. В этом примере показано, как можно
определить анонимную структуру и присвоить ей значение за один шаг,
который я использую в листинге 10-14 для создания структуры с
полями ProductName и ProductPrice, которым я затем присваиваю
значения из полей Product. Скомпилировать и выполнить проект; вы
увидите следующий вывод:
{"ProductName":"Kayak","ProductPrice":275}

Создание массивов, срезов и карт, содержащих
структурные значения
Тип структуры можно не указывать при заполнении массивов, срезов и
карт значениями структуры, как показано в листинге 10-15.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
//otherNames []string
}
type StockLevel struct {
Product
Alternate Product
count int
}

},

array := [1]StockLevel {
{
Product: Product { "Kayak", "Watersports", 275.00

48.95 },

Alternate: Product{"Lifejacket", "Watersports",
count: 100,

},
}
fmt.Println("Array:", array[0].Product.name)
slice := []StockLevel {
{
Product: Product { "Kayak", "Watersports", 275.00
},
48.95 },
},

Alternate: Product{"Lifejacket", "Watersports",
count: 100,

}
fmt.Println("Slice:", slice[0].Product.name)

},

kvp := map[string]StockLevel {
"kayak": {
Product: Product { "Kayak", "Watersports", 275.00

48.95 },

Alternate: Product{"Lifejacket", "Watersports",
count: 100,

},

}

}
fmt.Println("Map:", kvp["kayak"].Product.name)

Листинг 10-15 Пропуск типа структуры в файле main.go в папке structs

Код в листинге 10-15 создает массив, срез и карту, все из которых
заполняются значением StockLevel. Компилятор может вывести тип
значения структуры из содержащейся структуры данных, что позволяет
выразить код более лаконично. В листинге 10-15 выводится следующий
результат:
Array: Kayak

Slice: Kayak
Map: Kayak

Понимание структур и указателей
Присвоение структуры новой переменной или использование
структуры в качестве параметра функции создает новое значение,
которое копирует значения поля, как показано в листинге 10-16.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
p1 := Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
p2 := p1
p1.name = "Original Kayak"

}

fmt.Println("P1:", p1.name)
fmt.Println("P2:", p2.name)

Листинг 10-16 Копирование значения структуры в файле main.go в папке structs

Значение структуры создается и присваивается переменной p1 и
копируется в переменную p2. Поле name первого значения структуры
изменяется, а затем записываются оба значения name. Вывод из
листинга 10-16 подтверждает, что при присвоении значения структуры
создается копия:

P1: Original Kayak
P2: Kayak
Как и другие типы данных, ссылки на значения структур можно
создавать с помощью указателей, как показано в листинге 10-17.
package main
import "fmt"
func main() {
type Product struct {
name, category string
price float64
}
p1 := Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
p2 := &p1
p1.name = "Original Kayak"

}

fmt.Println("P1:", p1.name)
fmt.Println("P2:", (*p2).name)
Листинг 10-17 Использование указателя на структуру в файле main.go в папке structs

Я использовал амперсанд для создания указателя на переменную p1
и присвоил адрес p2, тип которого становится *Product, что означает
указатель на значение Product. Обратите внимание, что я должен
использовать круглые скобки, чтобы следовать указателю на значение
структуры, а затем читать значение поля name, как показано на рисунке
10-6.

Рисунок 10-6 Чтение поля структуры через указатель

Эффект заключается в том, что изменение, внесенное в поле name,
считывается как через p1, так и через p2, создавая следующий вывод,
когда код компилируется и выполняется:
P1: Original Kayak
P2: Original Kayak

Понимание удобного синтаксиса указателя структуры
Доступ к полям структуры с помощью указателя неудобен, что является
проблемой, поскольку структуры обычно используются в качестве
аргументов и результатов функций, а указатели необходимы для того,
чтобы структуры не дублировались без необходимости и чтобы
изменения, сделанные функциями, влияли на значения, полученные в
качестве параметров, т.к. показано в листинге 10-18.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func calcTax(product *Product) {
if ((*product).price > 100) {
(*product).price += (*product).price * 0.2
}
}

func main() {
kayak := Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
calcTax(&kayak)

}

fmt.Println("Name:", kayak.name, "Category:",
kayak.category, "Price", kayak.price)

Листинг 10-18 Использование указателей структуры в файле main.go в папке structs

Этот код работает, но его трудно читать, особенно когда в одном и
том же блоке кода, например в теле метода calcTax, есть несколько
ссылок.
Чтобы упростить этот тип кода, Go будет следовать указателям на
поля структуры без символа звездочки, как показано в листинге 10-19.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func calcTax(product *Product) {
if (product.price > 100) {
product.price += product.price * 0.2
}
}
func main() {
kayak := Product {
name: "Kayak",
category: "Watersports",
price: 275,

}
calcTax(&kayak)

}

fmt.Println("Name:", kayak.name, "Category:",
kayak.category, "Price", kayak.price)

Листинг 10-19 Использование удобного синтаксиса указателя структуры в файле main.go
в папке structs

Звездочка и круглые скобки не требуются, что позволяет
рассматривать указатель на структуру так, как если бы он был
значением структуры, как показано на рисунке 10-7.

Рисунок 10-7 Использование структуры или указателя на структуру

Эта функция не меняет тип данных параметра функции, который
по-прежнему имеет значение *Product, и применяется только при
доступе к полям. Оба листинга 10-18 и 10-19 выдают следующий
результат:
Name: Kayak Category: Watersports Price 330

Понимание указателей на значения
В более ранних примерах указатели использовались в два этапа.
Первый шаг — создать значение и присвоить его переменной,
например:
...
kayak := Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
...

Второй шаг — использовать оператор адреса для создания
указателя, например:
...
calcTax(&kayak)
...
Нет необходимости присваивать переменной значение структуры
перед созданием указателя, а оператор адреса можно использовать
непосредственно с литеральным синтаксисом структуры, как показано
в листинге 10-20.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func calcTax(product *Product) {
if (product.price > 100) {
product.price += product.price * 0.2
}
}
func main() {
kayak := &Product {
name: "Kayak",
category: "Watersports",
price: 275,
}
calcTax(kayak)

}

fmt.Println("Name:", kayak.name, "Category:",
kayak.category, "Price", kayak.price)
Листинг 10-20 Создание указателя непосредственно в файле main.go в папке structs

Оператор адреса используется перед типом структуры, как показано
на рисунке 10-8.

Рисунок 10-8 Создание указателя на значение структуры

Код в листинге 10-20 использует только указатель на значение
Product, а это означает, что нет смысла создавать обычную
переменную и затем использовать ее для создания указателя.
Возможность создавать указатели непосредственно из значений может
помочь сделать код более кратким, как показано в листинге 10-21.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func calcTax(product *Product) *Product {
if (product.price > 100) {
product.price += product.price * 0.2
}
return product
}
func main() {
kayak := calcTax(&Product {
name: "Kayak",
category: "Watersports",
price: 275,
})
fmt.Println("Name:", kayak.name, "Category:",
kayak.category, "Price", kayak.price)

}
Листинг 10-21 Использование указателей непосредственно в файле main.go в папке
structs

Я изменил функцию calcTax, чтобы она выдавала результат,
который позволяет функции преобразовывать значение Product с
помощью указателя. В основной функции я использовал оператор
адреса с литеральным синтаксисом для создания значения Product и
передал указатель на него в функцию calcTax, присваивая
преобразованный результат переменной типа *Pointer. Оба листинга
10-20 и 10-21 выдают следующий результат:
Name: Kayak Category: Watersports Price 330

Понимание функций конструктора структуры
Функция-конструктор отвечает за создание значений структуры с
использованием значений, полученных через параметры, как показано в
листинге 10-22.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func newProduct(name, category string,
*Product {
return &Product{name, category, price}
}

price

func main() {
products := [2]*Product {
newProduct("Kayak", "Watersports", 275),
newProduct("Hat", "Skiing", 42.50),
}
for _, p := range products {

float64)

fmt.Println("Name:",
"Category:", p.category, "Price", p.price)
}
}

p.name,

Листинг 10-22 Определение функции конструктора в файле main.go в папке structs

Функции-конструкторы используются для согласованного создания
структурных значений. Функции-конструкторы обычно называются new
или New, за которыми следует тип структуры, так что функцияконструктор для создания значений Product называется newProduct. (Я
объясняю, почему имена функций-конструкторов часто начинаются с
заглавной буквы в главе 12.)
Функции-конструкторы возвращают указатели на структуры, а
оператор адреса используется непосредственно с литеральным
синтаксисом структуры, как показано на рисунке 10-9.

Рисунок 10-9 Использование указателей в функции-конструкторе

Мне нравится создавать значения в функциях-конструкторах,
полагаясь на позиции полей, как показано в листинге 10-22, хотя это
только мое предпочтение. Важно, чтобы вы не забывали возвращать
указатель, чтобы избежать дублирования значения структуры при
выходе из функции. В листинге 10-22 для хранения данных о товарах
используется массив, и вы можете увидеть использование указателей в
типе массива:
...
products := [2]*Product {
...
Этот тип задает массив, который будет содержать два указателя на
значения структуры Product. Код в листинге 10-22 при компиляции и
выполнении выдает следующий результат:

Name: Kayak Category: Watersports Price 275
Name: Hat Category: Skiing Price 42.5
Преимуществом использования функций-конструкторов является
согласованность, гарантирующая, что изменения в процессе
построения отражаются во всех значениях структуры, созданных
функцией. Например, в листинге 10-23 конструктор изменяется для
применения скидки ко всем продуктам.
...
func newProduct(name, category string, price
*Product {
return &Product{name, category, price - 10}
}
...

float64)

Листинг 10-23 Изменение конструктора в файле main.go в папке structs

Это простое изменение, но оно будет применено ко всем значениям
Product, созданным функцией newProduct, а это означает, что мне не
нужно находить все точки в коде, где создаются значения Product, и
изменять их по отдельности. К сожалению, Go не препятствует
использованию литерального синтаксиса, когда определена функцияконструктор, что означает необходимость тщательного использования
функций-конструкторов. Код в листинге 10-23 выдает следующий
результат:
Name: Kayak Category: Watersports Price 265
Name: Hat Category: Skiing Price 32.5

Использование типов указателей для полей структуры
Указатели также можно использовать для полей структур, включая
указатели на другие типы структур, как показано в листинге 10-24.
package main
import "fmt"
type Product struct {
name, category string
price float64

*Supplier
}
type Supplier struct {
name, city string
}
func newProduct(name, category string, price float64,
supplier *Supplier) *Product {
return &Product{name, category, price -10, supplier}
}
func main() {
acme := &Supplier { "Acme Co", "New York"}
products := [2]*Product {
newProduct("Kayak", "Watersports", 275, acme),
newProduct("Hat", "Skiing", 42.50, acme),
}

}

for _, p := range products {
fmt.Println("Name:", p.name, "Supplier:",
p.Supplier.name, p.Supplier.city)
}

Листинг 10-24 Использование указателей для полей структуры в файле main.go в папке structs

Я добавил к типу Product встроенное поле, которое использует тип
Supplier, и обновил функцию newProduct, чтобы она принимала
указатель на Supplier. Доступ к полям, определенным структурой
Supplier, осуществляется с использованием поля, определенного
структурой Product, как показано на рисунке 10-10.

Рисунок 10-10 Доступ к вложенному полю структуры

Обратите внимание, как Go обрабатывает использование типа
указателя для встроенного поля структуры, что позволяет мне
обращаться к полю по имени типа структуры, которым в данном
примере является Supplier. Код в листинге 10-24 выдает следующий
результат:
Name: Kayak Supplier: Acme Co New York
Name: Hat Supplier: Acme Co New York

Общие сведения о копировании поля указателя
При копировании структур необходимо соблюдать осторожность, чтобы
учесть влияние на поля указателя, как показано в листинге 10-25.
package main
import "fmt"
type Product struct {
name, category string
price float64
*Supplier
}
type Supplier struct {
name, city string
}
func newProduct(name, category string, price float64,
supplier *Supplier) *Product {
return &Product{name, category, price -10, supplier}
}
func main() {
acme := &Supplier { "Acme Co", "New York"}
p1 := newProduct("Kayak", "Watersports", 275, acme)
p2 := *p1
p1.name = "Original Kayak"
p1.Supplier.name = "BoatCo"

for _, p := range []Product { *p1, p2 } {
fmt.Println("Name:", p.name, "Supplier:",
p.Supplier.name, p.Supplier.city)
}
}
Листинг 10-25 Копирование структуры в файле main.go в папке structs

Функция newProduct используется для создания указателя на
значение Product, которое присваивается переменной с именем p1.
Указатель следует и присваивается переменной с именем p2, что
приводит к копированию значения Product. Поля p1.name и
p1.Supplier.name изменяются, а затем используется цикл for для
записи сведений об обоих значениях Product, что приводит к
следующему результату:
Name: Original Kayak Supplier: BoatCo New York
Name: Kayak Supplier: BoatCo New York
Выходные данные показывают, что изменение поля name затронуло
только одно из значений Product, в то время как изменение поля
Supplier.name затронуло оба. Это происходит потому, что при
копировании структуры Product был скопирован указатель,
присвоенный полю Supplier, а не значение, на которое он указывает,
создавая эффект, показанный на рисунке 10-11.

Рисунок 10-11 Эффект копирования структуры с полем указателя

Это часто называют поверхностной копией, когда копируются
указатели, но не значения, на которые они указывают. В Go нет
встроенной поддержки выполнения глубокого копирования, когда
указатели отслеживаются, а их значения дублируются. Вместо этого
необходимо выполнить копирование вручную, как показано в листинге
10-26.
package main
import "fmt"
type Product struct {
name, category string
price float64
*Supplier
}
type Supplier struct {
name, city string
}
func newProduct(name, category string, price float64,
supplier *Supplier) *Product {
return &Product{name, category, price -10, supplier}
}
func copyProduct(product *Product) Product {
p := *product
s := *product.Supplier
p.Supplier = &s
return p
}
func main() {
acme := &Supplier { "Acme Co", "New York"}
p1 := newProduct("Kayak", "Watersports", 275, acme)
p2 := copyProduct(p1)

p1.name = "Original Kayak"
p1.Supplier.name = "BoatCo"

}

for _, p := range []Product { *p1, p2 } {
fmt.Println("Name:", p.name, "Supplier:",
p.Supplier.name, p.Supplier.city)
}
Листинг 10-26 Копирование значения структуры в файле main.go в папке structs

Чтобы обеспечить дублирование Supplier, функция copyProduct
присваивает его отдельной переменной, а затем создает указатель на эту
переменную. Это неудобно, но эффект заключается в принудительном
копировании структуры, хотя этот метод специфичен для одного типа
структуры и должен повторяться для каждого поля вложенной
структуры. Вывод из листинга 10-26 показывает эффект глубокого
копирования:
Name: Original Kayak Supplier: BoatCo New York
Name: Kayak Supplier: Acme Co New York

Понимание нулевого значения для структур и указателей на
структуры
Нулевое значение для типа структуры — это значение структуры,
полям которой присвоен нулевой тип. Нулевое значение указателя на
структуру равно nil, как показано в листинге 10-27.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func main() {
var prod Product
var prodPtr *Product

fmt.Println("Value:", prod.name,
prod.price)
fmt.Println("Pointer:", prodPtr)
}

prod.category,

Листинг 10-27 Изучение нулевых типов в файле main.go в папке structs

Скомпилируйте и выполните проект, и вы увидите нулевые
значения, представленные в выводе, с пустыми строками для полей
name и category, поскольку пустая строка является нулевым значением
для строкового типа:
Value: 0
Pointer:
Существует ловушка, с которой я часто сталкиваюсь, когда
структура определяет поле с указателем на другой тип структуры, как
показано в листинге 10-28.
package main
import "fmt"
type Product struct {
name, category string
price float64
*Supplier
}
type Supplier struct {
name, city string
}
func main() {
var prod Product
var prodPtr *Product
fmt.Println("Value:", prod.name,
prod.price, prod.Supplier.name)
fmt.Println("Pointer:", prodPtr)
}

prod.category,

Листинг 10-28 Добавление поля указателя в файл main.go в папку structs

Проблема здесь заключается в попытке доступа к полю name
встроенной структуры. Нулевое значение встроенного поля равно nil,
что вызывает следующую ошибку времени выполнения:
panic: runtime error: invalid memory address or nil pointer
dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x5bc592]
goroutine 1 [running]:
main.main()
C:/structs/main.go:20 +0x92
exit status 2
Я сталкиваюсь с этой ошибкой так часто, что обычно
инициализирую поля указателя структуры, как показано в листинге 1029 и часто повторяется в последующих главах.
...
func main() {
var prod Product = Product{ Supplier: &Supplier{}}
var prodPtr *Product
fmt.Println("Value:", prod.name,
prod.price, prod.Supplier.name)
fmt.Println("Pointer:", prodPtr)
}
...

prod.category,

Листинг 10-29 Инициализация поля указателя структуры в файле main.go в папке structs

Это позволяет избежать ошибки времени выполнения, которую вы
можете увидеть в выводе, полученном при компиляции и выполнении
проекта:
Value: 0
Pointer:

Резюме

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

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

Ответ

Кто они такие? Методы — это функции, которые вызываются в структуре и имеют доступ ко
всем полям, определенным типом значения. Интерфейсы определяют наборы
методов, которые могут быть реализованы типами структур.
Почему они
полезны?

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

Как они
Методы определяются с помощью ключевого слова func, но с добавлением
используются? получателя. Интерфейсы определяются с использованием ключевых слов
type и interface.
Есть ли
подводные
камни или
ограничения?

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

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

Таблица 11-2 суммирует главу.
Таблица 11-2 Краткое содержание главы
Проблема

Решение

Листинг

Определить метод

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

4–8, 13–
15

Вызывать методы для ссылок на
значения структуры

Используйте указатель на полученный метод

9, 10

Проблема

Решение

Листинг

Определить методы для
неструктурных типов

Используйте псевдоним типа

11, 12

Описать общие характеристики,
которые будут общими для
нескольких типов

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

16

Реализовать интерфейс

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

17, 18

Использовать интерфейс

Вызовите методы для значения интерфейса

19–21

Решить, будут ли создаваться
копии значений структуры при
назначении переменным
интерфейса.

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

22–25

Сравнить значения интерфейса

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

26, 27

Доступ к динамическому типу
значения интерфейса

Используйте утверждение типа

28–31

Определить переменную,
Используйте пустой интерфейс
которой можно присвоить любое
значение

32–34

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем
methodAndInterfaces. Перейдите в папку methodAndInterfaces и
выполните команду, показанную в листинге 11-1, для инициализации
проекта.
go mod init methodsandinterfaces
Листинг 11-1 Инициализация проекта

Добавьте файл с именем main.go в папку methodAndInterfaces с
содержимым, показанным в листинге 11-2.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/pro-

go. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func main() {
products := []*Product {
{"Kayak", "Watersports", 275 },
{"Lifejacket", "Watersports", 48.95 },
{"Soccer Ball", "Soccer", 19.50},
}
for _, p := range products {
fmt.Println("Name:", p.name, "Category:", p.category,
"Price", p.price)
}
}
Листинг 11-2 Содержимое файла main.go в папке methodAndInterfaces

Используйте командную строку для запуска команды, показанной в
листинге 11-3, в папке methodAndInterfaces.
go run .
Листинг 11-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Name: Kayak Category: Watersports Price 275
Name: Lifejacket Category: Watersports Price 48.95
Name: Soccer Ball Category: Soccer Price 19.5

Определение и использование методов
Методы — это функции, которые можно вызывать через значение, и
они представляют собой удобный способ выражения функций, которые
работают с определенным типом. Лучший способ понять, как работают
методы, — начать с обычной функции, как показано в листинге 11-4.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func printDetails(product *Product) {
fmt.Println("Name:", product.name,
product.category,
"Price", product.price)
}

"Category:",

func main() {
products := []*Product {
{"Kayak", "Watersports", 275 },
{"Lifejacket", "Watersports", 48.95 },
{"Soccer Ball", "Soccer", 19.50},
}
for _, p := range products {
printDetails(p)
}
}
Листинг 11-4 Определение функции в файле main.go в папке methodAndInterfaces

Функция printDetails получает указатель на Product, который
используется для записи значения полей name, category и price.
Ключевым моментом в этом разделе является способ вызова функции
printDetails:

...
printDetails(p)
...
За именем функции следуют аргументы, заключенные в круглые
скобки. В листинге 11-5 реализована та же функциональность, что и в
методе.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func newProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}
func (product *Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.price)
}

float64)

"Category:",

func main() {
products := []*Product {
newProduct("Kayak", "Watersports", 275),
newProduct("Lifejacket", "Watersports", 48.95),
newProduct("Soccer Ball", "Soccer", 19.50),
}

}

for _, p := range products {
p.printDetails()
}
Листинг 11-5 Определение метода в файле main.go в папке methodAndInterfaces

Методы определяются как функции с использованием того же
ключевого слова func, но с добавлением приемника, обозначающего
специальный параметр, являющийся типом, с которым работает метод,
как показано на рисунке 11-1.

Рисунок 11-1 Метод

Тип получателя для этого метода — *Product, и ему дается имя
product, которое можно использовать в методе так же, как и любой
нормальный параметр функции. Не требуется никаких изменений в
кодовом блоке, который может обрабатывать приемник как обычный
параметр функции:
...
func (product *Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.price)
}
...

"Category:",

Что отличает методы от обычных функций, так это способ вызова
метода:
...
p.printDetails()
...
Методы вызываются через значение, тип которого соответствует
получателю. В этом случае я использую значение *Product,
сгенерированное циклом for, чтобы вызвать метод printDetails для
каждого значения в срезе и получить следующий результат:

Name: Kayak Category: Watersports Price 275
Name: Lifejacket Category: Watersports Price 48.95
Name: Soccer Ball Category: Soccer Price 19.5

Определение параметров метода и результатов
Методы могут определять параметры и результаты точно так же, как
обычные функции, как показано в листинге 11-6, но с добавлением
получателя.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func newProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}
func (product *Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.calcTax(0.2, 100))
}

float64)

"Category:",

func (product *Product) calcTax(rate, threshold float64)
float64 {
if (product.price > threshold) {
return product.price + (product.price * rate)
}
return product.price;
}
func main() {
products := []*Product {
newProduct("Kayak", "Watersports", 275),
newProduct("Lifejacket", "Watersports", 48.95),

}

}

newProduct("Soccer Ball", "Soccer", 19.50),

for _, p := range products {
p.printDetails()
}

Листинг 11-6 Параметр и результат в файле main.go в папке methodAndInterfaces

Параметры метода определяются между круглыми скобками,
которые следуют за именем, за которым следует тип результата, как
показано на рисунке 11-2.

Рисунок 11-2 Метод с параметрами и результатом

Метод calcTax определяет параметры rate и threshold и
возвращает результат float64. В блоке кода метода не требуется
никакой специальной обработки, чтобы отличить получатель от
обычных параметров.
При вызове метода аргументы предоставляются так же, как и для
обычной функции, например:
...
product.calcTax(0.2, 100)
...
В этом примере метод printDetails вызывает метод calcTax, что
приводит к следующему результату:
Name: Kayak Category: Watersports Price 330
Name: Lifejacket Category: Watersports Price 48.95
Name: Soccer Ball Category: Soccer Price 19.5

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

приемника должна быть уникальной, независимо от других
определенных параметров. В листинге 11-7 я определил методы с
одинаковыми именами, но разными типами получателей.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
type Supplier struct {
name, city string
}
func newProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}
func (product *Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.calcTax(0.2, 100))
}

float64)

"Category:",

func (product *Product) calcTax(rate, threshold float64)
float64 {
if (product.price > threshold) {
return product.price + (product.price * rate)
}
return product.price;
}
func (supplier *Supplier) printDetails() {
fmt.Println("Supplier:", supplier.name,
supplier.city)
}
func main() {

"City:",

products := []*Product {
newProduct("Kayak", "Watersports", 275),
newProduct("Lifejacket", "Watersports", 48.95),
newProduct("Soccer Ball", "Soccer", 19.50),
}
for _, p := range products {
p.printDetails()
}

}

suppliers := []*Supplier {
{ "Acme Co", "New York City"},
{ "BoatCo", "Chicago"},
}
for _,s := range suppliers {
s.printDetails()
}

Листинг 11-7 Методы с одинаковыми именами в файле main.go в папке methodAndInterfaces

Существуют методы printDetails как для типов *Product, так и
для *Supplier, что разрешено, поскольку каждый из них представляет
уникальную комбинацию имени и типа получателя. Код в листинге 117 выводит следующий результат:
Name: Kayak Category: Watersports Price 330
Name: Lifejacket Category: Watersports Price 48.95
Name: Soccer Ball Category: Soccer Price 19.5
Supplier: Acme Co City: New York City
Supplier: BoatCo City: Chicago
Компилятор сообщит об ошибке, если я попытаюсь определить
метод, который дублирует существующую комбинацию имя/
получатель, независимо от того, отличаются ли остальные параметры
метода, как показано в листинге 11-8.
package main
import "fmt"
type Product struct {
name, category string

price float64
}
type Supplier struct {
name, city string
}
// ...other methods omitted for brevity...
func (supplier *Supplier) printDetails() {
fmt.Println("Supplier:", supplier.name,
supplier.city)
}

"City:",

func (supplier *Supplier) printDetails(showName bool) {
if (showName) {
fmt.Println("Supplier:", supplier.name, "City:",
supplier.city)
} else {
fmt.Println("Supplier:", supplier.name)
}
}
func main() {
products := []*Product {
newProduct("Kayak", "Watersports", 275),
newProduct("Lifejacket", "Watersports", 48.95),
newProduct("Soccer Ball", "Soccer", 19.50),
}
for _, p := range products {
p.printDetails()
}

}

suppliers := []*Supplier {
{ "Acme Co", "New York City"},
{ "BoatCo", "Chicago"},
}
for _,s := range suppliers {
s.printDetails()
}

Листинг 11-8 Определение другого метода в файле main.go в папке методовAndInterfaces

Новый метод выдает следующую ошибку компилятора:
# command-line-arguments
.\main.go:34:6: method redeclared: Supplier.printDetails
method(*Supplier) func()
method(*Supplier) func(bool)
.\main.go:34:27: (*Supplier).printDetails redeclared in this
block
previous declaration at .\main.go:30:6

Понимание получателей указателей и значений
Метод, получатель которого является типом указателя, также может
быть вызван через обычное значение базового типа, а это означает, что
метод типа *Product, например, может использоваться со значением
Product, как показано в листинге 11-9.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
// type Supplier struct {
//
name, city string
// }
// func newProduct(name, category string, price float64)
*Product {
//
return &Product{ name, category, price }
// }
func (product *Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.calcTax(0.2, 100))
}

"Category:",

func (product *Product) calcTax(rate, threshold float64)
float64 {
if (product.price > threshold) {
return product.price + (product.price * rate)
}
return product.price;
}
// func (supplier *Supplier) printDetails() {
//
fmt.Println("Supplier:", supplier.name, "City:",
supplier.city)
// }
func main() {
kayak := Product { "Kayak", "Watersports", 275 }
kayak.printDetails()
}
Листинг 11-9 Вызов метода в файле main.go в папке methodAndInterfaces

Переменной kayak присваивается значение Product, но она
используется с методом printDetails, получателем которого является
*Product. Go позаботится о несоответствии и без проблем вызовет
метод. Верен и противоположный процесс: метод, который получает
значение, может быть вызван с помощью указателя, как показано в
листинге 11-10.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
func (product Product) printDetails() {
fmt.Println("Name:", product.name,
product.category,
"Price", product.calcTax(0.2, 100))
}

"Category:",

func (product *Product) calcTax(rate, threshold float64)
float64 {
if (product.price > threshold) {
return product.price + (product.price * rate)
}
return product.price;
}
func main() {
kayak := &Product { "Kayak", "Watersports", 275 }
kayak.printDetails()
}
Листинг 11-10 Вызов метода в файле main.go в папке methodAndInterfaces

Эта функция означает, что вы можете писать методы в зависимости
от того, как вы хотите, чтобы они вели себя, используя указатели, чтобы
избежать копирования значений или позволить получателю быть
измененным методом.
Примечание
Одним из результатов этой функции является то, что типы значений
и указателей считаются одинаковыми, когда речь идет о перегрузке
методов, а это означает, что метод с именем printDetails, тип
получателя которого — Product, будет конфликтовать с методом
printDetails, тип получателя которого — *Product.
Оба листинга 11-9 и 11-10 выдают следующий результат:
Name: Kayak Category: Watersports Price 330
ВЫЗОВ МЕТОДОВ ЧЕРЕЗ ТИП ПОЛУЧАТЕЛЯ
Необычным аспектом методов Go является то, что они могут
вызываться с использованием типа получателя, поэтому метод с
такой сигнатурой:
...
func (product Product) printDetails() {
...

можно вызывать так:
...
Product.printDetails(Product{ "Kayak", "Watersports", 275
})
...
За именем типа получателя метода, Product, в данном случае
следует точка и имя метода. Аргумент — это значение Product,
которое будет использоваться в качестве значения получателя.
Функция автоматического сопоставления указателя и значения,
показанная в листингах 11-9 и 11-10, не применяется при вызове
метода через его тип получателя, что означает, что метод с
сигнатурой указателя, например:
...
func (product *Product) printDetails() {
...
должен вызываться через тип указателя и передавать аргумент
указателя, например:
...
(*Product).printDetails(&Product{
275 })
...

"Kayak",

"Watersports",

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

Определение методов для псевдонимов типов
Методы могут быть определены для любого типа, определенного в
текущем пакете. Я объясню, как добавлять пакеты в проект в главе 12,
но для этой главы есть один файл кода, содержащий один пакет, а это
означает, что методы могут быть определены только для типов,
определенных в файле main.go.

Но это не ограничивает методы только структурами, потому что
ключевое слово type может использоваться для создания псевдонимов
для любого типа, а методы могут быть определены для псевдонима. (Я
ввел ключевое слово type в главе 9, чтобы упростить работу с типами
функций.) В листинге 11-11 создается псевдоним и метод.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
type ProductList []Product
func
(products
*ProductList)
calcCategoryTotals()
map[string]float64 {
totals := make(map[string]float64)
for _, p := range *products {
totals[p.category] = totals[p.category] + p.price
}
return totals
}
func main() {
products := ProductList {
{ "Kayak", "Watersports", 275 },
{ "Lifejacket", "Watersports", 48.95 },
{"Soccer Ball", "Soccer", 19.50 },
}
for
category,
total
:=
range
products.calcCategoryTotals() {
fmt.Println("Category: ", category, "Total:", total)
}
}
Листинг 11-11 Определение метода для псевдонима типа в файле main.go в папке
methodAndInterfaces

Ключевое слово type используется для создания псевдонима для
типа []Product с именем ProductList. Этот тип можно использовать
для определения методов либо непосредственно для приемников типа
значения, либо с помощью указателя, как в этом примере.
Вы не всегда сможете получить данные с типом, необходимым для
вызова метода, определенного для псевдонима, например, при
обработке результатов функции. В этих ситуациях вы можете
выполнить преобразование типа, как показано в листинге 11-12.
package main
import "fmt"
type Product struct {
name, category string
price float64
}
type ProductList []Product
func
(products
*ProductList)
calcCategoryTotals()
map[string]float64 {
totals := make(map[string]float64)
for _, p := range *products {
totals[p.category] = totals[p.category] + p.price
}
return totals
}
func getProducts() []Product {
return []Product {
{ "Kayak", "Watersports", 275 },
{ "Lifejacket", "Watersports", 48.95 },
{"Soccer Ball", "Soccer", 19.50 },
}
}
func main() {
products := ProductList(getProducts())

for
category,
total
:=
range
products.calcCategoryTotals() {
fmt.Println("Category: ", category, "Total:", total)
}
}
Листинг 11-12 Выполнение преобразования типов в файле main.go в папке
methodAndInterfaces

Результатом функции getProducts является []Product, который
преобразуется в ProductList с явным преобразованием, позволяющим
использовать метод, определенный для псевдонима. Код в листингах
11-11 и 11-11 выдает следующий результат:
Category: Watersports Total: 323.95
Category: Soccer Total: 19.5

Размещение типов и методов в отдельных
файлах
По мере усложнения проекта количество кода, необходимого для
определения пользовательских типов и их методов, быстро становится
слишком большим для управления в одном файле кода. Проекты Go
могут быть структурированы в несколько файлов, которые компилятор
объединяет при сборке проекта.
Примеры в следующем разделе слишком длинные, чтобы их можно
было выразить в одном листинге кода, не заполняя оставшуюся часть
главы длинными разделами кода, которые не меняются, поэтому я
собираюсь представить несколько файлов кода.
Эта функция является частью поддержки Go для пакетов, которая
предоставляет различные способы структурирования файлов кода в
проекте и которые я описываю в главе 12. В этой главе я собираюсь
использовать самый простой аспект пакетов, который заключается в
использовании несколько файлов кода в папке проекта.
Добавьте файл с именем product.go в папку methodAndInterfaces
с содержимым, показанным в листинге 11-13.
package main
type Product struct {

}

name, category string
price float64
Листинг 11-13 Содержимое файла product.go в папке methodAndInterfaces

Добавьте файл с именем service.go в папку methodAndInterfaces
и используйте его для определения типа, показанного в листинге 11-14.
package main
type Service struct {
description string
durationMonths int
monthlyFee float64
}
Листинг 11-14 Содержимое файла service.go в папке методовAndInterfaces

Наконец, замените содержимое файла main.go тем, что показано в
листинге 11-15.
package main
import "fmt"
func main() {
kayak := Product { "Kayak", "Watersports", 275 }
insurance := Service {"Boat Cover", 12, 89.50 }
fmt.Println("Product:",

kayak.name,

"Price:",

kayak.price)
fmt.Println("Service:", insurance.description, "Price:",
insurance.monthlyFee * float64(insurance.durationMonths))
}
Листинг 11-15 Замена содержимого файла main.go в папке methodAndInterfaces

Этот код создает значения, используя типы структур, определенные
в других файлах. Скомпилируйте и выполните проект, который выдаст
следующий результат:
Product: Kayak Price: 275

Service: Boat Cover Price: 1074

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

Определение интерфейса
Эта проблема решается с помощью интерфейсов, которые описывают
набор методов без указания реализации этих методов. Если тип
реализует все методы, определенные интерфейсом, то значение этого
типа можно использовать везде, где разрешен интерфейс. Первым
шагом является определение интерфейса, как показано в листинге 1116.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
kayak := Product { "Kayak", "Watersports", 275 }
insurance := Service {"Boat Cover", 12, 89.50 }
fmt.Println("Product:",
kayak.name,
"Price:",
kayak.price)
fmt.Println("Service:", insurance.description, "Price:",
insurance.monthlyFee
*
float64(insurance.durationMonths))

}
Листинг 11-16 Определение интерфейса в файле main.go в папке methodAndInterfaces

Интерфейс определяется с помощью ключевого слова type, имени,
ключевого слова interface и тела, состоящего из сигнатур методов,
заключенных в фигурные скобки, как показано на рисунке 11-3.

Рисунок 11-3 Определение интерфейса

Этому интерфейсу было присвоено имя Expense, а телоинтерфейса
содержит единственную сигнатуру метода. Сигнатуры методов состоят
из имени, параметров и типов результатов, как показано на рисунке 114.

Рисунок 11-4 Сигнатура метода

Интерфейс Expense описывает два метода. Первый метод — это
getName, который не принимает аргументов и возвращает строку.

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

Реализация интерфейса
Чтобы реализовать интерфейс, все методы, указанные интерфейсом,
должны быть определены для типа структуры, как показано в листинге
11-17.
package main
type Product struct {
name, category string
price float64
}
func (p Product) getName() string {
return p.name
}
func (p Product) getCost(_ bool) float64 {
return p.price
}
Листинг 11-17 Реализация интерфейса в файле product.go в папке methodAndInterfaces

В большинстве языков требуется использование ключевого слова,
чтобы указать, когда тип реализует интерфейс, но Go просто требует,
чтобы все методы, указанные интерфейсом, были определены. Go
позволяет использовать разные имена параметров и результатов, но
методы должны иметь одинаковые имена, типы параметров и типы
результатов. В листинге 11-18 определены методы, необходимые для
реализации интерфейса для типа Service.
package main
type Service struct {
description string
durationMonths int
monthlyFee float64
}
func (s Service) getName() string {

}

return s.description

func (s Service) getCost(recur bool) float64 {
if (recur) {
return s.monthlyFee * float64(s.durationMonths)
}
return s.monthlyFee
}
Листинг 11-18 Реализация интерфейса в файле service.go в папке methodAndInterfaces

Интерфейсы описывают только методы, а не поля. По этой причине
в интерфейсах часто указываются методы, которые возвращают
значения, хранящиеся в полях структуры, например метод getName в
листингах 11-17 и 11-18.

Использование интерфейса
После того как вы реализовали интерфейс, вы можете ссылаться на
значения через тип интерфейса, как показано в листинге 11-19.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
expenses := []Expense {
Product { "Kayak", "Watersports", 275 },
Service {"Boat Cover", 12, 89.50 },
}
for _, expense := range expenses {
fmt.Println("Expense:", expense.getName(), "Cost:",
expense.getCost(true))
}
}

Листинг 11-19 Использование интерфейса в файле main.go в папке methodAndInterfaces

В этом примере я определил срез Expense и заполнил его
значениями Product и Service, созданными с использованием
литерального синтаксиса. Срез используется в цикле for, который
вызывает методы getName и getCost для каждого значения.
Переменные, тип которых является интерфейсом, имеют два типа:
статический тип и динамический тип. Статический тип — это
интерфейсный тип. Динамический тип — это тип значения,
присвоенного переменной, которая реализует интерфейс, например
Product или Service в данном случае. Статический тип никогда не
меняется — например, статический тип переменной Expense — это
всегда Expense, — но динамический тип может измениться путем
присвоения нового значения другого типа, реализующего интерфейс.
Цикл for имеет дело только со статическим типом — Expense — и
не знает (и не должен знать) динамический тип этих значений.
Использование интерфейса позволило мне сгруппировать разрозненные
динамические типы вместе и использовать общие методы, указанные
для статического типа интерфейса. Скомпилируйте и выполните
проект; вы получите следующий вывод:
Expense: Kayak Cost: 275
Expense: Boat Cover Cost: 1074

Использование интерфейса в функции
Типы интерфейсов могут использоваться для переменных, параметров
функций и результатов функций, как показано в листинге 11-20.
Примечание
Методы не могут быть определены с использованием интерфейсов в
качестве приемников. С интерфейсом связаны только те методы,
которые он указывает.
package main
import "fmt"
type Expense interface {

}

getName() string
getCost(annual bool) float64

func calcTotal(expenses []Expense) (total float64) {
for _, item := range expenses {
total += item.getCost(true)
}
return
}
func main() {
expenses := []Expense {
Product { "Kayak", "Watersports", 275 },
Service {"Boat Cover", 12, 89.50 },
}
for _, expense := range expenses {
fmt.Println("Expense:", expense.getName(), "Cost:",
expense.getCost(true))
}
fmt.Println("Total:", calcTotal(expenses))
}
Листинг 11-20 Использование интерфейса в файле main.go в папке methodAndInterfaces

Функция calcTotal получает срез, содержащий значения Expense,
которые обрабатываются с помощью цикла for для получения
итогового значения float64. Скомпилируйте и выполните проект,
который выдаст следующий результат:
Expense: Kayak Cost: 275
Expense: Boat Cover Cost: 1074
Total: 1349

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

package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func calcTotal(expenses []Expense) (total float64) {
for _, item := range expenses {
total += item.getCost(true)
}
return
}
type Account struct {
accountNumber int
expenses []Expense
}
func main() {
account := Account {
accountNumber: 12345,
expenses: []Expense {
Product { "Kayak", "Watersports", 275 },
Service {"Boat Cover", 12, 89.50 },
},
}
for _, expense := range account.expenses {
fmt.Println("Expense:", expense.getName(), "Cost:",
expense.getCost(true))
}
fmt.Println("Total:", calcTotal(account.expenses))
}
Листинг 11-21 Использование интерфейса в поле структуры в файле main.go в папке
methodAndInterfaces

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

и любое другое поле. Скомпилируйте и выполните проект, который
выдаст следующий результат:
Expense: Kayak Cost: 275
Expense: Boat Cover Cost: 1074
Total: 1349

Понимание эффекта приемников метода указателя
Методы, определенные типами Product и Service, имеют приемники
значений, что означает, что методы будут вызываться с копиями
значения Product или Service. Это может сбивать с толку, поэтому в
листинге 11-22 приведен простой пример.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
product := Product { "Kayak", "Watersports", 275 }
var expense Expense = product
product.price = 100
fmt.Println("Product field value:", product.price)
fmt.Println("Expense
method
result:",
expense.getCost(false))
}
Листинг 11-22 Использование значения в файле main.go в папке методовAndInterfaces

В этом примере создается значение структуры Product, оно
присваивается переменной Expense, изменяется значение поля price
значения структуры и выводится значение поля напрямую и через
метод интерфейса. Скомпилируйте и выполните код; вы получите
следующий результат:

Product field value: 100
Expense method result: 275
Значение Product было скопировано, когда оно было присвоено
переменной Expense, что означает, что изменение поля price не влияет
на результат метода getCost.
Указатель на значение структуры можно использовать при
назначении переменной интерфейса, как показано в листинге 11-23.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
product := Product { "Kayak", "Watersports", 275 }
var expense Expense = &product
product.price = 100
fmt.Println("Product field value:", product.price)
fmt.Println("Expense
method
result:",
expense.getCost(false))
}
Листинг 11-23 Использование указателя в файле main.go в папке методовAndInterfaces

Использование указателя означает, что ссылка на значение Product
присваивается переменной Expense, но это не меняет тип переменной
интерфейса, который по-прежнему является Expense. Скомпилируйте и
выполните проект, и вы увидите эффект ссылки в выводе, который
показывает, что изменение в поле price отражается в результате метода
getCost:
Product field value: 100
Expense method result: 100

Это полезно, потому что это означает, что вы можете выбрать, как
будет использоваться значение, присвоенное переменной интерфейса.
Но это также может противоречить здравому смыслу, потому что
переменная всегда имеет тип Expense, независимо от того, присвоено
ли ей значение Product или *Product.
Вы можете принудительно использовать ссылки, указав получатели
указателей при реализации методов интерфейса, как показано в
листинге 11-24.
package main
type Product struct {
name, category string
price float64
}
func (p *Product) getName() string {
return p.name
}
func (p *Product) getCost(_ bool) float64 {
return p.price
}
Листинг 11-24 Использование приемников указателей в файле product.go в папке
methodAndInterfaces

Это небольшое изменение, но оно означает, что тип Product больше
не реализует интерфейс Expense, поскольку необходимые методы
больше не определены. Вместо этого интерфейс реализуется типом
*Product, что означает, что указатели на значения Product можно
рассматривать как значения Expense, но не как обычные значения.
Скомпилируйте и выполните проект, и вы получите тот же результат,
что и в листинге 11-23:
Product field value: 100
Expense method result: 100
В листинге 11-25 значение Product присваивается переменной
Expense.

package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
product := Product { "Kayak", "Watersports", 275 }
var expense Expense = product
product.price = 100
fmt.Println("Product field value:", product.price)
fmt.Println("Expense
method
result:",
expense.getCost(false))
}
Листинг 11-25 Присвоение значения в файле main.go в папке methodAndInterfaces

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

ошибку,

.\main.go:14:9: cannot use product (type Product) as type
Expense in assignment:
Product does not implement Expense (getCost method
has pointer receiver)

Сравнение значений интерфейса
Значения интерфейса можно сравнивать с помощью операторов
сравнения Go, как показано в листинге 11-26. Два значения интерфейса
равны, если они имеют одинаковый динамический тип и все их поля
равны.
package main
import "fmt"

type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
var
var
var
var

}

e1
e2
e3
e4

Expense
Expense
Expense
Expense

=
=
=
=

&Product { name: "Kayak" }
&Product { name: "Kayak" }
Service { description: "Boat Cover" }
Service { description: "Boat Cover" }

fmt.Println("e1 == e2", e1 == e2)
fmt.Println("e3 == e4", e3 == e4)

Листинг 11-26 Сравнение значений интерфейса в файле main.go в папке methodAndInterfaces

Следует соблюдать осторожность при сравнении значений
интерфейса, и неизбежно требуются некоторые знания о динамических
типах.
Первые два значения Expense не равны. Это связано с тем, что
динамический тип этих значений является типом указателя, а указатели
равны, только если они указывают на одно и то же место в памяти.
Вторые два значения Expense равны, потому что это простые значения
структуры с одинаковыми значениями поля. Скомпилируйте и
выполните проект, чтобы подтвердить равенство этих значений:
e1 == e2 false
e3 == e4 true
Проверки на равенство интерфейсов также могут вызывать ошибки
времени выполнения, если динамический тип не сопоставим. Листинг
11-27 добавляет поле в структуру Service.
package main
type Service struct {
description string
durationMonths int
monthlyFee float64
features []string

}
func (s Service) getName() string {
return s.description
}
func (s Service) getCost(recur bool) float64 {
if (recur) {
return s.monthlyFee * float64(s.durationMonths)
}
return s.monthlyFee
}
Листинг 11-27 Добавление поля в файл service.go в папку methodAndServices

Как объяснялось в главе 7, срезы несопоставимы. Скомпилируйте и
выполните проект, и вы увидите эффект нового поля:
panic:
runtime
error:
comparing
main.Service
goroutine 1 [running]:
main.main()
C:/main.go:20 +0x1c5
exit status 2

uncomparable

type

Выполнение утверждений типа
Интерфейсы могут быть полезны, но они могут создавать проблемы, и
часто полезно иметь возможность прямого доступа к динамическому
типу, что известно как сужение типа, процесс перехода от менее
точного типа к более точному типу.
Утверждение типа используется для доступа к динамическому
типу значения интерфейса, как показано в листинге 11-28.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}

func main() {
expenses := []Expense {
Service {"Boat Cover", 12, 89.50, []string{} },
Service {"Paddle Protect", 12, 8, []string{} },
}

}

for _, expense := range expenses {
s := expense.(Service)
fmt.Println("Service:", s.description, "Price:",
s.monthlyFee * float64(s.durationMonths))
}

Листинг 11-28 Использование утверждения типа в файле main.go в папке
methodAndInterfaces

Утверждение типа выполняется путем применения точки после
значения, за которым следует целевой тип в круглых скобках, как
показано на рисунке 11-5.

Рисунок 11-5 Утверждение типа

В листинге 11-28 я использовал утверждение типа для доступа к
динамическому значению Service из среза типов интерфейса Expense.
Когда у меня есть значение службы для работы, я могу использовать
все поля и методы, определенные для типа Service, а не только методы,
определенные интерфейсом Expense.
УТВЕРЖДЕНИЕ ТИПА ПРОТИВ ПРЕОБРАЗОВАНИЯ ТИПА
Не путайте утверждения типа, как показано на рисунке 11-6, с
синтаксисом преобразования типов, описанным в главе 5.
Утверждения типа можно применять только к интерфейсам, и они

используются для того, чтобы сообщить компилятору, что значение
интерфейса
имеет
определенный
динамический
тип.
Преобразования типов могут применяться только к определенным
типам, а не к интерфейсам, и только в том случае, если структура
этих типов совместима, например, преобразование между типами
структур с одинаковыми полями.
Код в листинге 11-28 выдает следующий результат при компиляции и
выполнении:
Service: Boat Cover Price: 1074
Service: Paddle Protect Price: 96

Тестирование перед выполнением утверждения типа
Когда используется утверждение типа, компилятор полагает, что у
программиста больше знаний и знаний о динамических типах в коде,
чем он может сделать вывод, например, что срез Expense содержит
только значения Supplier. Чтобы увидеть, что происходит, когда это не
так, в листинге 11-29 к срезу Expense добавляется значение *Product.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
expenses := []Expense {
Service {"Boat Cover", 12, 89.50, []string{} },
Service {"Paddle Protect", 12, 8, []string{} },
&Product { "Kayak", "Watersports", 275 },
}
for _, expense := range expenses {
s := expense.(Service)
fmt.Println("Service:", s.description, "Price:",

}

}

s.monthlyFee * float64(s.durationMonths))

Листинг 11-29 Смешивание динамических типов в файле main.go в папке
methodAndInterfaces

Скомпилируйте и выполните проект; вы увидите следующую
ошибку при выполнении кода:
panic: interface conversion: main.Expense is *main.Product,
not main.Service
Среда выполнения Go попыталась выполнить утверждение и
потерпела неудачу. Чтобы избежать этой проблемы, существует
специальная форма утверждения типа, которая указывает, может ли
быть выполнено утверждение, как показано в листинге 11-30.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {
expenses := []Expense {
Service {"Boat Cover", 12, 89.50, []string{} },
Service {"Paddle Protect", 12, 8, []string{} },
&Product { "Kayak", "Watersports", 275 },
}
for _, expense := range expenses {
if s, ok := expense.(Service); ok {
fmt.Println("Service:", s.description, "Price:",
s.monthlyFee * float64(s.durationMonths))
} else {
fmt.Println("Expense:", expense.getName(),
"Cost:", expense.getCost(true))
}

}

}
Листинг 11-30 Тестирование утверждения в файле main.go в папке methodAndInterfaces

Утверждения типа могут давать два результата, как показано на
рисунке 11-6. Первому результату присваивается динамический тип, а
второму результату присваивается bool значение, указывающее, можно
ли выполнить утверждение.

Рисунок 11-6 Два результата утверждения типа

Значение bool можно использовать с оператором if для выполнения
операторов для определенного динамического типа. Скомпилируйте и
выполните проект; вы увидите следующий вывод:
Service: Boat Cover Price: 1074
Service: Paddle Protect Price: 96
Expense: Kayak Cost: 275

Включение динамических типов
Операторы Go switch могут использоваться для доступа к
динамическим типам, как показано в листинге 11-31, что может быть
более кратким способом выполнения утверждений типа с операторами
if.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
func main() {

expenses := []Expense {
Service {"Boat Cover", 12, 89.50, []string{} },
Service {"Paddle Protect", 12, 8, []string{} },
&Product { "Kayak", "Watersports", 275 },
}
for _, expense := range expenses {
switch value := expense.(type) {
case Service:
fmt.Println("Service:", value.description,
"Price:",
value.monthlyFee *
float64(value.durationMonths))
case *Product:
fmt.Println("Product:", value.name, "Price:",
value.price)
default:
fmt.Println("Expense:", expense.getName(),
"Cost:", expense.getCost(true))
}
}
}
Листинг 11-31 Включение типов в файле main.go в папке methodAndInterfaces

Оператор switch использует специальное утверждение типа, в
котором используется ключевое слово type, как показано на рисунке
11-7.

Рисунок 11-7 Переключатель типа

Каждый оператор case указывает тип и блок кода, который будет
выполняться, когда значение, оцениваемое оператором switch, имеет
указанный тип. Компилятор Go достаточно умен, чтобы понять
взаимосвязь между значениями, оцениваемыми оператором switch, и
не будет разрешать операторы case для типов, которые не совпадают.
Например, компилятор будет жаловаться, если имеется оператор case

для типа Product, потому что оператор switch оценивает значения
Expense, а тип Product не имеет методов, необходимых для реализации
интерфейса (поскольку методы в файле product.go использовать
приемники указателей, показанные в листинге 11-24).
В операторе case результат может рассматриваться как указанный
тип, а это означает, что в операторе case, указывающем, например, тип
Supplier, могут использоваться все поля и методы, определенные
типом Supplier.
Оператор default можно использовать для указания блока кода,
который будет выполняться, когда ни один из операторов case не
совпадает. Скомпилируйте и запустите проект, и вы увидите
следующий вывод:
Service: Boat Cover Price: 1074
Service: Paddle Protect Price: 96
Product: Kayak Price: 275

Использование пустого интерфейса
Go позволяет пользователю пустого интерфейса — то есть интерфейса,
не определяющего методы — представлять любой тип, что может быть
полезным способом группировки разрозненных типов, не имеющих
общих характеристик, как показано в листинге 11-32.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
type Person struct {
name, city string
}
func main() {

var expense Expense = &Product { "Kayak", "Watersports",
275 }
data := []interface{} {
expense,
Product { "Lifejacket", "Watersports", 48.95 },
Service {"Boat Cover", 12, 89.50, []string{} },
Person { "Alice", "London"},
&Person { "Bob", "New York"},
"This is a string",
100,
true,
}
for _, item := range data {
switch value := item.(type) {
case Product:
fmt.Println("Product:", value.name, "Price:",
value.price)
case *Product:
fmt.Println("Product Pointer:", value.name,
"Price:", value.price)
case Service:
fmt.Println("Service:", value.description,
"Price:",
value.monthlyFee *
float64(value.durationMonths))
case Person:
fmt.Println("Person:", value.name, "City:",
value.city)
case *Person:
fmt.Println("Person Pointer:", value.name,
"City:", value.city)
case string, bool, int:
fmt.Println("Built-in type:", value)
default:
fmt.Println("Default:", value)

}

}

}

Листинг 11-32 Использование пустого интерфейса в файле main.go в папке
methodAndInterfaces

Пустой интерфейс используется в литеральном синтаксисе,
определяемом ключевым словом interface и пустыми фигурными
скобками, как показано на рисунке 11-8.

Рисунок 11-8 Пустой интерфейс

Пустой интерфейс представляет все типы, включая встроенные
типы и любые определенные структуры и интерфейсы. В листинге я
определяю пустой срез массива со смесью значений Product, *Product,
Service, Person, *Person, string, int и bool. Срез обрабатывается
циклом for с операторами switch, которые сужают каждое значение до
определенного типа. Скомпилируйте и выполните проект, который
выдаст следующий результат:
Product Pointer: Kayak Price: 275
Product: Lifejacket Price: 48.95
Service: Boat Cover Price: 1074
Person: Alice City: London
Person Pointer: Bob City: New York
Built-in type: This is a string
Built-in type: 100
Built-in type: true

Использование пустого интерфейса для параметров
функций
Пустой интерфейс можно использовать в качестве типа параметра
функции, что позволяет вызывать функцию с любым значением, как
показано в листинге 11-33.
package main
import "fmt"
type Expense interface {
getName() string

}

getCost(annual bool) float64

type Person struct {
name, city string
}
func processItem(item interface{}) {
switch value := item.(type) {
case Product:
fmt.Println("Product:", value.name, "Price:",
value.price)
case *Product:
fmt.Println("Product Pointer:", value.name,
"Price:", value.price)
case Service:
fmt.Println("Service:", value.description,
"Price:",
value.monthlyFee *
float64(value.durationMonths))
case Person:
fmt.Println("Person:", value.name, "City:",
value.city)
case *Person:
fmt.Println("Person Pointer:", value.name,
"City:", value.city)
case string, bool, int:
fmt.Println("Built-in type:", value)
default:
fmt.Println("Default:", value)
}
}
func main() {
var expense Expense = &Product { "Kayak", "Watersports",
275 }
data := []interface{} {
expense,
Product { "Lifejacket", "Watersports", 48.95 },
Service {"Boat Cover", 12, 89.50, []string{} },
Person { "Alice", "London"},

}

}

&Person { "Bob", "New York"},
"This is a string",
100,
true,

for _, item := range data {
processItem(item)
}

Листинг 11-33 Использование пустого параметра интерфейса в файле main.go в папке
methodAndInterfaces

Пустой интерфейс также можно использовать для переменных
параметров, что позволяет вызывать функцию с любым количеством
аргументов, каждый из которых может быть любого типа, как показано
в листинге 11-34.
package main
import "fmt"
type Expense interface {
getName() string
getCost(annual bool) float64
}
type Person struct {
name, city string
}
func processItems(items ...interface{}) {
for _, item := range items {
switch value := item.(type) {
case Product:
fmt.Println("Product:", value.name, "Price:",
value.price)
case *Product:
fmt.Println("Product Pointer:", value.name,
"Price:", value.price)
case Service:

"Price:",

fmt.Println("Service:", value.description,
value.monthlyFee

*

float64(value.durationMonths))
case Person:
fmt.Println("Person:", value.name, "City:",
value.city)
case *Person:
fmt.Println("Person Pointer:", value.name,
"City:", value.city)
case string, bool, int:
fmt.Println("Built-in type:", value)
default:
fmt.Println("Default:", value)
}
}
}
func main() {
var expense Expense = &Product { "Kayak", "Watersports",
275 }
data := []interface{} {
expense,
Product { "Lifejacket", "Watersports", 48.95 },
Service {"Boat Cover", 12, 89.50, []string{} },
Person { "Alice", "London"},
&Person { "Bob", "New York"},
"This is a string",
100,
true,
}
}

processItems(data...)

Листинг 11-34 Использование переменных параметров в файле main.go в папке
methodAndInterfaces

Листинг 11-33 и Листинг 11-33 выдают следующий результат, когда
проект компилируется и выполняется:
Product Pointer: Kayak Price: 275

Product: Lifejacket
Service: Boat Cover
Person: Alice City:
Person Pointer: Bob
Built-in type: This
Built-in type: 100
Built-in type: true

Price: 48.95
Price: 1074
London
City: New York
is a string

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

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

Ответ

Кто они такие?

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

Почему они
полезны?

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

Как они
используются?

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

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

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

Таблица 12-2 суммирует главу.
Таблица 12-2 Краткое содержание главы
Проблема

Решение

Листинг

Определить пакет

Создайте папку и добавьте файлы кода с операторами
package.

4, 9, 10,
15, 16

Использовать пакет

Добавьте оператор import, указывающий путь к
пакету и включающему его модулю.

5

Управление доступом к
функциям в пакете

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

Проблема

Решение

Листинг

Устранение конфликтов
пакетов

Используйте псевдоним или точечный импорт.

11–14

Выполнение задач при
загрузке пакета

Определите функцию инициализации.

17, 18

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

Используйте пустой идентификатор в операторе
import.

19, 20

Использовать внешний
пакет

Используйте команду go get.

21, 22

Удалить неиспользуемые
зависимости пакетов

Используйте команду go mod tidy.

23

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем packages.
Перейдите в папку packages и выполните команду, показанную в
листинге 12-1, чтобы инициализировать проект.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init packages
Листинг 12-1 Инициализация проекта

Добавьте файл с именем main.go в папку packages с содержимым,
показанным в листинге 12-2.
package main
import "fmt"
func main() {
fmt.Println("Hello, Packages and Modules")

}
Листинг 12-2 Содержимое файла main.go в папке packages

Используйте командную строку для запуска команды, показанной в
листинге 12-3, в папке packages.
go run .
Листинг 12-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:
Hello, Packages and Modules

Понимание файла модуля
Первым шагом для всех примеров проектов в этой книге было создание
файла модуля, что было сделано с помощью команды в листинге 12-1.
Первоначальная цель файла модуля заключалась в том, чтобы
разрешить публикацию кода, чтобы его можно было использовать в
других проектах и, возможно, другими разработчиками. Файлы
модулей все еще используются для этой цели, но Go начал получать
широкое развитие, и, как это произошло, процент опубликованных
проектов упал. В наши дни наиболее распространенной причиной
создания файла модуля является то, что он упрощает установку
опубликованных пакетов и имеет дополнительный эффект, позволяя
использовать команду, показанную в листинге 12-3, вместо того, чтобы
предоставлять Go build tools со списком отдельных файлов для
компиляции.
Команда в листинге 12-1 создала файл с именем go.mod в папке
packages со следующим содержимым:
module packages
go 1.17
Оператор module указывает имя модуля, которое было указано
командой в листинге 12-1. Это имя важно, поскольку оно используется
для импорта функций из других пакетов, созданных в рамках того же

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

Создание пользовательского пакета
Пакеты позволяют добавить структуру в проект, чтобы связанные
функции были сгруппированы вместе. Создайте папку packages/store
и добавьте в нее файл с именем product.go, содержимое которого
показано в листинге 12-4.
package store
type Product struct {
Name, Category string
price float64
}
Листинг 12-4 Содержимое файла product.go в папке packages/store

Пользовательский пакет определяется с помощью ключевого слова
package, а указанный мной пакет называется store:
...
package store
...
Имя, указанное в операторе package, должно совпадать с именем
папки, в которой создаются файлы кода, которая в данном случае store.
Тип Product имеет несколько важных отличий от аналогичных
типов, определенных в предыдущих главах, как я объясню в
следующих разделах.
КОММЕНТАРИИ ЭКСПОРТИРУЕМЫХ ФУНКЦИЙ
Линтер Go сообщит об ошибке для любой функции,
экспортированной из пакета и не описанной в комментарии.
Комментарии должны быть простыми и описательными, и принято
начинать комментарий с названия функции, например:
...

// Product describes an item for sale
type Product struct {
Name, Category string // Name and type of the product
price float64
}
...
При комментировании пользовательских типов также можно
описать экспортированные поля. Go также поддерживает
комментарий, описывающий весь пакет, который появляется перед
ключевым словом package, например:
...
// Package store provides types and methods
// commonly required for online sales
package store
...
Эти комментарии обрабатываются инструментом go doc,
который создает документацию по коду. Я не добавлял комментарии
к примерам в этой книге для краткости, но комментирование кода
особенно важно при написании пакетов, которые используются
другими разработчиками.

Использование пользовательского пакета
Зависимости от пользовательских пакетов объявляются с помощью
оператора import, как показано в листинге 12-5.
package main
import (
"fmt"
"packages/store"
)
func main() {
product := store.Product {
Name: "Kayak",
Category: "Watersports",
}

fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)
}
Листинг 12-5 Использование пользовательского пакета в файле main.go в папке packages

Оператор import задает пакет в виде пути, состоящего из имени
модуля, созданного командой в листинге 12-1, и имени пакета,
разделенных косой чертой, как показано на рисунке 12-1.

Рисунок 12-1 Импорт пользовательского пакета

Доступ к экспортируемым функциям, предоставляемым пакетом,
осуществляется с использованием имени пакета в качестве префикса,
например:
...
var product *store.Product = &store.Product {
...
Чтобы указать тип Product, я должен указать префикс типа с
именем пакета, как показано на рисунке 12-2.

Рисунок 12-2 Использование имени пакета

Создайте и выполните проект, который выдаст следующий
результат:

Name: Kayak
Category: Watersports

Понимание управления доступом к пакетам
Тип Product, определенный в листинге 12-4, имеет важное отличие от
аналогичных типов, определенных в предыдущих главах: свойства Name
и Category начинаются с заглавной буквы.
В Go необычный подход к управлению доступом. Вместо того,
чтобы полагаться на специальные ключевые слова, такие как public и
private, Go проверяет первую букву имен, присвоенных функциям в
файле кода, таким как типы, функции и методы. Если первая буква
строчная, то функция может использоваться только в пакете, который ее
определяет. Функции экспортируются для использования вне пакета,
если им присваивается первая буква в верхнем регистре.
Имя типа структуры в листинге 12-4 — Product, что означает, что
этот тип можно использовать вне пакета store. Имена полей Name и
Category также начинаются с заглавной буквы, что означает, что они
также экспортируются. Поле price имеет первую строчную букву, что
означает, что доступ к нему возможен только внутри пакета store.
Рисунок 12-3 иллюстрирует эти различия.

Рисунок 12-3 Экспортированные и частные функции

Компилятор применяет правила экспорта пакета, а это означает, что
при доступе к полю price за пределами пакета store будет
сгенерирована ошибка, как показано в листинге 12-6.
package main

import (
"fmt"
"packages/store"
)
func main() {
product := store.Product {
Name: "Kayak",
Category: "Watersports",
price: 279,
}

}

fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)
fmt.Println("Price:", product.price)
Листинг 12-6 Доступ к неэкспортированному полю в файле main.go в папке packages

Первое изменение пытается установить значение для поля price
при использовании литерального синтаксиса для создания значения
Product. Второе изменение пытается прочитать значение поля price.
Правила контроля доступа применяются компилятором, который
сообщает о следующих ошибках при компиляции кода:
.\main.go:13:9: cannot refer to unexported field 'price' in
struct literal of type store.Product
.\main.go:18:34: product.price undefined (cannot refer to
unexported field or method price)
Чтобы устранить эти ошибки, я могу либо экспортировать поле
price, либо экспортировать методы или функции, обеспечивающие
доступ к значению поля. В листинге 12-7 определяется функцияконструктор для создания значений Product и методов для получения и
установки поля price.
package store
type Product struct {
Name, Category string
price float64

}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}

float64)

func (p *Product) Price() float64 {
return p.price
}
func (p *Product) SetPrice(newPrice float64)
p.price = newPrice
}

{

Листинг 12-7 Определение методов в файле product.go в папке store

Правила управления доступом не применяются к отдельным
параметрам функции или метода, а это означает, что функция
NewProduct должна иметь первый экспортируемый символ в верхнем
регистре, но имена параметров могут быть строчными.
Методы следуют типичному соглашению об именах для
экспортированных методов, обращающихся к полю, так что метод
Price возвращает значение поля, а метод SetPrice присваивает новое
значение. Листинг 12-8 обновляет код в файле main.go для
использования новых функций.
package main
import (
"fmt"
"packages/store"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)

}

fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)
fmt.Println("Price:", product.Price())

Листинг 12-8 Использование функций пакета в файле main.go в папке packages

Скомпилируйте и выполните проект с помощью команды из
листинга 12-8, и вы получите следующий вывод, демонстрирующий,
что код в main пакете может считывать поле price с помощью метода
Price:
Name: Kayak
Category: Watersports
Price: 279

Добавление файлов кода в пакеты
Пакеты могут содержать несколько файлов кода, и для упрощения
разработки правила управления доступом и префиксы пакетов не
применяются при доступе к функциям, определенным в одном пакете.
Добавьте файл с именем tax.go в папку store с содержимым,
показанным в листинге 12-9.
package store
const defaultTaxRate float64 = 0.2
const minThreshold = 10
type taxRate struct {
rate, threshold float64
}
func newTaxRate(rate, threshold float64) *taxRate {
if (rate == 0) {
rate = defaultTaxRate
}
if (threshold < minThreshold) {
threshold = minThreshold
}
return &taxRate { rate, threshold }
}
func (taxRate *taxRate) calcTax(product *Product) float64 {
if (product.price > taxRate.threshold) {
return product.price + (product.price * taxRate.rate)
}

return product.price
}
Листинг 12-9 Содержимое файла tax.go в папке store

Все функции, определенные в файле tax.go, не экспортируются,
что означает, что их можно использовать только в пакете store.
Обратите внимание, что метод calcTax может получить доступ к полю
price типа Product и делает это без обращения к типу как к
store.Product, поскольку он находится в том же пакете:
...
func (taxRate *taxRate) calcTax(product *Product) float64 {
if (product.price > taxRate.threshold) {
return product.price + (product.price * taxRate.rate)
}
return product.price
}
...
В листинге 12-10 я изменил метод Product.Price, чтобы он
возвращал значение поля price плюс налог.
package store
var standardTax = newTaxRate(0.25, 20)
type Product struct {
Name, Category string
price float64
}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}
func (p *Product) Price() float64 {
return standardTax.calcTax(p)
}
func (p *Product) SetPrice(newPrice float64)
p.price = newPrice

{

float64)

}
Листинг 12-10 Расчет налога в файле product.go в папке store

Метод Price может получить доступ к неэкспортированному
методу calcTax, но этот метод и тип, к которому он применяется,
доступны для использования только в пакете store. Скомпилируйте и
выполните код с помощью команды, показанной в листинге 12-10, и вы
получите следующий вывод:
Name: Kayak
Category: Watersports
Price: 348.75
КАК ИЗБЕЖАТЬ ЛОВУШКИ ПЕРЕОПРЕДЕЛЕНИЯ
Распространенной ошибкой является повторное использование имен
в разных файлах в одном пакете. Это то, что я делаю часто, в том
числе при написании примера, показанного в листинге 12-10. Моя
первоначальная версия кода в файле product.go содержала
следующее утверждение:
...
var taxRate = newTaxRate(0.25, 20)
...
Это вызывает ошибку компилятора, поскольку файл tax.go
определяет тип структуры с именем taxRate. Компилятор не делает
различий между именами, присвоенными переменным, и именами,
присвоенными типам, и сообщает об ошибке, например:
store\tax.go:6:6: taxRate redeclared in this block
previous declaration at store\product.go:3:5
Вы также можете увидеть ошибки в редакторе кода, говорящие о
том, что taxRate является недопустимым типом. Это одна и та же
проблема, выраженная по-разному. Чтобы избежать этих ошибок, вы
должны убедиться, что функции верхнего уровня, определенные в
пакете, имеют уникальные имена. Имена не обязательно должны
быть уникальными в пакетах или внутри функций и методов.

Разрешение конфликтов имен пакетов
Когда пакет импортируется, комбинация имени модуля и имени пакета
обеспечивает уникальную идентификацию пакета. Но при доступе к
функциям, предоставляемым пакетом, используется только имя пакета,
что может привести к конфликтам. Чтобы увидеть, как возникает эта
проблема, создайте папку packages/fmt и добавьте в нее файл с
именем formats.go с кодом, показанным в листинге 12-11.
package fmt
import "strconv"
func ToCurrency(amount float64) string {
return "$" + strconv.FormatFloat(amount, 'f', 2, 64)
}
Листинг 12-11 Содержимое файла formats.go в папке fmt

Этот файл экспортирует функцию с именем ToCurrency, которая
получает значение float64 и создает отформатированную сумму в
долларах с помощью функции strconv.FormatFloat, описанной в
главе 17.
Пакет fmt, определенный в листинге 12-11, имеет то же имя, что и
один из наиболее широко используемых пакетов стандартных
библиотек. Это вызывает проблему при использовании обоих пакетов,
как показано в листинге 12-12.
package main
import (
"fmt"
"packages/store"
"packages/fmt"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)

}

fmt.Println("Price:", fmt.ToCurrency(product.Price()))
Листинг 12-12 Использование пакетов с тем же именем в файле main.go в папке packages

Скомпилируйте проект, и вы получите следующие ошибки:
.\main.go:6:5: fmt redeclared as imported package name
previous declaration at .\main.go:4:5
.\main.go:13:5: undefined: "packages/fmt".Println
.\main.go:14:5: undefined: "packages/fmt".Println
.\main.go:15:5: undefined: "packages/fmt".Println

Использование псевдонима пакета
Одним из способов решения конфликтов имен пакетов является
использование псевдонима, который позволяет получить доступ к
пакету с использованием другого имени, как показано в листинге 12-13.
package main
import (
"fmt"
"packages/store"
currencyFmt "packages/fmt"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)
fmt.Println("Price:",
currencyFmt.ToCurrency(product.Price()))
}
Листинг 12-13 Использование псевдонима пакета в файле main.go в папке packages

Псевдоним, под которым будет известен пакет, объявляется перед
путем импорта, как показано на рисунке 12-4.

Рисунок 12-4 Псевдоним пакета

Псевдоним в этом примере разрешает конфликт имен, поэтому к
функциям, определенным пакетом, импортированным с помощью пути
packages/fmt, можно получить доступ с использованием currencyFmt
в качестве префикса, например:
...
fmt.Println("Price:",
currencyFmt.ToCurrency(product.Price()))
...
Скомпилируйте и выполните проект, и вы получите следующий
вывод, основанный на функциях, определенных пакетом fmt в
стандартной библиотеке, и пользовательским пакетом fmt, которому
присвоен псевдоним:
Name: Kayak
Category: Watersports
Price: $348.75

Использование точечного импорта
Существует специальный псевдоним, известный как точечный импорт,
который позволяет использовать функции пакета без использования
префикса, как показано в листинге 12-14.
package main
import (
"fmt"
"packages/store"

. "packages/fmt"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)

}

fmt.Println("Name:", product.Name)
fmt.Println("Category:", product.Category)
fmt.Println("Price:", ToCurrency(product.Price()))

Листинг 12-14 Использование точечного импорта в файле main.go в папке packages

Точечный импорт использует точку в качестве псевдонима пакета,
как показано на рисунке 12-5.

Рисунок 12-5 Использование точечного импорта

Точечный импорт позволяет мне получить доступ к функции
ToCurrency без использования префикса, например:
...
fmt.Println("Price:", ToCurrency(product.Price()))
...
При использовании точечного импорта вы должны убедиться, что
имена функций, импортированных из пакета, не определены в
импортирующем пакете. Например, это означает, что я должен
убедиться, что имя ToCurrency не используется какой-либо функцией,
определенной в main пакете. По этой причине точечный импорт следует
использовать с осторожностью.

Создание вложенных пакетов
Пакеты могут быть определены внутри других пакетов, что упрощает
разбиение сложных функций на максимально возможное количество
модулей. Создайте папку packages/store/cart и добавьте в нее файл с
именем cart.go с содержимым, показанным в листинге 12-15.
package cart
import "packages/store"
type Cart struct {
CustomerName string
Products []store.Product
}
func (cart *Cart) GetTotal() (total float64) {
for _, p := range cart.Products {
total += p.Price()
}
return
}
Листинг 12-15 Содержимое файла cart.go в папке store/cart

Оператор package используется так же, как и любой другой пакет,
без необходимости включать имя родительского или включающего
пакета. И зависимость от пользовательских пакетов должна включать
полный путь к пакету, как показано в листинге. Код в листинге 12-15
определяет тип структуры с именем Cart, который экспортирует поля
CustomerName и Products, а также метод GetTotal.
При импорте вложенного пакета путь к пакету начинается с имени
модуля и перечисляет последовательность пакетов, как показано в
листинге 12-16.
package main
import (
"fmt"
"packages/store"
. "packages/fmt"
"packages/store/cart"

)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
cart := cart.Cart {
CustomerName: "Alice",
Products: []store.Product{ *product },
}

}

fmt.Println("Name:", cart.CustomerName)
fmt.Println("Total:", ToCurrency(cart.GetTotal()))
Листинг 12-16 Использование вложенного пакета в файле main.go в папке packages

Доступ к функциям, определенным вложенным пакетом,
осуществляется по имени пакета, как и к любому другому пакету. В
листинге 12-16 это означает, что доступ к типу и функции,
экспортируемым
пакетом
store/cart,
осуществляется
с
использованием cart в качестве префикса. Скомпилируйте и запустите
проект, и вы получите следующий вывод:
Name: Alice
Total: $348.75

Использование функций инициализации пакета
Каждый файл кода может содержать функцию инициализации, которая
выполняется только после загрузки всех пакетов и выполнения всех
остальных инициализаций, таких как определение констант и
переменных. Чаще всего функции инициализации используются для
выполнения вычислений, которые сложно выполнить или для
выполнения которых требуется дублирование, как показано в листинге
12-17.
package store
const defaultTaxRate float64 = 0.2
const minThreshold = 10

var categoryMaxPrices = map[string]float64 {
"Watersports": 250 + (250 * defaultTaxRate),
"Soccer": 150 + (150 * defaultTaxRate),
"Chess": 50 + (50 * defaultTaxRate),
}
type taxRate struct {
rate, threshold float64
}
func newTaxRate(rate, threshold float64) *taxRate {
if (rate == 0) {
rate = defaultTaxRate
}
if (threshold < minThreshold) {
threshold = minThreshold
}
return &taxRate { rate, threshold }
}
func (taxRate *taxRate) calcTax(product *Product) (price
float64) {
if (product.price > taxRate.threshold) {
price = product.price + (product.price *
taxRate.rate)
} else {
price = product.price
}
if max, ok := categoryMaxPrices[product.Category]; ok &&
price > max {
price = max
}
return
}
Листинг 12-17 Расчет максимальных цен в файле tax.go в папкеstore

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

Эту проблему можно легко решить с помощью цикла for, но Go
допускает циклы только внутри функций, и мне нужно выполнять эти
вычисления на верхнем уровне файла кода.
Решение состоит в использовании функции инициализации, которая
вызывается автоматически при загрузке пакета и в которой можно
использовать языковые функции, такие как циклы for, как показано в
листинге 12-18.
package store
const defaultTaxRate float64 = 0.2
const minThreshold = 10
var categoryMaxPrices = map[string]float64 {
"Watersports": 250,
"Soccer": 150,
"Chess": 50,
}
func init() {
for category, price := range categoryMaxPrices {
categoryMaxPrices[category] = price + (price *
defaultTaxRate)
}
}
type taxRate struct {
rate, threshold float64
}
func newTaxRate(rate, threshold float64) *taxRate {
// ...statements omitted for brevity...
}
func (taxRate *taxRate) calcTax(product *Product)
float64) {
// ...statements omitted for brevity...
}

(price

Листинг 12-18 Использование функции инициализации в файле tax.go в папке store

Функция инициализации называется init и определяется без
параметров и результата. Функция init вызывается автоматически и
предоставляет возможность подготовить пакет к использованию. Оба
листинга 12-17 и 12-18 при компиляции и выполнении выдают
следующий результат:
Name: Kayak
Price: $300.00
Функция init не является обычной функцией Go и не может быть
вызвана напрямую. И, в отличие от обычных функций, в одном файле
может быть определено несколько функций init, и все они будут
выполняться.
ИЗБЕЖАНИЕ ЛОВУШКИ ФУНКЦИЙ МНОЖЕСТВЕННОЙ
ИНИЦИАЛИЗАЦИИ
Каждый файл кода может иметь свою собственную функцию
инициализации. При использовании стандартного компилятора Go
функции инициализации выполняются на основе алфавитного
порядка имен файлов, поэтому функция в файле a.go будет
выполняться перед функцией в файле b.go и так далее.
Но этот порядок не является частью спецификации языка Go, и
на него не следует полагаться. Ваши функции инициализации
должны быть автономными и не зависеть от других функций init,
которые были вызваны ранее.

Импорт пакета только для эффектов инициализации
Go предотвращает импорт пакетов, но не их использование, что может
быть проблемой, если вы полагаетесь на эффект функции
инициализации, но вам не нужно использовать какие-либо функции,
экспортируемые пакетом. Создайте папку packages/data и добавьте в
нее файл с именем data.go с содержимым, показанным в листинге 1219.
package data
import "fmt"

func init() {
fmt.Println(("data.go init function invoked"))
}
func GetData() []string {
return []string {"Kayak", "Lifejacket", "Paddle", "Soccer
Ball"}
}
Листинг 12-19 Содержимое файла data.go в папке data

Функция инициализации записывает сообщение, когда она
вызывается для целей этого примера. Если мне нужен эффект функции
инициализации, но мне не нужно использовать функцию GetData,
которую экспортирует пакет, я могу импортировать пакет, используя
пустой идентификатор в качестве псевдонима для имени пакета, как
показано в листинге 12-20.
package main
import (
"fmt"
"packages/store"
. "packages/fmt"
"packages/store/cart"
_ "packages/data"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
cart := cart.Cart {
CustomerName: "Alice",
Products: []store.Product{ *product },
}
fmt.Println("Name:", cart.CustomerName)
fmt.Println("Total:", ToCurrency(cart.GetTotal()))
}
Листинг 12-20 Импорт для инициализации в файл main.go в папке packages

Пустой идентификатор — символ подчеркивания — позволяет
импортировать пакет, не требуя использования его экспортированных
функций. Скомпилируйте и запустите проект, и вы увидите сообщение,
написанное функцией инициализации, определенной в листинге 12-19:
data.go init function invoked
Name: Alice
Total: $300.00

Использование внешних пакетов
Проекты могут быть расширены с использованием пакетов,
разработанных третьими сторонами. Пакеты загружаются и
устанавливаются с помощью команды go get. Запустите команду,
показанную в листинге 12-21, в папке packages, чтобы добавить пакет в
пример проекта.
go get github.com/fatih/color@v1.10.0
Листинг 12-21 Установка пакета

Аргументом команды go get является путь к модулю, содержащему
пакет, который вы хотите использовать. За именем следует символ @, а
затем номер версии пакета, перед которым стоит буква v, как показано
на рисунке 12-6.

Рисунок 12-6 Выбор пакета

Команда go get является сложной и знает, что путь, указанный в
листинге 12-21, является URL-адресом GitHub. Загружается указанная
версия модуля, а содержащиеся в нем пакеты компилируются и
устанавливаются, чтобы их можно было использовать в проекте.
(Пакеты распространяются в виде исходного кода, что позволяет
компилировать их для платформы, на которой вы работаете.)

ПОИСК ПАКЕТОВ GO
Есть два полезных ресурса для поиска пакетов Go. Первый — это
https://pkg.go.dev, который предоставляет поисковую систему. К
сожалению, может потребоваться некоторое время, чтобы выяснить,
какие ключевые слова необходимы для поиска определенного типа
пакета.
Второй
ресурс

https://github.com/golang/go/wiki/Projects,
который
предоставляет кураторский список проектов Go, сгруппированных
по категориям. Не все проекты, перечисленные на pkg.go.dev, есть
в списке, и я предпочитаю использовать оба ресурса для поиска
пакетов.
При выборе модулей следует соблюдать осторожность. Многие
модули Go пишутся отдельными разработчиками для решения
проблемы, а затем публикуются для использования кем-либо еще.
Это создает богатую модульную экосистему, но это означает, что
обслуживание и поддержка могут быть непоследовательными.
Например, модуль github.com/fatih/color, который я использую в
этом разделе, устарел и больше не получает обновлений. Я рад
продолжать использовать его, так как мое применение в этой главе
простое, а код работает хорошо. Вы должны выполнить такую же
оценку для модулей, на которые вы полагаетесь в своих проектах.
Изучите файл go.mod после завершения команды go get, и вы увидите
новые операторы конфигурации:
module packages
go 1.17
require (
github.com/fatih/color v1.10.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae //
indirect
)
Оператор
require
github.com/fatih/color

отмечает
и других

зависимость
необходимых

от
модуля
ему модулей.

Комментарий indirect в конце операторов добавляется автоматически,
поскольку пакеты не используются кодом в проекте. Файл с именем
go.sum создается при получении модуля и содержит контрольные
суммы, используемые для проверки пакетов.
Примечание
Вы также можете использовать файл go.mod для создания
зависимостей от проектов, которые вы создали локально, и именно
этот подход я использую в третьей части для примера SportsStore.
Подробности см. в главе 35.
После установки модуля содержащиеся в нем пакеты можно
использовать в проекте, как показано в листинге 12-22.
package main
import (
//"fmt"
"packages/store"
. "packages/fmt"
"packages/store/cart"
_ "packages/data"
"github.com/fatih/color"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
cart := cart.Cart {
CustomerName: "Alice",
Products: []store.Product{ *product },
}

}

color.Green("Name: " + cart.CustomerName)
color.Cyan("Total: " + ToCurrency(cart.GetTotal()))

Листинг 12-22 Использование стороннего пакета в файле main.go в папке packages

Внешние
пакеты
импортируются
и
используются
как
пользовательские пакеты. Оператор import указывает путь к модулю, и
последняя часть этого пути используется для доступа к функциям,
экспортируемым пакетом. В этом случае пакет называется color, и это
префикс, используемый для доступа к функциям пакета.
Функции Green и Cyan, используемые в листинге 12-22, записывают
цветной вывод, и если вы скомпилируете и запустите проект, вы
увидите вывод, показанный на рисунке 12-7.

Рисунок 12-7 Запуск примера приложения

ПОНИМАНИЕ ВЫБОРА МИНИМАЛЬНОЙ ВЕРСИИ
При первом запуске команды go get в листинге 12-22 вы увидите
список загруженных модулей, который иллюстрирует, что модули
имеют свои собственные зависимости и что они разрешаются
автоматически:
go: downloading github.com/fatih/color v1.10.0
go: downloading github.com/mattn/go-isatty v0.0.12
go: downloading github.com/mattn/go-colorable v0.1.8
go: downloading golang.org/x/sys v0.0.0-20200223170610d5e6a3e2c0ae
Загрузки кэшируются, поэтому вы не увидите сообщения при
следующем использовании команды go get для того же модуля.
Вы можете обнаружить, что ваш проект зависит от разных
версий модуля, особенно в сложных проектах с большим

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

Управление внешними пакетами
Команда go get добавляет зависимости в файл go.mod, но они не
удаляются автоматически, если внешний пакет больше не требуется. В
листинге 12-23 изменено содержимое файла main.go, чтобы исключить
использование пакета github.com/fatih/color.
package main
import (
"fmt"
"packages/store"
. "packages/fmt"
"packages/store/cart"
_ "packages/data"
//"github.com/fatih/color"
)
func main() {
product := store.NewProduct("Kayak", "Watersports", 279)
cart := cart.Cart {
CustomerName: "Alice",

}

}

Products: []store.Product{ *product },

// color.Green("Name: " + cart.CustomerName)
// color.Cyan("Total: " + ToCurrency(cart.GetTotal()))
fmt.Println("Name:", cart.CustomerName)
fmt.Println("Total:", ToCurrency(cart.GetTotal()))

Листинг 12-23 Удаление пакета в файле main.go в папке packages

Чтобы обновить файл go.mod, чтобы отразить изменения, запустите
команду, показанную в листинге 12-24, в папке packages.
go mod tidy
Листинг 12-24 Обновление зависимостей пакетов

Команда проверяет код проекта, определяет, что больше нет
зависимости
ни
от
одного
из
пакетов
от
модуля
github.com/fatih/color, и удаляет оператор require из файла go.mod:
module packages
go 1.17

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

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

Ответ

Что это?

Композиция — это процесс создания новых типов путем
объединения структур и интерфейсов.

Почему это полезно?

Композиция позволяет определять типы на основе существующих
типов.

Как это используется?

Существующие типы встраиваются в новые типы.

Есть ли подводные
камни или
ограничения?

Композиция работает не так, как наследование, и для достижения
желаемого результата необходимо соблюдать осторожность.

Есть ли альтернативы?

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

Таблица 13-2 суммирует главу.
Таблица 13-2 Краткое содержание главы
Проблема

Решение

Листинг

Составление типа
структуры

Добавить встроенное поле

7-9, 14–
17

Построить на уже
составленном типе

Создайте цепочку встроенных типов

10–13

Составьте тип интерфейса Добавьте имя существующего интерфейса в
определение нового интерфейса.

25–26

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем composition.
Запустите команду, показанную в листинге 13-1, в папке composition,
чтобы создать файл модуля.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init composition
Листинг 13-1 Инициализация модуля

Добавьте файл с именем main.go в папку composition с
содержимым, показанным в листинге 13-2.
package main
import "fmt"
func main() {
}

fmt.Println("Hello, Composition")
Листинг 13-2 Содержимое файла main.go в папке composition

Используйте командную строку для запуска команды, показанной в
листинге 13-3, в папке composition.
go run .
Листинг 13-3 Запуск примера проекта

Код в файле main.go будет скомпилирован и выполнен, что
приведет к следующему результату:

Hello, Composition

Понимание композиции типов
Если вы привыкли к таким языкам, как C# или Java, то вы создали
базовый класс и создали подклассы для добавления более
специфических функций. Подклассы наследуют функциональные
возможности базового класса, что предотвращает дублирование кода.
Результатом является набор классов, где базовый класс определяет
общую функциональность, которая дополняется более специфическими
функциями в отдельных подклассах, как показано на рисунке 13-1.

Рисунок 13-1 Набор классов

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

Определение базового типа
Отправной точкой является определение типа структуры и метода,
которые я буду использовать для создания более конкретных типов в
последующих примерах. Создайте папку composition/store и
добавьте в нее файл с именем product.go с содержимым, показанным в
листинге 13-4.
package store
type Product struct {
Name, Category string
price float64
}
func (p *Product) Price(taxRate float64) float64 {
return p.price + (p.price * taxRate)
}
Листинг 13-4 Содержимое файла product.go в папке store

Структура Product определяет поля Name и Category, которые
экспортируются, и поле price, которое не экспортируется. Существует
также метод Price, который принимает параметр float64 и использует
его с полем цены для расчета price с учетом налогов.

Определение конструктора
Поскольку Go не поддерживает классы, он также не поддерживает
конструкторы классов. Как я объяснил, общепринятым соглашением
является определение функции-конструктора с именем New,
такой как NewProduct, как показано в листинге 13-5, и которая
позволяет предоставлять значения для всех полей, даже для тех,
которые не были экспортируется. Как и в случае с другими функциями
кода, использование заглавной буквы в имени функции-конструктора
определяет, экспортируется ли оно за пределы пакета.
package store
type Product struct {
Name, Category string
price float64
}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}

float64)

func (p *Product) Price(taxRate float64) float64 {
return p.price + (p.price * taxRate)
}
Листинг 13-5 Определение конструктора в файле product.go в папке store

Функции-конструкторы являются лишь соглашением, и их
использование не является принудительным, что означает, что
экспортированные типы могут быть созданы с использованием
литерального синтаксиса, если неэкспортируемым полям не присвоены
значения. В листинге 13-6 показано использование функцииконструктора и литерального синтаксиса.
package main

import (
"fmt"
"composition/store"
)
func main() {
kayak := store.NewProduct("Kayak", "Watersports", 275)
lifejacket := &store.Product{ Name: "Lifejacket",
Category: "Watersports"}
for _, p := range []*store.Product { kayak, lifejacket} {
fmt.Println("Name:", p.Name, "Category:", p.Category,
"Price:", p.Price(0.2))
}
}
Листинг 13-6 Создание структурных значений в файле main.go в папке composition

Конструкторы следует использовать всякий раз, когда они
определены, поскольку они облегчают управление изменениями в
способе
создания
значений
и
обеспечивают
правильную
инициализацию полей. В листинге 13-6 использование литерального
синтаксиса означает, что полю price не присваивается значение, что
влияет на выходные данные метода Price. Но поскольку Go не
поддерживает принудительное использование конструкторов, их
использование требует дисциплины.
Скомпилируйте и запустите проект, и вы получите следующий
вывод:
Name: Kayak Category: Watersports Price: 330
Name: Lifejacket Category: Watersports Price: 0

Типы композиций
Go поддерживает композицию, а не наследование, которое достигается
путем объединения типов структур. Добавьте файл с именем boat.go в
папку store с содержимым, показанным в листинге 13-7.
package store

type Boat struct {
*Product
Capacity int
Motorized bool
}
func NewBoat(name string, price float64, capacity int,
motorized bool) *Boat {
return &Boat {
NewProduct(name, "Watersports", price), capacity,
motorized,
}
}
Листинг 13-7 Содержимое файла boat.go в папке store

Тип структуры Boat определяет встроенное поле *Product, как
показано на рисунке 13-2.

Рисунок 13-2 Встраивание типа

Структура может смешивать обычные и встроенные типы полей, но
встроенные поля являются важной частью функции композиции, как вы
скоро увидите.
Функция NewBoat — это конструктор, который использует свои
параметры для создания Boat со встроенным значением Product. В
листинге 13-8 показано использование новой структуры.
package main
import (

"fmt"
"composition/store"
)
func main() {
boats := []*store.Boat {
store.NewBoat("Kayak", 275, 1, false),
store.NewBoat("Canoe", 400, 3, false),
store.NewBoat("Tender", 650.25, 2, true),
}
for _, b := range boats {
fmt.Println("Conventional:",
"Direct:", b.Name)
}
}

b.Product.Name,

Листинг 13-8 Использование структуры лодки в файле main.go в папке composition

Новые операторы создают срез Boat *Boat, который заполняется с
помощью функции-конструктора NewBoat.
Go уделяет особое внимание типам структур, которые имеют поля,
тип которых является другим типом структуры, таким же образом, как
тип Boat имеет поле *Product в примере проекта. Вы можете увидеть
эту специальную обработку в операторе цикла for, который отвечает за
запись сведений о каждой Boat.
Go позволяет получить доступ к полям вложенного типа двумя
способами. Первый — это традиционный подход к навигации по
иерархии типов для достижения требуемого значения. Поле *Product
является встроенным, что означает, что его имя соответствует его типу.
Чтобы добраться до поля Name, я могу перемещаться по вложенному
типу, например так:
...
fmt.Println("Conventional:",
b.Name)
...

b.Product.Name,

"Direct:",

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

...
fmt.Println("Conventional:",
b.Name)
...

b.Product.Name,

"Direct:",

Тип Boat не определяет поле Name, но его можно рассматривать так,
как если бы оно было определено, благодаря функции прямого доступа.
Это известно как продвижение полей, и Go по существу выравнивает
типы, так что тип Boat ведет себя так, как будто он определяет поля,
предоставляемые вложенным типом Product, как показано на рисунке
13-3.

Рисунок 13-3 Продвигаемые поля

Скомпилируйте и выполните проект, и вы увидите, что значения,
получаемые обоими подходами, одинаковы:
Conventional: Kayak Direct: Kayak
Conventional: Canoe Direct: Canoe
Conventional: Tender Direct: Tender
Также продвигаются методы, так что методы, определенные для
вложенного типа, могут быть вызваны из включающего типа, как
показано в листинге 13-9.
package main
import (
"fmt"
"composition/store"
)

func main() {
boats := []*store.Boat {
store.NewBoat("Kayak", 275, 1, false),
store.NewBoat("Canoe", 400, 3, false),
store.NewBoat("Tender", 650.25, 2, true),
}

}

for _, b := range boats {
fmt.Println("Boat:", b.Name, "Price:", b.Price(0.2))
}
Листинг 13-9 Вызов метода в файле main.go в папке composition

Если тип поля является значением, например Product, то будут
продвинуты любые методы, определенные с приемниками Product или
*Product. Если тип поля является указателем, например *Product, то
будут запрашиваться только методы с приемниками *Product.
Для типа *Boat не определен метод Price, но Go продвигает метод,
определенный с помощью приемника *Product. Скомпилируйте и
запустите проект, и вы получите следующий вывод:
Boat: Kayak Price: 330
Boat: Canoe Price: 480
Boat: Tender Price: 780.3
ПОНИМАНИЕ ПРОДВИГАЕМЫХ ПОЛЕЙ И
ЛИТЕРАЛЬНОГО СИНТАКСИСА
Go применяет специальную обработку к продвинутым полям после
создания значения структуры. Так, например, если я использую
функцию NewBoat для создания такого значения:
...
boat := store.NewBoat("Kayak", 275, 1, false)
...
затем я могу читать и назначать значения продвигаемым полям,
например:
...

boat.Name = "Green Kayak"
...
Но эта функция недоступна при использовании литерального
синтаксиса для создания значений в первую очередь, а это означает,
что я не могу заменить функцию NewBoat, например:
...
boat
:=
store.Boat
{
Name:
"Watersports",
Capacity: 1, Motorized: false }
...

"Kayak",

Category:

Компилятор не позволяет присваивать значения напрямую и
сообщает об ошибке «неизвестное поле» при компиляции кода. Если
вы используете литеральный синтаксис, вы должны присвоить
значение вложенному полю, например:
...
boat := store.Boat { Product: &store.Product{ Name:
"Kayak",
Category: "Watersports"}, Capacity: 1, Motorized: false
}
...
Как я объяснял в разделе «Создание цепочки вложенных типов»,
Go упрощает использование функции композиции для создания
сложных типов, что делает литеральный синтаксис все более
сложным в использовании и создает код, подверженный ошибкам и
сложный в обслуживании. Я советую использовать функцииконструкторы и вызывать один конструктор из другого, как функция
NewBoat вызывает функцию NewProduct в листинге 13-7.

Создание цепочки вложенных типов
Функцию композиции можно использовать для создания сложных
цепочек вложенных типов, поля и методы которых повышаются до
включающего типа верхнего уровня. Добавьте файл с именем
rentboats.go в папку store с содержимым, показанным в листинге 1310.

package store
type RentalBoat struct {
*Boat
IncludeCrew bool
}
func NewRentalBoat(name string, price float64, capacity int,
motorized, crewed bool) *RentalBoat {
return &RentalBoat{NewBoat(name, price, capacity,
motorized), crewed}
}
Листинг 13-10 Содержимое файла Rentalboats.go в папке store

Тип RentalBoat составлен из типа *Boat, который, в свою очередь,
составлен из типа *Product, образуя цепочку. Go выполняет
продвижение, так что к полям, определенным всеми тремя типами в
цепочке, можно получить прямой доступ, как показано в листинге 1311.
package main
import (
"fmt"
"composition/store"
)
func main() {
rentals := []*store.RentalBoat {
store.NewRentalBoat("Rubber Ring", 10, 1, false,
false),
store.NewRentalBoat("Yacht", 50000, 5, true, true),
store.NewRentalBoat("Super Yacht", 100000, 15, true,
true),
}
for _, r := range rentals {
fmt.Println("Rental Boat:", r.Name, "Rental Price:",
r.Price(0.2))
}
}

Листинг 13-11 Доступ к вложенным полям непосредственно в файле main.go в папке
composition

Go продвигает поля из вложенных типов Boat и Product, чтобы к
ним можно было получить доступ через тип RentalBoat верхнего
уровня, который позволяет читать поле Name в листинге 13-11. Методы
также повышаются до типа верхнего уровня, поэтому я могу
использовать метод Price, даже если он определен для типа *Product,
который находится в конце цепочки. Код в листинге 13-11 выдает
следующий результат при компиляции и выполнении:
Rental Boat: Rubber Ring Rental Price: 12
Rental Boat: Yacht Rental Price: 60000
Rental Boat: Super Yacht Rental Price: 120000

Использование нескольких вложенных типов в одной и той
же структуре
Типы могут определять несколько полей структуры, и Go будет
продвигать поля для всех из них. В листинге 13-12 определяется новый
тип, описывающий экипаж лодки и использующий его в качестве типа
для поля в другой структуре.
package store
type Crew struct {
Captain, FirstOfficer string
}
type RentalBoat struct {
*Boat
IncludeCrew bool
*Crew
}
func NewRentalBoat(name string, price float64, capacity int,
motorized, crewed bool, captain, firstOfficer string)
*RentalBoat {
return &RentalBoat{NewBoat(name, price, capacity,
motorized), crewed,
&Crew{captain, firstOfficer}}
}

Листинг 13-12 Определение нового типа в файле Rentalboats.go в папке store

Тип RentalBoat имеет поля *Boat и *Crew, а Go продвигает поля и
методы из обоих вложенных типов, как показано в листинге 13-13.
package main
import (
"fmt"
"composition/store"
)
func main() {
rentals := []*store.RentalBoat {
store.NewRentalBoat("Rubber Ring", 10, 1, false,
false, "N/A", "N/A"),
store.NewRentalBoat("Yacht", 50000, 5, true, true,
"Bob", "Alice"),
store.NewRentalBoat("Super Yacht", 100000, 15, true,
true,
"Dora", "Charlie"),
}
for _, r := range rentals {
fmt.Println("Rental Boat:", r.Name, "Rental Price:",
r.Price(0.2),
"Captain:", r.Captain)
}
}
Листинг 13-13 Использование продвигаемых полей в файле main.go в папке composition

Скомпилируйте и выполните проект, и вы получите следующий
вывод, показывающий добавление сведений о команде:
Rental Boat: Rubber Ring Rental Price: 12 Captain: N/A
Rental Boat: Yacht Rental Price: 60000 Captain: Bob
Rental Boat: Super Yacht Rental Price: 120000 Captain: Dora

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

именем, что может привести к неожиданным результатам. Добавьте
файл с именем specialdeal.go в папку store с кодом, показанным в
листинге 13-14.
package store
type SpecialDeal struct {
Name string
*Product
price float64
}
func NewSpecialDeal(name string, p *Product, discount
float64) *SpecialDeal {
return &SpecialDeal{ name, p, p.price - discount }
}
func (deal *SpecialDeal ) GetDetails() (string, float64,
float64) {
return deal.Name, deal.price, deal.Price(0)
}
Листинг 13-14 Содержимое файла specialdeal.go в папке store

Тип SpecialDeal определяет встроенное поле *Product. Эта
комбинация приводит к дублированию полей, поскольку оба типа
определяют поля Name и price. Существует также функцияконструктор и метод GetDetails, который возвращает значения полей
Name и price, а также результат метода Price, который вызывается с
нулем в качестве аргумента, чтобы упростить следование примеру. В
листинге 13-15 новый тип используется для демонстрации того, как
обрабатывается продвижение.
package main
import (
"fmt"
"composition/store"
)
func main() {

product := store.NewProduct("Kayak", "Watersports", 279)
50)

deal := store.NewSpecialDeal("Weekend Special", product,
Name, price, Price := deal.GetDetails()

}

fmt.Println("Name:", Name)
fmt.Println("Price field:", price)
fmt.Println("Price method:", Price)
Листинг 13-15 Использование нового типа в файле main.go в папке composition

Этот листинг создает *Product, который затем используется для
создания *SpecialDeal.
Вызывается метод GetDetails, и
записываются три возвращаемых им результата. Скомпилируйте и
запустите код, и вы увидите следующий вывод:
Name: Weekend Special
Price field: 229
Price method: 279
Первые два результата вполне ожидаемы: поля Name и price из типа
Product не продвигаются, поскольку в типе SpecialDeal есть поля с
одинаковыми именами.
Третий результат может вызвать проблемы. Go может продвигать
метод Price, но когда он вызывается, он использует поле price из
Product, а не из SpecialDeal.
Легко забыть, что продвижение полей и методов — это просто
функция удобства. Этот оператор в листинге 13-14:
...
return deal.Name, deal.price, deal.Price(0)
...
это более краткий способ выразить это утверждение:
...
return deal.Name, deal.price, deal.Product.Price(0)
...

Когда метод вызывается через его поле структуры, становится ясно,
что результат вызова метода Price не будет использовать поле price,
определенное типом SpecialDeal.
Если я хочу иметь возможность вызвать метод Price и получить
результат, основанный на поле SpecialDeal.price, я должен
определить новый метод, как показано в листинге 13-16.
package store
type SpecialDeal struct {
Name string
*Product
price float64
}
func NewSpecialDeal(name string, p *Product, discount
float64) *SpecialDeal {
return &SpecialDeal{ name, p, p.price - discount }
}
func (deal *SpecialDeal ) GetDetails() (string, float64,
float64) {
return deal.Name, deal.price, deal.Price(0)
}
func (deal *SpecialDeal) Price(taxRate float64) float64 {
return deal.price
}
Листинг 13-16 Определение метода в файле specialdeal.go в папке store

Новый метод Price не позволяет Go продвигать метод Product и
выдает следующий результат при компиляции и выполнении проекта:
Name: Weekend Special
Price field: 229
Price method: 229

Понимание неоднозначности продвижения
Связанная проблема возникает, когда два встроенных поля используют
одни и те же имена полей или методов, как показано в листинге 13-17.

package main
import (
"fmt"
"composition/store"
)
func main() {
kayak := store.NewProduct("Kayak", "Watersports", 279)
type OfferBundle struct {
*store.SpecialDeal
*store.Product
}
bundle := OfferBundle {
store.NewSpecialDeal("Weekend Special", kayak, 50),
store.NewProduct("Lifrejacket", "Watersports",
48.95),
}
}

fmt.Println("Price:", bundle.Price(0))

Листинг 13-17 Неоднозначный метод в файле main.go в папке composition

Тип OfferBundle имеет два встроенных поля, каждое из которых
имеет метод Price. Go не может различать методы, и код в листинге 1317 выдает следующую ошибку при компиляции:
.\main.go:22:33: ambiguous selector bundle.Price

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

требуются типы, из которых он составлен, как показано в листинге 1318.
package main
import (
"fmt"
"composition/store"
)
func main() {
products := map[string]*store.Product {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for _, p := range products {
fmt.Println("Name:", p.Name, "Category:", p.Category,
"Price:", p.Price(0.2))
}
}
Листинг 13-18 Смешивание типов в файле main.go в папке composition

Компилятор Go не позволит использовать Boat в качестве значения
в срезе, где требуются значения Product. В таких языках, как C# или
Java, это было бы разрешено, потому что Boat был бы подклассом
Product, но Go не так работает с типами. Если вы скомпилируете
проект, вы получите следующую ошибку:
.\main.go:11:9: cannot use store.NewBoat("Kayak", 279, 1,
false) (type *store.Boat) as type *store.Product in map value

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

встроенном поле. Чтобы увидеть, как это работает, добавьте файл с
именем forsale.go в папку store с содержимым, показанным в
листинге 13-19.
package store
type ItemForSale interface {
Price(taxRate float64) float64
}
Листинг 13-19 Содержимое файла forsale.go в папке store

Тип ItemForSale — это интерфейс, определяющий единственный
метод с именем Price, с одним параметром float64 и одним
результатом float64. В листинге 13-20 тип интерфейса используется
для
создания
карты,
которая
заполняется
элементами,
соответствующими интерфейсу.
package main
import (
"fmt"
"composition/store"
)
func main() {
products := map[string]store.ItemForSale {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for key, p := range products {
fmt.Println("Key:", key, "Price:", p.Price(0.2))
}
}
Листинг 13-20 Использование интерфейса в файле main.go в папке composition

Изменение карты таким образом, чтобы она использовала
интерфейс, позволяет мне сохранять значения Product и Boat. Тип
Product напрямую соответствует интерфейсу ItemForSale, поскольку

существует метод Price, который соответствует сигнатуре, указанной
интерфейсом, и имеет приемник *Product.
Не существует метода Price, принимающего приемник *Boat, но
Go учитывает метод Price, продвигаемый из встроенного поля типа
Boat, который он использует для удовлетворения требований
интерфейса. Скомпилируйте и запустите проект, и вы получите
следующий вывод:
Key: Kayak Price: 334.8
Key: Ball Price: 23.4

Понимание ограничения переключения типа
Интерфейсы могут указывать только методы, поэтому при записи
вывода я использовал ключ, используемый для хранения значений в
карте в листинге 13-20. В главе 11 я объяснил, что операторы switch
могут использоваться для получения доступа к базовым типам, но это
не работает так, как можно было бы ожидать, как показано в листинге
13-21.
package main
import (
"fmt"
"composition/store"
)
func main() {
products := map[string]store.ItemForSale {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for key, p := range products {
switch item := p.(type) {
case *store.Product, *store.Boat:
fmt.Println("Name:", item.Name, "Category:",
item.Category,
"Price:", item.Price(0.2))
default:

p.Price(0.2))
}
}

fmt.Println("Key:", key, "Price:",

}

Листинг 13-21 Доступ к базовому типу в файле main.go в папке composition

Оператор case в листинге 13-21 указывает *Product и *Boat, что
приводит к сбою компилятора со следующей ошибкой:
.\main.go:21:42: item.Name undefined (type store.ItemForSale
has no field or method Name)
.\main.go:21:66:
item.Category
undefined
(type
store.ItemForSale has no field or method Category)
Эта проблема заключается в том, что операторы case, которые
определяют несколько типов, будут соответствовать значениям всех
этих типов, но не будут выполнять утверждение типа. Для листинга 1321 это означает, что значения *Product и *Boat будут соответствовать
оператору case, но тип переменной item будет ItemForSale, поэтому
компилятор выдает ошибку. Вместо этого должны использоваться
дополнительные утверждения типа или однотипные операторы case,
как показано в листинге 13-22.
package main
import (
"fmt"
"composition/store"
)
func main() {
products := map[string]store.ItemForSale {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for key, p := range products {

switch item := p.(type) {
case *store.Product:
fmt.Println("Name:", item.Name, "Category:",
item.Category,
"Price:", item.Price(0.2))
case *store.Boat:
fmt.Println("Name:", item.Name, "Category:",
item.Category,
"Price:", item.Price(0.2))
default:
fmt.Println("Key:", key, "Price:",
p.Price(0.2))
}
}
}
Листинг 13-22 Использование отдельных операторов case в файле main.go в папке
composition

Утверждение типа выполняется оператором case, когда указан один
тип, хотя это может привести к дублированию при обработке каждого
типа. Код в листинге 13-22 выдает следующий результат, когда проект
компилируется и выполняется:
Name: Kayak Category: Watersports Price: 334.8
Name: Soccer Ball Category: Soccer Price: 23.4
Альтернативным решением является определение методов
интерфейса, обеспечивающих доступ к значениям свойств. Это можно
сделать, добавив методы к существующему интерфейсу или определив
отдельный интерфейс, как показано в листинге 13-23.
package store
type Product struct {
Name, Category string
price float64
}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}

float64)

func (p *Product) Price(taxRate float64) float64 {
return p.price + (p.price * taxRate)
}
type Describable interface {
GetName() string
GetCategory() string
}
func (p *Product) GetName() string {
return p.Name
}
func (p *Product) GetCategory() string {
return p.Category
}
Листинг 13-23 Определение интерфейса в файле product.go в папке store

Интерфейс Describable определяет методы GetName и
GetCategory, которые реализованы для типа *Product. В листинге 1324 оператор switch изменен так, что вместо полей используются
интерфейсы.
package main
import (
"fmt"
"composition/store"
)
func main() {
products := map[string]store.ItemForSale {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for key, p := range products {
switch item := p.(type) {
case store.Describable:

fmt.Println("Name:", item.GetName(),
"Category:", item.GetCategory(),
"Price:", item.
(store.ItemForSale).Price(0.2))
default:
fmt.Println("Key:", key, "Price:",
p.Price(0.2))
}
}
}
Листинг 13-24 Использование интерфейсов в файле main.go в папке composition

Это работает, но для доступа к методу Price требуется утверждение
типа интерфейса ItemForSale. Это проблематично, поскольку тип
может реализовать интерфейс Describable, но не интерфейс
ItemForSale, что может вызвать ошибку времени выполнения. Я мог
бы справиться с утверждением типа, добавив метод Price в интерфейс
Describable, но есть альтернатива, которую я опишу в следующем
разделе. Скомпилируйте и запустите проект, и вы увидите следующий
вывод:
Name: Kayak Category: Watersports Price: 334.8
Name: Soccer Ball Category: Soccer Price: 23.4

Составление интерфейсов
Go позволяет составлять интерфейсы из других интерфейсов, как
показано в листинге 13-25.
package store
type Product struct {
Name, Category string
price float64
}
func NewProduct(name, category string, price
*Product {
return &Product{ name, category, price }
}
func (p *Product) Price(taxRate float64) float64 {

float64)

}

return p.price + (p.price * taxRate)

type Describable interface {
GetName() string
GetCategory() string
ItemForSale
}
func (p *Product) GetName() string {
return p.Name
}
func (p *Product) GetCategory() string {
return p.Category
}
Листинг 13-25 Составление интерфейса в файле product.go в папке store

Один интерфейс может заключать в себе другой, в результате чего
типы должны реализовывать все методы, определенные включающим и
вложенным интерфейсами. Интерфейсы проще, чем структуры, и нет
полей или методов для продвижения. Результатом составления
интерфейсов
является
объединение
методов,
определенных
включающим и вложенным типами. В этом примере объединение
означает, что для реализации интерфейса Describable требуются
методы GetName, GetCategory и Price. Методы GetName и GetCategory,
определенные
непосредственно
интерфейсом
Describable,
объединяются с методом Price, определенным интерфейсом
ItemForSale.
Изменение интерфейса Describable означает, что утверждение
типа, которое я использовал в предыдущем разделе, больше не
требуется, как показано в листинге 13-26.
package main
import (
"fmt"
"composition/store"
)
func main() {

products := map[string]store.ItemForSale {
"Kayak": store.NewBoat("Kayak", 279, 1, false),
"Ball": store.NewProduct("Soccer Ball", "Soccer",
19.50),
}
for key, p := range products {
switch item := p.(type) {
case store.Describable:
fmt.Println("Name:", item.GetName(),
"Category:", item.GetCategory(),
"Price:", item.Price(0.2))
default:
fmt.Println("Key:", key, "Price:",
p.Price(0.2))
}
}
}
Листинг 13-26 Удаление утверждения в файле main.go в папке composition

Значение любого типа, реализующего интерфейс Describable,
должно иметь метод Price из-за композиции, выполненной в листинге
13-25, что означает, что метод может быть вызван без потенциально
рискованного утверждения типа. Скомпилируйте и запустите проект, и
вы получите следующий вывод:
Name: Kayak Category: Watersports Price: 334.8
Name: Soccer Ball Category: Soccer Price: 23.4

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

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

Ответ

Кто они такие?

Горутины — это легкие потоки, созданные и управляемые средой выполнения
Go. Каналы — это конвейеры, передающие значения определенного типа.

Почему они
полезны?

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

Как они
используются?

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

Есть ли
подводные
камни или
ограничения?

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

Есть ли
альтернативы?

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

Таблица 14-2 суммирует содержание главы.
Таблица 14-2 Краткое содержание главы
Проблема

Решение

Листинг

Выполненить функции асинхронно

Создайте горутину

7

Получить результат из функции, выполняемой
асинхронно

Использовать канал

10, 15, 16,
22–26

Отправка и получение значений с помощью канала Используйте выражения со
стрелками

11–13

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

17–20

Используйте функцию закрытия

Проблема

Решение

Листинг

Перечислить значения, полученные из канала

Используйте цикл for с
ключевым словом range

21

Отправка или получение значений с
использованием нескольких каналов

Используйте оператор select

27–32

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем concurrency.
Запустите команду, показанную в листинге 14-1, чтобы создать файл
модуля.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/pro-go.
См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init concurrency
Листинг 14-1 Инициализация модуля

Добавьте файл с именем product.go в папку параллелизма с
содержимым, показанным в листинге 14-2.
package main
import "strconv"
type Product struct {
Name, Category string
Price float64
}
var ProductList = []*Product {
{ "Kayak", "Watersports", 279 },
{ "Lifejacket", "Watersports", 49.95 },
{ "Soccer Ball", "Soccer", 19.50 },
{ "Corner Flags", "Soccer", 34.95 },
{ "Stadium", "Soccer", 79500 },
{ "Thinking Cap", "Chess", 16 },

}

{ "Unsteady Chair", "Chess", 75 },
{ "Bling-Bling King", "Chess", 1200 },

type ProductGroup []*Product
type ProductData = map[string]ProductGroup
var Products = make(ProductData)
func ToCurrency(val float64) string {
return "$" + strconv.FormatFloat(val, 'f', 2, 64)
}
func init() {
for _, p := range ProductList {
if _, ok := Products[p.Category]; ok {
Products[p.Category] = append(Products[p.Category],
p)
} else {
Products[p.Category] = ProductGroup{ p }
}
}
}
Листинг 14-2 Содержимое файла product.go в папке concurrency

Этот файл определяет настраиваемый тип с именем Product, а также
псевдонимы типов, которые я использую для создания карты, которая
упорядочивает продукты по категориям. Я использую тип Product в срезе
и карте и полагаюсь на функцию инициализации, описанную в главе 12,
для заполнения карты содержимым среза, который сам заполняется с
использованием литерального синтаксиса. Этот файл также содержит
функцию ToCurrency, которая форматирует значения float64 в строки
долларовой валюты, которые я буду использовать для форматирования
результатов в этой главе.
Добавьте файл с именем operations.go в папку concurrency с
содержимым, показанным в листинге 14-3.
package main
import "fmt"
func CalcStoreTotal(data ProductData) {

}

var storeTotal float64
for category, group := range data {
storeTotal +=group.TotalPrice(category)
}
fmt.Println("Total:", ToCurrency(storeTotal))
func (group ProductGroup) TotalPrice(category string, ) (total
float64) {
for _, p := range group {
total += p.Price
}
fmt.Println(category, "subtotal:", ToCurrency(total))
return
}
Листинг 14-3 Содержимое файла operations.go в папке concurrency

В этом файле определяются методы, работающие с псевдонимами
типов, созданными в файле product.go. Как я объяснял в главе 11, методы
могут быть определены только для типов, созданных в том же пакете, что
означает, что я не могу определить метод, например, для типа []*Product,
но я могу создать псевдоним для этого типа и используйте псевдоним в
качестве приемника метода.
Добавьте файл с именем main.go в папку параллелизма с содержимым,
показанным в листинге 14-4.
package main
import "fmt"
func main() {

}

fmt.Println("main function started")
CalcStoreTotal(Products)
fmt.Println("main function complete")
Листинг 14-4 Содержимое файла main.go в папке concurrency

Используйте командную строку для запуска команды, показанной в
листинге 14-5, в папке concurrency.
go run .

Листинг 14-5 Запуск примера проекта

Код будет скомпилирован и выполнен, что приведет к следующему
выводу:
main function started
Watersports subtotal: $328.95
Soccer subtotal: $79554.45
Chess subtotal: $1291.00
Total: $81174.40
main function complete

Понимание того, как Go выполняет код
Ключевым строительным блоком для выполнения программы Go является
горутина, представляющая собой облегченный поток, созданный средой
выполнения Go. Все программы Go используют по крайней мере одну
горутину, потому что именно так Go выполняет код в main функции. Когда
скомпилированный код Go выполняется, среда выполнения создает
горутину, которая начинает выполнять операторы в точке входа, которая
является main функцией в основном пакете. Каждый оператор в main
функции выполняется в том порядке, в котором они определены. Горутина
продолжает выполнять операторы, пока не достигнет конца основной
функции, после чего приложение завершает работу.
Горутина выполняет каждый оператор в main функции синхронно, что
означает, что она ожидает завершения оператора, прежде чем перейти к
следующему оператору. Операторы в функции main могут вызывать
другие функции, использовать циклы for, создавать значения и
использовать все другие возможности, описанные в этой книге. Основная
горутина будет проходить через код, следуя своему пути, выполняя по
одному оператору за раз.
Для примера приложения это означает, что карта продуктов
обрабатывается последовательно, так что каждая категория продуктов
обрабатывается по очереди, а внутри каждой категории обрабатывается
каждый продукт, как показано на рисунке 14-1.

Рисунок 14-1 Последовательное исполнение

В листинге 14-6 добавлен оператор, который записывает сведения о
каждом продукте по мере его обработки, что демонстрирует поток,
показанный на рисунке.
package main
import "fmt"
func CalcStoreTotal(data ProductData) {
var storeTotal float64
for category, group := range data {
storeTotal += group.TotalPrice(category)
}
fmt.Println("Total:", ToCurrency(storeTotal))
}
func (group ProductGroup) TotalPrice(category string) (total
float64) {
for _, p := range group {
fmt.Println(category, "product:", p.Name)
total += p.Price
}
fmt.Println(category, "subtotal:", ToCurrency(total))
return
}
Листинг 14-6 Добавление оператора в файл operations.go в папке concurrency

Скомпилируйте и выполните код, и вы увидите вывод, подобный
следующему:

main function started
Soccer product: Soccer Ball
Soccer product: Corner Flags
Soccer product: Stadium
Soccer subtotal: $79554.45
Chess product: Thinking Cap
Chess product: Unsteady Chair
Chess product: Bling-Bling King
Chess subtotal: $1291.00
Watersports product: Kayak
Watersports product: Lifejacket
Watersports subtotal: $328.95
Total: $81174.40
main function complete
Вы можете увидеть разные результаты в зависимости от порядка, в
котором ключи извлекаются из карты, но важно то, что все продукты в
категории обрабатываются до того, как выполнение перейдет к следующей
категории.
Преимущества синхронного выполнения заключаются в простоте и
согласованности — поведение синхронного кода легко понять и
предсказать. Недостатком является то, что он может быть неэффективным.
Последовательная работа с девятью элементами данных, как в примере, не
представляет никаких проблем, но большинство реальных проектов имеют
большие объемы данных или требуют выполнения других задач, а это
означает, что последовательное выполнение занимает слишком много
времени и не дает результатов достаточно быстро.

Создание дополнительных горутин
Go позволяет разработчику создавать дополнительные горутины, которые
выполняют код одновременно с main горутиной. Go упрощает создание
новых горутин, как показано в листинге 14-7.
package main
import "fmt"
func CalcStoreTotal(data ProductData) {
var storeTotal float64
for category, group := range data {
go group.TotalPrice(category)

}

}
fmt.Println("Total:", ToCurrency(storeTotal))

func (group ProductGroup) TotalPrice(category string) (total
float64) {
for _, p := range group {
fmt.Println(category, "product:", p.Name)
total += p.Price
}
fmt.Println(category, "subtotal:", ToCurrency(total))
return
}
Листинг 14-7 Создание подпрограмм Go в файле operations.go в папке concurrency

Горутина создается с использованием ключевого слова go, за которым
следует функция или метод, которые должны выполняться асинхронно,
как показано на рисунке 14-2.

Рисунок 14-2 Горутина

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

Рисунок 14-3 Параллельные вызовы функций

Подпрограммы Go упрощают вызов функций и методов, но изменение
в листинге 14-7 привело к общей проблеме. Скомпилируйте и выполните
проект, и вы получите следующие результаты:
main function started
Total: $0.00
main function complete
Вы можете увидеть немного другие результаты, которые могут
включать промежуточные итоги по одной или нескольким категориям. Но,
в большинстве случаев, вы увидите эти сообщения. Перед введением в код
горутин метод TotalPrice вызывался так:
...
storeTotal += group.TotalPrice(category)
...
Это синхронный вызов функции. Он указывает среде выполнения
выполнять операторы в методе TotalPrice один за другим и присваивать
результат переменной с именем storeTotal. Выполнение не будет
продолжаться до тех пор, пока не будут обработаны все операторы
TotalPrice. Но в листинге 14-7 представлена горутина для выполнения
функции, например:
...
go group.TotalPrice(category)
...

Этот оператор указывает среде выполнения выполнять операторы в
методе TotalPrice с использованием новой горутины. Среда выполнения
не ждет, пока горутина выполнит метод, и немедленно переходит к
следующему оператору. В этом весь смысл горутин, потому что метод
TotalPrice будет вызываться асинхронно, а это означает, что его
операторы оцениваются одной горутиной в то же время, когда исходная
горутина выполняет операторы в основной функции. Но, как я объяснял
ранее, программа завершается, когда main горутина выполняет все
операторы в main функции.
В результате программа завершается до того, как будут созданы
горутины для завершения выполнения метода TotalPrice, поэтому
промежуточные итоги отсутствуют.
Я объясню, как решить эту проблему, когда буду вводить
дополнительные функции, но на данный момент все, что мне нужно
сделать, это предотвратить завершение программы на время, достаточное
для завершения горутин, как показано в листинге 14-8.
package main
import (
"fmt"
"time"
)
func main() {

}

fmt.Println("main function started")
CalcStoreTotal(Products)
time.Sleep(time.Second * 5)
fmt.Println("main function complete")
Листинг 14-8 Отложенный выход программы в файле main.go в папке concurrency

Пакет time является частью стандартной библиотеки и описан в главе
19. Пакет time предоставляет функцию Sleep, которая приостанавливает
горутину, выполняющую инструкцию. Период ожидания указывается с
помощью набора числовых констант, представляющих интервалы, так что
time.Second представляет одну секунду и умножается на 5, чтобы создать
пятисекундный период.
В этом случае он приостановит выполнение main горутины, что даст
созданным горутинам время для выполнения метода TotalPrice. По

истечении периода ожидания main горутина возобновит выполнение
операторов, достигнет конца функции и заставит программу завершиться.
Скомпилируйте и запустите проект, и вы получите следующий вывод:
main function started
Watersports product: Kayak
Watersports product: Lifejacket
Watersports subtotal: $328.95
Soccer product: Soccer Ball
Soccer product: Corner Flags
Soccer product: Stadium
Soccer subtotal: $79554.45
Chess product: Thinking Cap
Chess product: Unsteady Chair
Chess product: Bling-Bling King
Chess subtotal: $1291.00
Total: $0.00
main function complete
Программа больше не существует раньше, но трудно быть уверенным,
что горутины работают одновременно. Это потому, что пример настолько
прост, что одна горутина может завершиться за небольшое количество
времени, которое требуется Go для создания и запуска следующего. В
листинге 14-9 я добавил еще одну паузу, которая замедлит выполнение
метода TotalPrice, чтобы показать, как выполняется код. (Это то, чего вы
не должны делать в реальном проекте, но это полезно для понимания того,
как работают эти функции.)
package main
import (
"fmt"
"time"
)
func CalcStoreTotal(data ProductData) {
var storeTotal float64
for category, group := range data {
go group.TotalPrice(category)
}
fmt.Println("Total:", ToCurrency(storeTotal))
}

func (group ProductGroup) TotalPrice(category string) (total
float64) {
for _, p := range group {
fmt.Println(category, "product:", p.Name)
total += p.Price
time.Sleep(time.Millisecond * 100)
}
fmt.Println(category, "subtotal:", ToCurrency(total))
return
}
Листинг 14-9 Добавление оператора Sleep в файл operations.go в папке concurrency

Новый оператор добавляет 100 миллисекунд к каждой итерации цикла
for в методе TotalPrice. Скомпилируйте и выполните код, и вы увидите
вывод, подобный следующему:
main function started
Total: $0.00
Soccer product: Soccer Ball
Watersports product: Kayak
Chess product: Thinking Cap
Chess product: Unsteady Chair
Watersports product: Lifejacket
Soccer product: Corner Flags
Chess product: Bling-Bling King
Soccer product: Stadium
Watersports subtotal: $328.95
Soccer subtotal: $79554.45
Chess subtotal: $1291.00
main function complete
Вы можете увидеть другой порядок результатов, но ключевым
моментом является то, что сообщения для разных категорий чередуются,
показывая, что данные обрабатываются параллельно. (Если изменение в
листинге 14-9 не дает ожидаемых результатов, возможно, вам придется
увеличить паузу, введенную функцией time.Sleep.)

Возврат результатов из горутин
Когда я создавал горутины в листинге 14-7, я изменил способ вызова
метода TotalPrice. Изначально код выглядел так:
...

storeTotal += group.TotalPrice(category)
...
Но когда я представил подпрограмму Go, я изменил утверждение на
следующее:
...
go group.TotalPrice(category)
...
Я получил асинхронное выполнение, но потерял результат метода,
поэтому вывод из листинга 14-9 включает в себя нулевой результат для
общего итога:
...
Total: $0.00
...
Получение результата от функции, которая выполняется асинхронно,
может быть сложным, поскольку требует координации между горутиной,
которая создает результат, и горутиной, которая использует результат.
Чтобы решить эту проблему, Go предоставляет каналы, которые
являются магистралями, по которым данные могут быть отправлены и
получены. Я собираюсь ввести канал в пример поэтапно, начиная с
листинга 14-10, что означает, что пример не будет компилироваться, пока
процесс не будет завершен.
package main
import (
"fmt"
"time"
)
func CalcStoreTotal(data ProductData) {
var storeTotal float64
var channel chan float64 = make(chan float64)
for category, group := range data {
go group.TotalPrice(category)
}
fmt.Println("Total:", ToCurrency(storeTotal))
}

func (group ProductGroup) TotalPrice(category string) (total
float64) {
for _, p := range group {
fmt.Println(category, "product:", p.Name)
total += p.Price
time.Sleep(time.Millisecond * 100)
}
fmt.Println(category, "subtotal:", ToCurrency(total))
return
}
Листинг 14-10 Определение канала в файле operations.go в папке concurrency

Каналы строго типизированы, что означает, что они будут нести
значения указанного типа или интерфейса. Тип канала — это ключевое
слово chan, за которым следует тип, который будет передавать канал, как
показано на рисунке 14-4. Каналы создаются с помощью встроенной
функции make с указанием типа канала.

Рисунок 14-4 Определение канала

Я использовал полный синтаксис объявления переменных в этом
листинге, чтобы подчеркнуть тип, которым является chan float64, что
означает канал, который будет передавать значения float64.
Примечание Пакет sync предоставляет функции для управления
горутинами, которые совместно используют данные, как описано в
главе 30.

Отправка результата с использованием канала
Следующим шагом является обновление метода TotalPrice, чтобы он
отправлял свой результат по каналу, как показано в листинге 14-11.
package main
import (
"fmt"
"time"

)
func CalcStoreTotal(data ProductData) {
var storeTotal float64
var channel chan float64 = make(chan float64)
for category, group := range data {
go group.TotalPrice(category)
}
fmt.Println("Total:", ToCurrency(storeTotal))
}
func
(group
ProductGroup)
TotalPrice(category
string,
resultChannel chan float64) {
var total float64
for _, p := range group {
fmt.Println(category, "product:", p.Name)
total += p.Price
time.Sleep(time.Millisecond * 100)
}
fmt.Println(category, "subtotal:", ToCurrency(total))
resultChannel >+279.00>0000279.00>279.00 1)
Printfln("Bool: %t", len(name) > 100)
}
Листинг 17-17 Форматирование логических значений в файле main.go в папке
usingstrings

Скомпилируйте
и
запустите
отформатированный вывод:

проект,

и

вы

Bool: true
Bool: false

Использование глагола форматирования указателя
Глагол, описанный в таблице 17-11, применяется к указателям.

увидите

Таблица 17-11 Глагол форматирования указателя
Глагол Описание
%p

Эта команда отображает шестнадцатеричное представление места хранения
указателя.

В листинге 17-18 показано использование глагола-указателя.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {
fmt.Printf(template + "\n", values...)
}
func main() {
name := "Kayak"
Printfln("Pointer: %p", &name)
}
Листинг 17-18 Форматирование указателя в файле main.go в папке usingstrings

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

код, и вы
вы можете

увидите
увидеть

вывод,
другое

Pointer: 0xc00004a240

Сканирование строк
Пакет fmt предоставляет функции для сканирования строк, то есть
процесса анализа строк, содержащих значения, разделенные
пробелами. Таблица 17-12 описывает эти функции, некоторые из
которых используются вместе с функциями, описанными в
последующих главах.
Таблица 17-12 Функции fmt для сканирования строк
Функция

Описание

Функция

Описание

Scan(...vals)

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

Scanln(...vals) Эта функция работает так же, как Scan, но останавливает чтение, когда
встречает символ новой строки.
Scanf(template, Эта функция работает так же, как Scan, но использует строку шаблона для
...vals)
выбора значений из получаемых входных данных.
Fscan(reader,
...vals)

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

Fscanln(reader, Эта функция работает так же, как Fscan, но останавливает чтение, когда
...vals)
встречает символ новой строки.
Fscanf(reader,
template,
...vals)

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

Sscan(str,
...vals)

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

Sscanf(str,
template,
...vals)

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

Sscanln(str,
template,
...vals)

Эта функция работает так же, как Sscanf, но останавливает сканирование
строки, как только встречается символ новой строки.

Решение о том, какую функцию сканирования использовать,
зависит от источника строки для сканирования, способа обработки
новых строк и необходимости использования шаблона. В листинге 1719 показано основное использование функции Scan, с которого можно
начать.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {

}

fmt.Printf(template + "\n", values...)

func main() {
var name string
var category string
var price float64
fmt.Print("Enter text to scan: ")
n, err := fmt.Scan(&name, &category, &price)
if (err == nil) {
Printfln("Scanned %v values", n)
Printfln("Name: %v, Category: %v, Price: %.2f", name,
category, price)
} else {
Printfln("Error: %v", err.Error())
}
}
Листинг 17-19 Сканирование строки в файле main.go в папке usingstrings

Функция Scan считывает строку из стандартного ввода и сканирует
ее на наличие значений, разделенных пробелами. Значения,
извлеченные из строки, присваиваются параметрам в том порядке, в
котором они определены. Чтобы функция Scan могла присваивать
значения, ее параметры являются указателями.
В листинге 17-19 я определяю переменные name, category и price
и использую их в качестве аргументов функции Scan:
...
n, err := fmt.Scan(&name, &category, &price)
...
При вызове функция Scan считывает строку, извлекает три
значения, разделенных пробелами, и присваивает их переменным.
Скомпилируйте и запустите проект, и вам будет предложено ввести
текст, например:
...
Enter text to scan:

...
Введите Kayak Watersports 279, что означает слово Kayak, за
которым следует пробел, за которым следует слово Watersports, за
которым следует пробел, за которым следует число 279. Нажмите
Enter, и строка будет отсканирована, и будет получен следующий
результат:
Scanned 3 values
Name: Kayak, Category: Watersports, Price: 279.00
Функция Scan должна преобразовать полученные подстроки в
значения Go и сообщит об ошибке, если строка не может быть
обработана. Запустите код еще раз, но введите Kayak Watersports
Zero, и вы получите следующую ошибку:
Error: strconv.ParseFloat: parsing "": invalid syntax
Строка Zero не может быть преобразована в значение Go float64,
которое является типом параметра Price.
СКАНИРОВАНИЕ В СРЕЗ
Если вам нужно просмотреть ряд значений одного типа,
естественным подходом будет просмотр среза или массива,
например:
...
vals := make([]string, 3)
fmt.Print("Enter text to scan: ")
fmt.Scan(vals...)
Printfln("Name: %v", vals)
...
Этот код не будет скомпилирован, потому что срез строки не
может быть правильно разложен для использования с вариативным
параметром. Требуется дополнительный шаг, а именно:
...
vals := make([]string, 3)
ivals := make([]interface{}, 3)

for i := 0; i < len(vals); i++ {
ivals[i] = &vals[i]
}
fmt.Print("Enter text to scan: ")
fmt.Scan(ivals...)
Printfln("Name: %v", vals)
...
Это неудобный процесс, но его можно обернуть
вспомогательной функцией, чтобы вам не приходилось каждый раз
создавать срез interface.

Работа с символами новой строки
По умолчанию сканирование обрабатывает новые строки так же, как
пробелы, выступающие в качестве разделителей между значениями.
Чтобы увидеть это поведение, запустите проект и, когда появится
запрос на ввод, введите Kayak, затем пробел, затем Watersports, затем
клавишу Enter, 279, а затем снова клавишу Enter. Эта
последовательность выдаст следующий результат:
Scanned 3 values
Name: Kayak, Category: Watersports, Price: 279.00
Функция Scan не прекращает поиск значений до тех пор, пока не
получит ожидаемое число, а первое нажатие клавиши Enter
рассматривается как разделитель, а не как завершение ввода. Функции,
имена которых заканчиваются на ln в таблице 17-12, такие как Scanln,
изменяют это поведение. В листинге 17-20 используется функция
Scanln.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {
fmt.Printf(template + "\n", values...)
}
func main() {

var name string
var category string
var price float64
fmt.Print("Enter text to scan: ")
n, err := fmt.Scanln(&name, &category, &price)
if (err == nil) {
Printfln("Scanned %v values", n)
Printfln("Name: %v, Category: %v, Price: %.2f", name,
category, price)
} else {
Printfln("Error: %v", err.Error())
}
}
Листинг 17-20 Использование функции Scanln в файле main.go в папке usingstrings

Скомпилируйте
и
выполните
проект
и
повторите
последовательность ввода. Когда вы впервые нажимаете клавишу
Enter, новая строка завершает ввод, оставляя функцию Scanln с
меньшим количеством значений, чем требуется, и производит
следующий вывод:
Error: unexpected newline

Использование другого источника строк
Функции, описанные в таблице 17-12, сканируют строки из трех
источников: стандартного ввода, средства чтения (описанного в главе
20) и значения, переданного в качестве аргумента. Предоставление
строки в качестве аргумента является наиболее гибким, поскольку это
означает, что строка может возникнуть откуда угодно. В листинге 1721 я заменил функцию Scanln на Sscan, которая позволяет мне
сканировать строковую переменную.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {
fmt.Printf(template + "\n", values...)

}
func main() {
var name string
var category string
var price float64
source := "Lifejacket Watersports 48.95"
n, err := fmt.Sscan(source, &name, &category, &price)
if (err == nil) {
Printfln("Scanned %v values", n)
Printfln("Name: %v, Category: %v, Price: %.2f", name,
category, price)
} else {
Printfln("Error: %v", err.Error())
}
}
Листинг 17-21 Сканирование переменной в файле main.go в папке usingstrings

Первым аргументом функции Sscan является сканируемая строка,
но во всем остальном процесс сканирования такой же. Скомпилируйте
и запустите проект, и вы увидите следующий вывод:
Scanned 3 values
Name: Lifejacket, Category: Watersports, Price: 48.95

Использование шаблона сканирования
Шаблон можно использовать для поиска значений в строке,
содержащей ненужные символы, как показано в листинге 17-22.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {
fmt.Printf(template + "\n", values...)
}
func main() {

var name string
var category string
var price float64
source := "Product Lifejacket Watersports 48.95"
template := "Product %s %s %f"
n, err := fmt.Sscanf(source, template, &name, &category,
&price)
if (err == nil) {
Printfln("Scanned %v values", n)
Printfln("Name: %v, Category: %v, Price: %.2f", name,
category, price)
} else {
Printfln("Error: %v", err.Error())
}
}
Листинг 17-22 Использование шаблона в файле main.go в папке usingstrings

Шаблон, используемый в листинге 17-22, игнорирует термин
Product, пропуская эту часть строки и позволяя начать сканирование
со следующего термина. Скомпилируйте и запустите проект, и вы
увидите следующий вывод:
Scanned 3 values
Name: Lifejacket, Category: Watersports, Price: 48.95
Сканирование с помощью шаблона не так гибко, как использование
регулярного выражения, потому что отсканированная строка может
содержать только значения, разделенные пробелами. Но использование
шаблона все же может быть полезным, если вам нужны только
некоторые значения в строке и вы не хотите определять сложные
правила сопоставления.

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

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

библиотекой

для

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

Ответ

Кто они
такие?

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

Почему они
полезны?

Это функции, которые используются на протяжении всей разработки.

Как они
Эти функции предоставляются в пакетах math, math/rand и sort.
используются?
Есть ли
подводные
камни или
ограничения?

Если не инициализировано начальным значением, числа, созданные пакетом
math/rand, не являются случайными.

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

Таблица 18-2 суммирует главу.
Таблица 18-2 Краткое содержание главы
Проблема

Решение

Листинг

Выполнить общие
расчеты

Используйте функции, определенные в пакете math

5

Генерация случайных Используйте функции пакета math/rand, позаботившись о 6–9
чисел
том, чтобы предоставить начальное значение.

Проблема

Решение

Листинг

Перемешать элементы Используйте функцию Shuffle
в срезе

10

Сортировка элементов Используйте функции, определенные в пакете sort
в срезе

11, 12,
15–20

Найти элемент в
отсортированном
срезе

13, 14

Используйте функции Search*

Подготовка к этой главе
Чтобы подготовиться к этой главе, откройте новую командную строку,
перейдите в удобное место и создайте каталог с именем
mathandsorting. Запустите команду, показанную в листинге 18-1, в
папке mathandsorting, чтобы создать файл модуля.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
go mod init mathandsorting
Листинг 18-1 Инициализация модуля

Добавьте файл с именем printer.go в папку mathandsorting с
содержимым, показанным в листинге 18-2.
package main
import "fmt"
func Printfln(template string, values ...interface{}) {
fmt.Printf(template + "\n", values...)
}
Листинг 18-2 Содержимое файла printer.go в папке mathandsorting

Добавьте файл с именем main.go в папку mathandsorting с
содержимым, показанным в листинге 18-3.
package main
func main() {
}

Printfln("Hello, Math and Sorting")
Листинг 18-3 Содержимое файла main.go в папке mathandsorting

Используйте командную строку для запуска команды, показанной в
листинге 18-4, в папке mathandsorting.
go run .
Листинг 18-4 Запуск примера проекта

Код будет скомпилирован и выполнен, что приведет к следующему
выводу:
Hello, Math and Sorting

Работа с числами
Как я объяснял в главе 4, язык Go поддерживает набор
арифметических операторов, которые можно применять к числовым
значениям, позволяя выполнять такие базовые задачи, как сложение и
умножение. Для более продвинутых операций стандартная библиотека
Go включает пакет math, предоставляющий обширный набор функций.
Функции, которые наиболее широко используются в типичном
проекте, описаны в таблице 18-3. См. документацию пакета по адресу
https://golang.org/pkg/math, чтобы узнать о полном наборе
функций, включая поддержку более конкретных областей, таких как
тригонометрия.
Таблица 18-3 Полезные функции из математического пакета
Функция

Описание

Функция

Описание

Abs(val)

Эта функция возвращает абсолютное значение значения float64, то есть
расстояние от нуля без учета направления.

Ceil(val)

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

Copysign(x, y)

Эта функция возвращает значение float64, которое является
абсолютным значением x со знаком y.

Floor(val)

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

Max(x, y)

Эта функция возвращает самое большое из указанных значений float64.

Min(x, y)

Эта функция возвращает наименьшее из указанных значений float64.

Mod(x, y)

Эта функция возвращает остаток x/y.

Pow(x, y)

Эта функция возвращает значение x, возведенное в степень y.

Round(val)

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

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

Все эти функции работают со значениями float64 и выдают
результаты float64, что означает, что вы должны явно
преобразовывать в другие типы и из них. В листинге 18-5 показано
использование функций, описанных в таблице 18-3.
package main
import "math"
func main() {
val1 := 279.00
val2 := 48.95
Printfln("Abs: %v", math.Abs(val1))
Printfln("Ceil: %v", math.Ceil(val2))
Printfln("Copysign: %v", math.Copysign(val1, -5))

}

Printfln("Floor: %v", math.Floor(val2))
Printfln("Max: %v", math.Max(val1, val2))
Printfln("Min: %v", math.Min(val1, val2))
Printfln("Mod: %v", math.Mod(val1, val2))
Printfln("Pow: %v", math.Pow(val1, 2))
Printfln("Round: %v", math.Round(val2))
Printfln("RoundToEven: %v", math.RoundToEven(val2))

Листинг 18-5 Using Functions from the math Package in the main.go File in the
mathandsorting Folder

Скомпилируйте и запустите проект, и вы увидите следующий
вывод:
Abs: 279
Ceil: 49
Copysign: -279
Floor: 48
Max: 279
Min: 48.95
Mod: 34.249999999999986
Pow: 77841
Round: 49
RoundToEven: 49
Пакет math также предоставляет набор констант для ограничений
числовых типов данных, как описано в таблице 18-4.
Таблица 18-4 Предельные константы
Имя

Описание

MaxInt8
MinInt8

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

MaxInt16
MinInt16

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

MaxInt32
MinInt32

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

MaxInt64
MinInt64

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

Имя

Описание

MaxUint8

Эта константа представляет наибольшее значение, которое может
быть представлено с помощью uint8. Наименьшее значение
равно нулю.

MaxUint16

Эта константа представляет наибольшее значение, которое может
быть представлено с помощью uint16. Наименьшее значение
равно нулю.

MaxUint32

Эта константа представляет наибольшее значение, которое может
быть представлено с помощью uint32. Наименьшее значение
равно нулю.

MaxUint64

Эта константа представляет наибольшее значение, которое может
быть представлено с помощью uint64. Наименьшее значение
равно нулю.

MaxFloat32
MaxFloat64

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

SmallestNonzeroFloat32 Эти константы представляют наименьшие ненулевые значения,
SmallestNonzeroFloat64 которые могут быть представлены с использованием значений
float32 и float64.

Генерация случайных чисел
Пакет math/rand обеспечивает поддержку генерации случайных чисел.
Наиболее полезные функции описаны в таблице 18-5. (Хотя в этом
разделе я использую термин случайный, числа, создаваемые пакетом
math/rand, являются псевдослучайными, что означает, что их не
следует использовать там, где важна случайность, например, для
генерации криптографических ключей.)
Таблица 18-5 Полезные функции math/rand
Функция

Описание

Seed(s)

Эта функция устанавливает начальное значение, используя указанное
значение int64.

Float32()

Эта функция генерирует случайное значение float32 в диапазоне от 0 до
1.

Float64()

Эта функция генерирует случайное значение float64 в диапазоне от 0 до
1.

Int()

Эта функция генерирует случайное int значение.

Intn(max)

Эта функция генерирует случайное int число меньше указанного
значения, как описано после таблицы.

Функция

Описание

UInt32()

Эта функция генерирует случайное значение uint32.

UInt64()

Эта функция генерирует случайное значение uint64.

Shuffle(count, Эта функция используется для рандомизации порядка элементов, как
func)
описано после таблицы.

Необычность пакета math/rand заключается в том, что он по
умолчанию возвращает последовательность предсказуемых значений,
как показано в листинге 18-6.
package main
import "math/rand"
func main() {

}

for i := 0; i < 5; i++ {
Printfln("Value %v : %v", i, rand.Int())
}

Листинг 18-6 Генерация предсказуемых значений в файле main.go в папке
mathandsorting

В этом примере вызывается функция Int и выводится значение.
Скомпилируйте и выполните код, и вы увидите следующий вывод:
Value
Value
Value
Value
Value

0
1
2
3
4

:
:
:
:
:

5577006791947779410
8674665223082153551
6129484611666145821
4037200794235010051
3916589616287113937

Код в листинге 18-6 всегда будет выдавать один и тот же набор
чисел, потому что начальное начальное значение всегда одно и то же.
Чтобы избежать создания одной и той же последовательности чисел,
функцию Seed необходимо вызывать с нефиксированным значением,
как показано в листинге 18-7.
package main

import (
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
Printfln("Value %v : %v", i, rand.Int())
}
}
Листинг 18-7 Установка начального значения в файле main.go в папке mathandsorting

Соглашение состоит в том, чтобы использовать текущее время в
качестве начального значения, что делается путем вызова функции
Now, предоставляемой пакетом time, и вызова метода UnixNano для
результата, который предоставляет значение int64, которое можно
передать в функцию начального значения. (Я описываю пакет времени
в главе 19.) Скомпилируйте и запустите проект, и вы увидите ряд
чисел, которые меняются при каждом выполнении программы. Вот
результат, который я получил:
Value
Value
Value
Value
Value

0
1
2
3
4

:
:
:
:
:

8113726196145714527
3479565125812279859
8074476402089812953
3916870404047362448
8226545715271170755

Генерация случайного числа в определенном диапазоне
Функцию Intn можно использовать для генерации числа с заданным
максимальным значением, как показано в листинге 18-8.
package main
import (
"math/rand"
"time"
)
func main() {

}

rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
Printfln("Value %v : %v", i, rand.Intn(10))
}

Листинг 18-8 Указание максимального значения в файле main.go в папке mathandsorting

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

0
1
2
3
4

:
:
:
:
:

7
5
4
0
7

Не существует функции для указания минимального значения, но
можно легко сдвинуть значения, сгенерированные функцией Intn, в
определенный диапазон, как показано в листинге 18-9.
package main
import (
"math/rand"
"time"
)
func IntRange(min, max int) int {
return rand.Intn(max - min) + min
}
func main() {

}

rand.Seed(time.Now().UnixNano())
for i := 0; i < 5; i++ {
Printfln("Value %v : %v", i, IntRange(10, 20))
}
Листинг 18-9 Указание нижней границы в файле main.go в папке mathandsorting

Функция IntRange возвращает случайное число в определенном
диапазоне. Скомпилируйте и выполните проект, и вы получите
последовательность чисел от 10 до 19, похожую на следующую:
Value
Value
Value
Value
Value

0
1
2
3
4

:
:
:
:
:

10
19
11
10
17

Перетасовка элементов
Функция Shuffle используется для случайного переупорядочивания
элементов, что она делает с помощью пользовательской функции, как
показано в листинге 18-10.
package main
import (
"math/rand"
"time"
)
var names = []string { "Alice", "Bob", "Charlie", "Dora",
"Edith"}
func main() {
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(names), func (first, second int) {
names[first], names[second] = names[second],
names[first]
})

}

for i, name := range names {
Printfln("Index %v: Name: %v", i, name)
}

Листинг 18-10 Перетасовка элементов в файле main.go в папке mathandsorting

Аргументами функции Shuffle являются количество элементов и
функция, которая меняет местами два элемента, идентифицируемых по

индексу. Функция вызывается для случайной замены элементов. В
листинге 18-10 анонимная функция переключает два элемента в срезе
names, а это означает, что использование функции Shuffle приводит к
перетасовке порядка значений names. Скомпилируйте и выполните
проект, и выходные данные будут отображать перетасованный порядок
элементов в срезе names, подобно следующему:
Index
Index
Index
Index
Index

0:
1:
2:
3:
4:

Name:
Name:
Name:
Name:
Name:

Edith
Dora
Charlie
Alice
Bob

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

Сортировка числовых и строковых срезов
Функции, описанные в таблице 18-6, используются для сортировки
срезов, содержащих значения int, float64 или string.
Таблица 18-6 Основные функции сортировки
Функция

Описание

Float64s(slice)

Эта функция сортирует срез значений float64. Элементы
сортируются на месте.

Float64sAreSorted(slice) Эта функция возвращает значение true, если элементы в
указанном срезе float64 упорядочены.
Ints(slice)

Эта функция сортирует срез значений int. Элементы
сортируются на месте.

IntsAreSorted(slice)

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

Strings(slice)

Эта функция сортирует срез string значений. Элементы
сортируются на месте.

Функция

Описание

StringsAreSorted(slice)

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

Каждый из типов данных имеет собственный набор функций,
которые сортируют данные или определяют, были ли они уже
отсортированы, как показано в листинге 18-11.
package main
import (
//"math/rand"
//"time"
"sort"
)
func main() {
ints := []int { 9, 4, 2, -1, 10}
Printfln("Ints: %v", ints)
sort.Ints(ints)
Printfln("Ints Sorted: %v", ints)
floats := []float64 { 279, 48.95, 19.50 }
Printfln("Floats: %v", floats)
sort.Float64s(floats)
Printfln("Floats Sorted: %v", floats)

}

strings := []string { "Kayak", "Lifejacket", "Stadium" }
Printfln("Strings: %v", strings)
if (!sort.StringsAreSorted(strings)) {
sort.Strings(strings)
Printfln("Strings Sorted: %v", strings)
} else {
Printfln("Strings Already Sorted: %v", strings)
}
Листинг 18-11 Сортировка срезов в файле main.go в папке mathandsorting

В этом примере выполняется сортировка срезов, содержащих
значения int и float64. Существует также string срез, который

тестируется с помощью функции StringsAreSorted, чтобы избежать
сортировки данных, которые уже упорядочены. Скомпилируйте и
запустите проект, и вы получите следующий вывод:
Ints: [9 4 2 -1 10]
Ints Sorted: [-1 2 4 9 10]
Floats: [279 48.95 19.5]
Floats Sorted: [19.5 48.95 279]
Strings: [Kayak Lifejacket Stadium]
Strings Already Sorted: [Kayak Lifejacket Stadium]
Обратите внимание, что функции в листинге 18-11 сортируют
элементы на месте, а не создают новый срез. Если вы хотите создать
новый отсортированный срез, вы должны использовать встроенные
функции make и copy, как показано в листинге 18-12. Эти функции
были представлены в главе 7.
package main
import (
"sort"
)
func main() {
ints := []int { 9, 4, 2, -1, 10}
sortedInts := make([]int, len(ints))
copy(sortedInts, ints)
sort.Ints(sortedInts)
Printfln("Ints: %v", ints)
Printfln("Ints Sorted: %v", sortedInts)
}
Листинг 18-12 Создание отсортированной копии среза в файле main.go в папке
mathandsorting

Скомпилируйте и запустите проект, и вы получите следующий
вывод:
Ints: [9 4 2 -1 10]
Ints Sorted: [-1 2 4 9 10]

Поиск отсортированных данных
Пакет sort определяет функции, описанные в таблице 18-7, для поиска
определенного значения в отсортированных данных.
Таблица 18-7 Функции для поиска отсортированных данных
Функция

Описание

SearchInts(slice,
val)

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

SearchFloat64s(slice, Эта функция ищет в отсортированном срезе указанное значение
val)
float64. Результатом является индекс указанного значения или,
если значение не найдено, индекс, по которому значение может
быть вставлено при сохранении порядка сортировки.
SearchStrings(slice,
val)

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

Search(count,
testFunc)

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

Функции, описанные в таблице 18-7, немного неудобны. Когда
значение найдено, функции возвращают его положение в срезе. Но
необычно, если значение не найдено, результатом является позиция, в
которую оно может быть вставлено при сохранении порядка
сортировки, как показано в листинге 18-13.
package main
import (
"sort"
)
func main() {
ints := []int { 9, 4, 2, -1, 10}
sortedInts := make([]int, len(ints))

copy(sortedInts, ints)
sort.Ints(sortedInts)
Printfln("Ints: %v", ints)
Printfln("Ints Sorted: %v", sortedInts)

}

indexOf4:= sort.SearchInts(sortedInts, 4)
indexOf3 := sort.SearchInts(sortedInts, 3)
Printfln("Index of 4: %v", indexOf4)
Printfln("Index of 3: %v", indexOf3)
Листинг 18-13 Поиск отсортированных данных в файле main.go в папке mathandsorting

Скомпилируйте и выполните код, и вы увидите, что поиск
значения, находящегося в срезе, дает тот же результат, что и поиск
несуществующего значения:
Ints: [9 4 2 -1 10]
Ints Sorted: [-1 2 4 9 10]
Index of 4: 2
Index of 3: 2
Этим функциям требуется дополнительный тест, чтобы увидеть,
является ли значение в месте, возвращаемом этими функциями, тем,
которое искали, как показано в листинге 18-14.
package main
import (
"sort"
)
func main() {
ints := []int { 9, 4, 2, -1, 10}
sortedInts := make([]int, len(ints))
copy(sortedInts, ints)
sort.Ints(sortedInts)
Printfln("Ints: %v", ints)
Printfln("Ints Sorted: %v", sortedInts)

indexOf4:= sort.SearchInts(sortedInts, 4)
indexOf3 := sort.SearchInts(sortedInts, 3)
Printfln("Index of 4: %v (present: %v)", indexOf4,
sortedInts[indexOf4] == 4)
Printfln("Index of 3: %v (present: %v)", indexOf3,
sortedInts[indexOf3] == 3)
}
Листинг 18-14 Устранение неоднозначности результатов поиска в файле main.go в папке
mathandsorting

Скомпилируйте и выполните проект, и вы получите следующие
результаты:
Ints: [9 4 2 -1 10]
Ints Sorted: [-1 2 4 9 10]
Index of 4: 2 (present: true)
Index of 3: 2 (present: false)

Сортировка пользовательских типов данных
Для сортировки пользовательских типов данных в пакете sort
определен интерфейс со странным названием Interface, в котором
указаны методы, описанные в таблице 18-8.
Таблица 18-8 Методы, определяемые интерфейсом sort.Interface
Функция Описание
Len()

Этот метод возвращает количество элементов, которые будут отсортированы.

Less(i,
j)

Этот метод возвращает значение true, если элемент с индексом i должен
появиться в отсортированной последовательности перед элементом j. Если
Less(i,j) и Less(j, i) оба false, то элементы считаются равными.

Swap(i,
j)

Этот метод меняет местами элементы по указанным индексам.

Когда тип определяет методы,описанные в таблице 18-8, его
можно сортировать с помощью функций, описанных в таблице 18-9,
которые определяются пакетом sort.
Таблица 18-9 Функции для сортировки типов, реализующих интерфейс
Функция

Описание

Функция

Описание

Sort(data)

Эта функция использует методы, описанные в таблице 18-8, для
сортировки указанных данных.

Stable(data)

Эта функция использует методы, описанные в таблице 18-8, для
сортировки указанных данных без изменения порядка элементов с
одинаковым значением.

IsSorted(data) Эта функция возвращает значение true, если данные отсортированы.
Reverse(data)

Эта функция меняет порядок данных.

Методы, определенные в таблице 18-8, применяются к набору
элементов данных, подлежащих сортировке, что означает введение
псевдонима типа и функций, которые выполняют преобразования для
вызова функций, определенных в таблице 18-9. Для демонстрации
добавьте файл с именем productsort.go в папку mathandsorting с
кодом, показанным в листинге 18-15.
package main
import "sort"
type Product struct {
Name string
Price float64
}
type ProductSlice []Product
func ProductSlices(p []Product) {
sort.Sort(ProductSlice(p))
}
func ProductSlicesAreSorted(p []Product) {
sort.IsSorted(ProductSlice(p))
}
func (products ProductSlice) Len() int {
return len(products)
}
func (products ProductSlice) Less(i, j int) bool {
return products[i].Price < products[j].Price

}
func (products ProductSlice) Swap(i, j int) {
products[i], products[j] = products[j], products[i]
}
Листинг 18-15 Содержимое файла productsort.go в папке mathandsorting

Тип ProductSlice является псевдонимом для среза Product и
является типом, для которого были реализованы методы интерфейса. В
дополнение к методам у меня есть функция ProductSlices, которая
принимает срез Product, преобразует его в тип ProductSlice и
передает в качестве аргумента функции Sort. Существует также
функция ProductSlicesAreSorted, которая вызывает функцию
IsSorted. Имена этой функции следуют соглашению, установленному
пакетом sort: после имени псевдонима следует буква s. В листинге
18.16 эти функции используются для сортировки среза значений
Product.
package main
import (
//"sort"
)
func main() {
products := []Product {
{ "Kayak", 279} ,
{ "Lifejacket", 49.95 },
{ "Soccer Ball", 19.50 },
}
ProductSlices(products)
for _, p := range products {
Printfln("Name: %v, Price: %.2f", p.Name, p.Price)
}
}
Листинг 18-16 Сортировка среза в файле main.go в папке mathandsorting

Скомпилируйте и выполните проект, и вы увидите, что выходные
данные показывают значения Product, отсортированные в порядке
возрастания поля Price:
Name: Soccer Ball, Price: 19.50
Name: Lifejacket, Price: 49.95
Name: Kayak, Price: 279.00

Сортировка с использованием разных полей
Композицию типов можно использовать для поддержки сортировки
одного и того же типа структуры с использованием разных полей, как
показано в листинге 18-17.
package main
import "sort"
type Product struct {
Name string
Price float64
}
type ProductSlice []Product
func ProductSlices(p []Product) {
sort.Sort(ProductSlice(p))
}
func ProductSlicesAreSorted(p []Product) {
sort.IsSorted(ProductSlice(p))
}
func (products ProductSlice) Len() int {
return len(products)
}
func (products ProductSlice) Less(i, j int) bool {
return products[i].Price < products[j].Price
}
func (products ProductSlice) Swap(i, j int) {

products[i], products[j] = products[j], products[i]
}
type ProductSliceName struct { ProductSlice }
func ProductSlicesByName(p []Product) {
sort.Sort(ProductSliceName{ p })
}
func (p ProductSliceName) Less(i, j int) bool {
return p.ProductSlice[i].Name < p.ProductSlice[j].Name
}
Листинг 18-17 Сортировка различных полей в файле productsort.go в папке mathandsorting

Тип структуры определяется для каждого поля структуры, для
которого требуется сортировка, со встроенным полем ProductSlice,
подобным этому:
...
type ProductSliceName struct { ProductSlice }
...
Функция композиции типа означает, что методы, определенные для
типа ProductSlice, повышаются до включающего типа. Определен
новый метод Less для включающего типа, который будет
использоваться для сортировки данных с использованием другого
поля, например:
...
func (p ProductSliceName) Less(i, j int) bool {
return p.ProductSlice[i].Name 28-21.
Отражение нельзя использовать для установки неэкспортированных полей
структуры, поэтому мне нужно выполнить дополнительную проверку, чтобы
избежать паники, пытаясь установить поле, которое никогда не может быть
установлено. (На самом деле, есть некоторые обходные пути для установки
неэкспортируемых полей, но они неприятны, и я не рекомендую их
использовать. Поиск в Интернете даст вам необходимую информацию, если
вы решили установить неэкспортируемые поля.)

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

29. Использование отражения, часть 3
В этой главе я завершаю описание поддержки отражения в Go, которое
я начал в главе 27 и продолжил в главе 28. В этой главе я объясню, как
отражение используется для функций, методов, интерфейсов и
каналов. Таблица 29-1 суммирует содержание главы.
Таблица 29-1 Краткое содержание главы
Проблема

Решение

Листинг

Проверить и вызывать отраженные
функции

Используйте методы Type и Value для
функций

5–7

Создание новых функций

Используйте функции FuncOf и MakeFunc.

8, 9

Проверить и вызвать отраженные
методы

Используйте методы Type и Value для
методов

10–12

Проверить отраженные интерфейсы

Используйте методы Type и Value для
интерфейсов

13–15

Проверить и использовать
отраженные каналы

Используйте методы Type и Value для
каналов

16–19

Подготовка к этой главе
В этой главе я продолжаю использовать проект reflection из главы
28. Чтобы подготовиться к этой главе, добавьте в проект reflection
файл с именем interfaces.go с содержимым, показанным в листинге
29-1.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/progo. См. Главу 2 о том, как получить помощь, если у вас возникнут
проблемы с запуском примеров.
package main

import "fmt"
type NamedItem interface {
GetName() string
unexportedMethod()
}
type CurrencyItem interface {
GetAmount() string
currencyName() string
}
func (p *Product) GetName() string {
return p.Name
}
func (c *Customer) GetName() string {
return c.Name
}
func (p *Product) GetAmount() string {
return fmt.Sprintf("$%.2f", p.Price)
}
func (p *Product) currencyName() string {
return "USD"
}
func (p *Product) unexportedMethod() {}
Листинг 29-1 Содержимое файла interfaces.go в папке reflection

Добавьте файл с именем functions.go в папку отражения с
содержимым, показанным в листинге 29-2.
package main
func Find(slice []string, vals... string) (matches bool) {
for _, s1 := range slice {
for _, s2 := range vals {
if s1 == s2 {
matches = true
return

}
}
}

}
return
Листинг 29-2 Содержимое файла functions.go в папке reflection

Добавьте файл с именем method.go в папку отражения с
содержимым, показанным в листинге 29-3.
package main
func (p Purchase) calcTax(taxRate float64) float64 {
return p.Price * taxRate
}
func (p Purchase) GetTotal() float64 {
return p.Price + p.calcTax(.20)
}
Листинг 29-3 Содержимое файла method.go в папке reflection

Запустите команду, показанную в листинге 29-4, в папке
reflection, чтобы скомпилировать и выполнить проект.
go run .
Листинг 29-4 Компиляция и выполнение проекта

Эта команда производит следующий вывод:
Name:
Name:
Name:
Name:

Customer, Type: main.Customer, Value: {Acme London}
Product, Type: main.Product, Value: {Kayak Boats 279}
Total, Type: float64, Value: 100.5
taxRate, Type: float64, Value: 10

Работа с типами функций
Как объяснялось в главе 9, функции в Go являются типами, и, как вы
могли ожидать, функции можно исследовать и использовать с
отражением. Структура Type определяет методы, которые можно
использовать для проверки типов функций, описанных в таблице 29-2.

Таблица 29-2 Методы Type для работы с функциями
Функция

Описание

NumIn()

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

In(index)

Этот метод возвращает Type, который отражает параметр по указанному
индексу.

IsVariadic() Этот метод возвращает значение true, если последний параметр является
вариативным.
NumOut()

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

Out(index)

Этот метод возвращает Type, который отражает результат по указанному
индексу.

В листинге 29-5 отражение используется для описания функции.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func inspectFuncType(f interface{}) {
funcType := reflect.TypeOf(f)
if (funcType.Kind() == reflect.Func) {
Printfln("Function parameters: %v", funcType.NumIn())
for i := 0 ; i < funcType.NumIn(); i++ {
paramType := funcType.In(i)
if (i < funcType.NumIn() -1) {
Printfln("Parameter #%v, Type: %v", i,
paramType)
} else {
Printfln("Parameter #%v, Type: %v, Variadic:
%v", i, paramType,
funcType.IsVariadic())
}
}
Printfln("Function results: %v", funcType.NumOut())
for i := 0 ; i < funcType.NumOut(); i++ {
resultType := funcType.Out(i)
Printfln("Result #%v, Type: %v", i, resultType)
}

}

}

func main() {
inspectFuncType(Find)
}
Листинг 29-5 Отражение функции в файле main.go в папке reflection

Функция inspectFuncType использует методы, описанные в
таблице 29-2, для проверки типа функции, сообщая о ее параметрах и
результатах. Скомпилируйте и выполните проект, и вы увидите
следующий вывод, описывающий функцию Find, определенную в
листинге 29-2:
Parameter #0, Type: []string
Parameter #1, Type: []string, Variadic: true
Function results: 1
Result #0, Type: bool
Выходные данные показывают, что функция Find имеет два
параметра, последний из которых является переменным, и один
результат.

Работа со значениями функций
Интерфейс Value определяет описанный в таблице 29-3 метод вызова
функций.
Таблица 29-3 Метод Value для вызова функций
Функция

Описание

Call(params) Эта функция вызывает отраженную функцию, используя []Value в качестве
параметров. Результатом является значение []Value, содержащее результаты
функции. Значения, предоставляемые в качестве параметров, должны
соответствовать значениям, определенным функцией.

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

Результаты возвращаются в виде другого среза Value, как показано в
листинге 29-6.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func invokeFunction(f interface{}, params ...interface{}) {
paramVals := []reflect.Value {}
for _, p := range params {
paramVals = append(paramVals, reflect.ValueOf(p))
}
funcVal := reflect.ValueOf(f)
if (funcVal.Kind() == reflect.Func) {
results := funcVal.Call(paramVals)
for i, r := range results {
Printfln("Result #%v: %v", i, r)
}
}
}
func main() {
names := []string { "Alice", "Bob", "Charlie" }
invokeFunction(Find, names, "London", "Bob")
}
Листинг 29-6 Вызов функции в файле main.go в папке reflection

Скомпилируйте и запустите проект, и вы увидите следующий
вывод:
Result #0: true
Вызов функции таким образом не является обычным требованием,
потому что вызывающий код мог просто вызвать функцию напрямую,
но этот пример делает использование метода Call понятным и
подчеркивает, что параметры и результаты выражаются с помощью
срезов Value. В листинге 29-7 приведен более реалистичный пример.

package main
import (
"reflect"
"strings"
//"fmt"
)
func mapSlice(slice interface{}, mapper interface{}) (mapped
[]interface{}) {
sliceVal := reflect.ValueOf(slice)
mapperVal := reflect.ValueOf(mapper)
mapped = []interface{} {}
if sliceVal.Kind() == reflect.Slice && mapperVal.Kind()
== reflect.Func &&
mapperVal.Type().NumIn() == 1 &&
mapperVal.Type().In(0) == sliceVal.Type().Elem()
{
for i := 0; i < sliceVal.Len(); i++ {
result := mapperVal.Call([]reflect.Value
{sliceVal.Index(i)})
for _, r := range result {
mapped = append(mapped, r.Interface())
}
}
}
return
}
func main() {
names := []string { "Alice", "Bob", "Charlie" }
results := mapSlice(names, strings.ToUpper)
Printfln("Results: %v", results)
}
Листинг 29-7 Вызов функции для элементов среза в файле main.go в папке reflection

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

mapper func(interface{}) interface{}
...
Проблема с этим подходом заключается в том, что он ограничивает
функции, которые можно использовать, теми, которые определены с
параметрами и результатами, которые являются пустым интерфейсом.
Вместо этого укажите всю функцию как одно пустое значение
интерфейса, например:
...
func mapSlice(slice interface{}, mapper interface{}) (mapped
[]interface{}) {
...
Это позволяет использовать любую функцию, но требует проверки
функции, чтобы убедиться, что ее можно использовать по назначению:
...
if sliceVal.Kind() == reflect.Slice && mapperVal.Kind() ==
reflect.Func &&
mapperVal.Type().NumIn() == 1 &&
mapperVal.Type().In(0) == sliceVal.Type().Elem() {
...
Эти проверки гарантируют, что функция определяет один параметр
и что тип параметра соответствует типу элемента среза.
Скомпилируйте и запустите проект, и вы увидите следующие
результаты:
Results: [ALICE BOB CHARLIE]

Создание и вызов новых типов функций и значений
Пакет reflect определяет функции, описанные в таблице 29-4, для
создания новых типов функций и значений.
Таблица 29-4 Функция reflect для создания новых типов функций и значений функций
Функция

Описание

Функция

Описание

FuncOf(params,
results, variadic)

Эта функция создает новый Type, который отражает тип функции с
указанными параметрами и результатами. Последний аргумент указывает,
имеет ли тип функции переменный параметр. Параметры и результаты
указаны как срезы Type.

MakeFunc(type, Эта функция возвращает Value, отражающее новую функцию,
fn)
являющуюся оболочкой функции fn. Функция должна принимать срез
Value в качестве единственного параметра и возвращать срез Value в
качестве единственного результата.

Одним из применений функции FuncOf является создание
сигнатуры типа и ее использование для проверки сигнатуры значения
функции, заменяющей проверки, выполненные в предыдущем разделе,
как показано в листинге 29-8.
package main
import (
"reflect"
"strings"
//"fmt"
)
func mapSlice(slice interface{}, mapper interface{}) (mapped
[]interface{}) {
sliceVal := reflect.ValueOf(slice)
mapperVal := reflect.ValueOf(mapper)
mapped = []interface{} {}
if sliceVal.Kind() == reflect.Slice && mapperVal.Kind()
== reflect.Func {
paramTypes := []reflect.Type { sliceVal.Type().Elem()
}
resultTypes := []reflect.Type {}
for i := 0; i < mapperVal.Type().NumOut(); i++ {
resultTypes = append(resultTypes,
mapperVal.Type().Out(i))
}
expectedFuncType := reflect.FuncOf(paramTypes,
resultTypes, mapperVal.Type().IsVariadic())
if (mapperVal.Type() == expectedFuncType) {
for i := 0; i < sliceVal.Len(); i++ {

result := mapperVal.Call([]reflect.Value
{sliceVal.Index(i)})
for _, r := range result {
mapped = append(mapped, r.Interface())
}
}
} else {
Printfln("Function type not as expected")
}
}
return
}
func main() {
names := []string { "Alice", "Bob", "Charlie" }
results := mapSlice(names, strings.ToUpper)
Printfln("Results: %v", results)
}
Листинг 29-8 Создание типа функции в файле main.go в папке reflection Folder

Этот подход не менее многословен, не в последнюю очередь
потому, что я хочу принимать функции с тем же типом параметра, что
и тип элемента среза, но с любым типом результата. Получить тип
элемента среза несложно, но мне нужно проделать некоторую работу,
чтобы создать срез Type, отражающий результаты функции
сопоставления, чтобы убедиться, что я создаю тип, который будет
корректно сравниваться. Скомпилируйте и запустите проект, и вы
увидите следующий вывод:
Results: [ALICE BOB CHARLIE]
Функция FuncOf дополняется функцией MakeFunc, которая создает
новые функции, используя тип функции в качестве шаблона. В
листинге 29-9 показано использование функции MakeFunc для создания
повторно используемой функции отображения типов.
package main
import (
"reflect"
"strings"

)

"fmt"

func makeMapperFunc(mapper interface{}) interface{} {
mapVal := reflect.ValueOf(mapper)
if mapVal.Kind() == reflect.Func && mapVal.Type().NumIn()
== 1 &&
mapVal.Type().NumOut() == 1 {
inType := reflect.SliceOf( mapVal.Type().In(0))
inTypeSlice := []reflect.Type { inType }
outType := reflect.SliceOf( mapVal.Type().Out(0))
outTypeSlice := []reflect.Type { outType }
funcType := reflect.FuncOf(inTypeSlice, outTypeSlice,
false)
funcVal := reflect.MakeFunc(funcType,
func (params []reflect.Value) (results
[]reflect.Value) {
srcSliceVal := params[0]
resultsSliceVal := reflect.MakeSlice(outType,
srcSliceVal.Len(), 10)
for i := 0; i < srcSliceVal.Len(); i++ {
r := mapVal.Call([]reflect.Value {
srcSliceVal.Index(i)})
resultsSliceVal.Index(i).Set(r[0])
}
results = []reflect.Value { resultsSliceVal }
return
})
return funcVal.Interface()
}
Printfln("Unexpected types")
return nil
}
func main() {
lowerStringMapper := makeMapperFunc(strings.ToLower).
(func([]string)[]string)
names := []string { "Alice", "Bob", "Charlie" }
results := lowerStringMapper(names)
Printfln("Lowercase Results: %v", results)

incrementFloatMapper := makeMapperFunc(func (val float64)
float64 {
return val + 1
}).(func([]float64)[]float64)
prices := []float64 { 279, 48.95, 19.50}
floatResults := incrementFloatMapper(prices)
Printfln("Increment Results: %v", floatResults)
floatToStringMapper := makeMapperFunc(func (val float64)
string {
return fmt.Sprintf("$%.2f", val)
}).(func([]float64)[]string)
Printfln("Price
Results:
%v",
floatToStringMapper(prices))
}
Листинг 29-9 Создание функции в файле main.go в папке reflection

Функция makeMapperFunc демонстрирует, насколько гибким может
быть рефлексия, но также показывает, насколько многословным и
плотным она может быть. Лучший способ понять эту функцию —
сосредоточиться на входах и выходах. makeMapperFunc принимает
функцию, которая преобразует одно значение в другое, с такой
сигнатурой:
...
func mapper(int) string
...
Эта гипотетическая функция получает значение типа int и
возвращает string результат. makeMapperFunc использует типы этой
функции для создания функции, которая будет выражена следующим
образом в обычном коде Go:
...
func useMapper(slice []int) []string {
results := []string {}
for _, val := range slice {
results = append(results, mapper(val))
}
return results
}

...
Функция useMapper представляет собой оболочку для функции
mapper. Функции mapper и useMapper легко определить в обычном
коде Go, но они специфичны для одного набора типов.
makeMapperFunc использует отражение, поэтому может принимать
любую функцию сопоставления и генерировать соответствующую
оболочку, которую затем можно использовать со стандартными
функциями безопасности типа Go.
Первым шагом является определение типов функции отображения:
...
inType := reflect.SliceOf( mapVal.Type().In(0))
inTypeSlice := []reflect.Type { inType }
outType := reflect.SliceOf( mapVal.Type().Out(0))
outTypeSlice := []reflect.Type { outType }
...
Затем эти типы используются для создания типа функции для
оболочки:
...
funcType := reflect.FuncOf(inTypeSlice, outTypeSlice, false)
...
Получив тип функции, я могу использовать его для создания
функции-оболочки с помощью функции MakeFunc:
...
funcVal := reflect.MakeFunc(funcType,
func (params []reflect.Value) (results []reflect.Value) {
...
Функция MakeFunc принимает Type, описывающий функцию, и
функцию, которую будет вызывать новая функция. В листинге 29-9
функция перечисляет элементы в срезе, вызывает функцию
сопоставления для каждого из них и создает срез результатов.
Результатом является функция, безопасная для типов, хотя она
требует утверждения типа:

...
lowerStringMapper
:=
(func([]string)[]string)
...

makeMapperFunc(strings.ToLower).

Функция makeMapperFunc получает функцию strings.ToLower и
создает функцию, которая принимает срез строки и возвращает срез
строк. Другие вызовы makeMapperFunc создают функции, которые
преобразуют значения float64 в другие значения float64 и
преобразуют значения float64 в строки денежного формата.
Скомпилируйте и запустите проект, и вы увидите следующий вывод:
Lowercase Results: [alice bob charlie]
Increment Results: [280 49.95 20.5]
Price Results: [$279.00 $48.95 $19.50]

Работа с методами
Структура Type определяет методы, описанные в таблице 29-5, для
проверки методов, определенных структурой.
Таблица 29-5 Методы Type для работы с методами
Функция

Описание

NumMethod()

Этот метод возвращает количество экспортированных методов,
определенных для отражаемого типа структуры.

Method(index)

Этот метод возвращает отраженный метод по указанному индексу,
представленному структурой Method.

MethodByName(name) Этот метод возвращает отраженный метод с указанным именем.
Результатами являются структура Method и bool значение,
указывающее, существует ли метод с указанным именем.

Примечание
Рефлексия не поддерживает создание новых методов. Ее можно
использовать только для проверки и вызова существующих методов.
Методы представлены структурой Method, которая определяет поля,
описанные в таблице 29-6.

Таблица 29-6 Поля, определяемые структурой Method
Функция Описание
Name

Это поле возвращает имя метода в виде строки.

PkgPath

Это поле используется с интерфейсами, как описано в разделе «Работа с
интерфейсами», а не с методами, доступ к которым осуществляется через тип
структуры. Поле возвращает string, содержащую путь к пакету. Пустая строка
используется для экспортируемых полей и будет содержать имя пакета структуры
для неэкспортированных полей.

Type

Это поле возвращает Type, описывающий тип функции метода.

Func

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

Index

Это поле возвращает int, указывающее индекс метода для использования с
методом Method, описанным в таблице 29-5.

Примечание
При проверке структур методы, которые продвигаются из
встроенных полей, включаются в результаты, полученные
методами, описанными в этом разделе.
Интерфейс Value также определяет методы для работы с отраженными
методами, как описано в таблице 29-7.
Таблица 29-7 Метод Value для работы с методами
Функция

Описание

NumMethod()

Этот метод возвращает количество экспортированных методов,
определенных для отражаемого типа структуры. Он вызывает метод
Type.NumMethod.

Method(index)

Этот метод возвращает Value, которое отражает функцию метода по
указанному индексу. Получатель не указывается в качестве первого
аргумента при вызове функции, как показано в разделе «Вызов
методов».

MethodByName(name) Этот метод возвращает Value, которое отражает функцию метода с
указанным именем. Получатель не указывается в качестве первого
аргумента при вызове функции, как показано в разделе «Вызов
методов».

Методы в таблице 29-7 — это удобные функции, которые
обеспечивают доступ к тем же базовым функциям, что и методы в

таблице 29-5, хотя существуют различия в том, как методы
вызываются, как описано в следующем разделе.
В листинге 29-10 определяется функция, описывающая методы,
определенные
структурой,
с
использованием
методов,
предоставленных структурой Type.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func inspectMethods(s interface{}) {
sType := reflect.TypeOf(s)
if sType.Kind() == reflect.Struct || (sType.Kind() ==
reflect.Ptr &&
sType.Elem().Kind() == reflect.Struct) {
Printfln("Type: %v, Methods: %v", sType,
sType.NumMethod())
for i := 0; i < sType.NumMethod(); i++ {
method := sType.Method(i)
Printfln("Method name: %v, Type: %v",
method.Name, method.Type)
}
}
}
func main() {

}

inspectMethods(Purchase{})
inspectMethods(&Purchase{})
Листинг 29-10 Описание методов в файле main.go в папке reflection

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

Type: main.Purchase, Methods: 1
Method name: GetTotal, Type: func(main.Purchase) float64
Type: *main.Purchase, Methods: 2
Method name: GetAmount, Type: func(*main.Purchase) string
Method name: GetTotal, Type: func(*main.Purchase) float64
Когда для типа Purchase используется отражение, перечисляются
только методы, определенные для Product. Но когда отражение
используется для типа *Purchase, перечисляются методы,
определенные для Product и *Product. Обратите внимание, что через
отражение доступны только экспортированные методы —
неэкспортированные методы нельзя проверить или вызвать.

Вызов методов
Структура Method определяет поле Func, которое возвращает Value,
которое можно использовать для вызова метода, используя тот же
подход, описанный ранее в этой главе, как показано в листинге 29-11.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func executeFirstVoidMethod(s interface{}) {
sVal := reflect.ValueOf(s)
for i := 0; i < sVal.NumMethod(); i++ {
method := sVal.Type().Method(i)
if method.Type.NumIn() == 1 {
results := method.Func.Call([]reflect.Value{ sVal
})
Printfln("Type: %v, Method: %v, Results: %v",
sVal.Type(), method.Name, results)
break
} else {
Printfln("Skipping method %v %v", method.Name,
method.Type.NumIn())
}
}

}
func main() {
executeFirstVoidMethod(&Product { Name: "Kayak", Price:
279})
}
Листинг 29-11 Вызов метода в файле main.go в папке reflection

Функция
executeFirstVoidMethod
перечисляет
методы,
определенные типом параметра, и вызывает первый метод,
определяющий один параметр. При вызове метода через поле
Method.Func первым аргументом должен быть получатель, то есть
значение структуры, для которой будет вызываться метод:
...
results := method.Func.Call([]reflect.Value{ sVal })
...
Это означает, что при поиске метода с одним параметром
выбирается метод, не принимающий аргументов, что можно увидеть в
результатах, полученных при компиляции и выполнении проекта:
Type: *main.Product, Method: GetAmount, Results: [$279.00]
Метод executeFirstVoidMethod выбрал метод GetAmount.
Получатель не указывается, когда метод вызывается через интерфейс
Value, как показано в листинге 29-12.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func executeFirstVoidMethod(s interface{}) {
sVal := reflect.ValueOf(s)
for i := 0; i < sVal.NumMethod(); i++ {
method := sVal.Method(i)
if method.Type().NumIn() == 0 {

results)

results := method.Call([]reflect.Value{})
Printfln("Type: %v, Method: %v, Results: %v",
sVal.Type(), sVal.Type().Method(i).Name,

break
} else {
Printfln("Skipping method %v %v",
sVal.Type().Method(i).Name,
method.Type().NumIn())
}
}
}
func main() {
executeFirstVoidMethod(&Product { Name: "Kayak", Price:
279})
}
Листинг 29-12 Вызов метода через значение в файле main.go в папке reflection

Чтобы найти метод, который я могу вызвать без дополнительных
аргументов, я должен искать нулевые параметры, так как получатель
явно не указан. Вместо этого получатель определяется из Value, для
которого вызывается метод Call:
...
results := method.Call([]reflect.Value{})
...
Этот пример выдает тот же результат, что и код в листинге 29-11.

Работы с интерфейсами
Структура Type определяет методы, которые можно использовать для
проверки типов интерфейсов, описанных в таблице 29-8. Большинство
этих методов также можно применять к структурам, как показано в
предыдущем разделе, но поведение немного отличается.
Таблица 29-8 Методы Type Methods для интерфейсов
Функция

Описание

Функция

Описание

Implements(type)

Этот метод возвращает значение true, если отраженное значение
реализует указанный интерфейс, который также представлен Value.

Elem()

Этот метод возвращает Value, которое отражает значение,
содержащееся в интерфейсе.

NumMethod()

Этот метод возвращает количество экспортированных методов,
определенных для отражаемого типа структуры.

Method(index)

Этот метод возвращает отраженный метод по указанному индексу,
представленному структурой Method.

MethodByName(name) Этот метод возвращает отраженный метод с указанным именем.
Результатами являются структура Method и bool значение,
указывающее, существует ли метод с указанным именем.

Следует соблюдать осторожность при использовании отражения
для интерфейсов, поскольку пакет reflect всегда начинается со
значения и будет пытаться работать с базовым типом этого значения.
Самый простой способ решить эту проблему — преобразовать
значение nil, как показано в листинге 29-13.
package main
import (
"reflect"
//"strings"
//"fmt"
)
func
checkImplementation(check
interface{},
targets
...interface{}) {
checkType := reflect.TypeOf(check)
if (checkType.Kind() == reflect.Ptr &&
checkType.Elem().Kind() == reflect.Interface) {
checkType := checkType.Elem()
for _, target := range targets {
targetType := reflect.TypeOf(target)
Printfln("Type %v implements %v: %v",
targetType, checkType,
targetType.Implements(checkType))
}
}
}

func main() {
currencyItemType := (*CurrencyItem)(nil)
checkImplementation(currencyItemType,
&Product{}, &Purchase{})
}

Product{},

Листинг 29-13 Отражение интерфейса в файле main.go в папке reflection

Чтобы указать интерфейс, который я хочу
конвертирую nil в указатель интерфейса, например:

проверить,

я

...
currencyItemType := (*CurrencyItem)(nil)
...
Это необходимо сделать с помощью указателя, который затем
следует в функции checkImplementation с помощью метода Elem,
чтобы получить Type, отражающий интерфейс, которым в этом
примере является CurrencyItem:
...
if (checkType.Kind() == reflect.Ptr &&
checkType.Elem().Kind() == reflect.Interface) {
checkType := checkType.Elem()
...
После этого легко проверить, реализует ли тип интерфейс,
используя метод Implements. Скомпилируйте и запустите проект, и вы
увидите следующий вывод:
Type main.Product implements main.CurrencyItem: false
Type *main.Product implements main.CurrencyItem: true
Type *main.Purchase implements main.CurrencyItem: true
Вывод показывает, что структура Product не реализует интерфейс,
а *Product реализует, потому что *Product — это тип получателя,
используемый для реализации методов, необходимых для
CurrencyItem. Тип *Purchase также реализует интерфейс, поскольку
он имеет вложенные поля структуры, определяющие необходимые
методы.

Получение базовых значений из интерфейсов
Хотя рефлексия обычно создает конкретные типы, бывают случаи,
когда необходимо использовать метод Elem для перехода от
интерфейса к типу, который его реализует, как показано в листинге 2914.
package main
import (
"reflect"
//"strings"
//"fmt"
)
type Wrapper struct {
NamedItem
}
func getUnderlying(item Wrapper, fieldName string) {
itemVal := reflect.ValueOf(item)
fieldVal := itemVal.FieldByName(fieldName)
Printfln("Field Type: %v", fieldVal.Type())
if (fieldVal.Kind() == reflect.Interface) {
Printfln("Underlying
Type:
fieldVal.Elem().Type())
}
}
func main() {

getUnderlying(Wrapper{NamedItem:

%v",

&Product{}},

"NamedItem")
}
Листинг 29-14 Получение базовых значений интерфейса в файле main.go в папке reflection

Тип Wrapper определяет вложенное поле NamedItem. Функция
getUnderlying использует рефлексию для получения поля и
записывает тип поля и базовый тип, полученный с помощью метода
Elem. Скомпилируйте и запустите проект, и вы увидите следующие
результаты:
Field Type: main.NamedItem

Underlying Type: *main.Product
Тип поля — это интерфейс NamedItem, но метод Elem показывает,
что базовое значение, присвоенное полю NamedItem, — это *Product.

Изучение методов интерфейса
Методы NumMethod, Method и MethodByName можно использовать для
интерфейсных типов, но результаты включают неэкспортированные
методы, чего нельзя сказать о непосредственном исследовании типа
структуры, как показано в листинге 29-15.
package main
import (
"reflect"
//"strings"
//"fmt"
)
type Wrapper struct {
NamedItem
}
func getUnderlying(item Wrapper, fieldName string) {
itemVal := reflect.ValueOf(item)
fieldVal := itemVal.FieldByName(fieldName)
Printfln("Field Type: %v", fieldVal.Type())
for i := 0; i < fieldVal.Type().NumMethod(); i++ {
method := fieldVal.Type().Method(i)
Printfln("Interface Method: %v, Exported: %v",
method.Name, method.PkgPath == "")
}
Printfln("--------")
if (fieldVal.Kind() == reflect.Interface) {
Printfln("Underlying
Type:
%v",
fieldVal.Elem().Type())
for i := 0; i < fieldVal.Elem().Type().NumMethod();
i++ {
method := fieldVal.Elem().Type().Method(i)
Printfln("Underlying Method: %v", method.Name)
}

}

}

func main() {

getUnderlying(Wrapper{NamedItem:

&Product{}},

"NamedItem")
}
Листинг 29-15 Изучение методов интерфейса в файле main.go в папке reflection Folder

Изменения записывают детали методов, полученных из интерфейса
и базовых типов. Скомпилируйте и запустите проект, и вы увидите
следующий вывод:
Field Type: main.NamedItem
Interface Method: GetName, Exported: true
Interface Method: unexportedMethod, Exported: false
-------Underlying Type: *main.Product
Underlying Method: GetAmount
Underlying Method: GetName
Список методов для интерфейса NamedItem включает
unexportedMethod, которого нет в списке для *Product. Существуют
дополнительные методы, определенные для *Product помимо тех,
которые требуются для интерфейса, поэтому метод GetAmount
отображается в выходных данных.
Методы можно вызывать через интерфейс, но перед
использованием метода Call необходимо убедиться, что они
экспортированы. Если вы попытаетесь вызвать неэкспортированный
метод, Call вызовет панику.

Работа с типами каналов
Структура Type определяет методы, которые можно использовать для
проверки типов каналов, описанных в таблице 29-9.
Таблица 29-9 Методы Type для каналов
Функция Описание

Функция Описание
ChanDir() Этот метод возвращает значение ChanDir, которое описывает направление
канала, используя одно из значений, показанных в таблице 29-10.
Elem()

Этот метод возвращает Type, который отражает тип, переносимый каналом.

Результат ChanDir, возвращаемый методом ChanDir, указывает
направление канала, которое можно сравнить с одной из констант
пакета reflect, описанных в таблице 29-10.
Таблица 29-10 Значения ChanDir
Функция Описание
RecvDir

Это значение указывает, что канал можно использовать для приема данных. При
выражении в виде строки это значение возвращает = minVal
} else if fVal, fValOk := value.(float64); fValOk {
valid = fVal >= float64(minVal)
} else if strVal, strValOk := value.(string); strValOk {

err = fmt.Errorf("The minimum length is %v characters", minVal)
valid = len(strVal) >= minVal
} else {
err = errors.New("The min validator is for int, float64, and str
values")
}
return
}
Листинг 34-24 Содержимое файла validator_functions.go в папке validation

Для выполнения проверки каждая функция получает имя проверяемого поля
структуры, значение, полученное из запроса, и необязательные аргументы, которые
настраивают процесс проверки. Чтобы создать реализацию и функции, которые будут
настраивать службу, добавьте файл с именем tag_validator.go в папку
platform/validation с содержимым, показанным в листинге 34-25.
package validation
import (
"reflect"
"strings"
)
func NewDefaultValidator(validators map[string]ValidatorFunc) Validator {
return &TagValidator{ DefaultValidators() }
}
type TagValidator struct {
validators map[string]ValidatorFunc
}
func (tv *TagValidator) Validate(data interface{}) (ok bool,
errs []ValidationError) {
errs = []ValidationError{}
dataVal := reflect.ValueOf(data)
if (dataVal.Kind() == reflect.Ptr) {
dataVal = dataVal.Elem()
}
if (dataVal.Kind() != reflect.Struct) {
panic("Only structs can be validated")
}
for i := 0; i < dataVal.NumField(); i++ {
fieldType := dataVal.Type().Field(i)
validationTag, found := fieldType.Tag.Lookup("validation")
if found {
for _, v := range strings.Split(validationTag, ",") {
var name, arg string = "", ""
if strings.Contains(v, ":") {
nameAndArgs := strings.SplitN(v, ":", 2)
name = nameAndArgs[0]
arg = nameAndArgs[1]

}

}

} else {
name = v
}
if validator, ok := tv.validators[name]; ok {
valid, err := validator(fieldType.Name,
dataVal.Field(i).Interface(), arg )
if (!valid) {
errs = append(errs, ValidationError{
FieldName: fieldType.Name,
Error: err,
})
}
} else {
panic("Unknown validator: " + name)
}

}
}
ok = len(errs) == 0
return
Листинг 34-25 Содержимое файла tag_validator.go в папке validation

Структура TagValidator реализует интерфейс Validator, ища тег структуры с
именем validation и анализируя его, чтобы увидеть, какая проверка требуется для
каждого поля структуры. Используется каждый указанный валидатор, а ошибки
собираются и возвращаются как результат метода Validate. Функция
NewDefaultValidation создает экземпляр структуры и используется для создания
службы проверки, как показано в листинге 34-26.
package services
import (
"platform/logging"
"platform/config"
"platform/templates"
"platform/validation"
)
func RegisterDefaultServices() {
// ...statements omitted for brevity...
err = AddSingleton(
func() validation.Validator {
validation.NewDefaultValidator(validation.DefaultValidators())
})
if (err != nil) {
panic(err)
}

return

}
Листинг 34-26 Регистрация службы проверки в файле services_default.go в папке services

Я зарегистрировал новую службу как
возвращаемые функцией DefaultValidators.

синглтон,

используя

валидаторы,

Выполнение проверки данных
Требуется некоторая подготовка, чтобы убедиться, что проверка данных работает. Вопервых, в листинге 34-27 создается новый метод обработчика и применяется тег
структуры проверки к обработчику запроса-заполнителя.
package placeholder
import (
"fmt"
"platform/logging"
"platform/http/actionresults"
"platform/http/handling"
"platform/validation"
)
var names = []string{"Alice", "Bob", "Charlie", "Dora"}
type NameHandler struct {
logging.Logger
handling.URLGenerator
validation.Validator
}
func (n NameHandler) GetName(i int) actionresults.ActionResult {
n.Logger.Debugf("GetName method invoked with argument: %v", i)
var response string
if (i < len(names)) {
response = fmt.Sprintf("Name #%v: %v", i, names[i])
} else {
response = fmt.Sprintf("Index out of bounds")
}
return actionresults.NewTemplateAction("simple_message.html",
response)
}
func (n NameHandler) GetNames() actionresults.ActionResult {
n.Logger.Debug("GetNames method invoked")
return actionresults.NewTemplateAction("simple_message.html", names)
}
type NewName struct {
Name string `validation:"required,min:3"`
InsertAtStart bool
}

func (n NameHandler) GetForm() actionresults.ActionResult {
postUrl, _ := n.URLGenerator.GenerateUrl(NameHandler.PostName)
return actionresults.NewTemplateAction("name_form.html", postUrl)
}
func (n NameHandler) PostName(new NewName) actionresults.ActionResult {
n.Logger.Debugf("PostName method invoked with argument %v", new)
if ok, errs := n.Validator.Validate(&new); !ok {
return actionresults.NewTemplateAction("validation_errors.html",
errs)
}
if (new.InsertAtStart) {
names = append([] string { new.Name}, names... )
} else {
names = append(names, new.Name)
}
return n.redirectOrError(NameHandler.GetNames)
}
func (n NameHandler) GetRedirect() actionresults.ActionResult {
return n.redirectOrError(NameHandler.GetNames)
}
func (n NameHandler) GetJsonData() actionresults.ActionResult {
return actionresults.NewJsonAction(names)
}
func (n NameHandler) redirectOrError(handler interface{},
data ...interface{}) actionresults.ActionResult {
url, err := n.GenerateUrl(handler)
if (err == nil) {
return actionresults.NewRedirectAction(url)
} else {
return actionresults.NewErrorAction(err)
}
}
Листинг 34-27 Подготовка к проверке в файле name_handler.go в папке placeholder

Тег проверки был добавлен в поле Name, применяя required и min валидаторы, что
означает, что требуется значение с минимальным количеством трех символов. Чтобы
упростить проверку проверки, я добавил метод-обработчик с именем GetForm,
который отображает шаблон с именем name_form.html. Когда данные получены
методом PostName, они проверяются с помощью службы, а шаблон
validation_errors.html используется для формирования ответа при наличии
ошибок проверки.
Добавьте файл с именем name_form.html в папку-заполнитель с содержимым,
показанным в листинге 34-28.
{{ layout "layout.html" }}



Name:



Insert At Front:



Submit


Листинг 34-28 Содержимое файла name_form.html в папке placeholder

Этот шаблон создает простую HTML-форму, которая отправляет данные на URLадрес, полученный от метода-обработчика. Добавьте файл с именем
validation_errors.html в папку placeholder с содержимым, показанным в листинге
34-29.
{{ layout "layout.html" }}
Validation Errors

{{ range . }}
{{.FieldName}}: {{ .Error }}
{{ end }}

Листинг 34-29 Содержимое файла validation_errors.html в папке placeholder

Срез ошибок проверки, полученных от метода обработчика, отображается в
списке. Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000/form. Нажмите кнопку Submit, не вводя значение в поле
Name, и вы увидите ошибки как от required, так и от min валидаторов, как показано
на рисунке 34-5.

Рисунок 34-5 Отображение ошибок проверки

Если вы введете имя, содержащее менее трех символов, вы увидите
предупреждение только от валидатора min. Если вы введете имя, состоящее из трех и
более символов, оно будет добавлено в список имен, как показано на рисунке 34-6.

Рисунок 34-6 Прохождение проверки данных

Добавление сеансов
Сеансы используют файлы cookie для идентификации связанных HTTP-запросов, что
позволяет отразить результаты одного действия пользователя в последующих
действиях. Как бы я ни рекомендовал писать собственную платформу для изучения Go
и стандартной библиотеки, это не распространяется на функции, связанные с
безопасностью, где важен хорошо спроектированный и тщательно протестированный
код. Файлы cookie и сеансы могут показаться не связанными с безопасностью, но они
составляют основу, с помощью которой многие приложения идентифицируют
пользователей после проверки их учетных данных. Небрежно написанная функция
сеанса может позволить пользователям получить доступ для обхода контроля доступа
или доступа к данным других пользователей.
В главе 32 я рекомендовал веб-инструментарий Gorilla как хорошее место для
начала в качестве альтернативы написанию собственного фреймворка. Один из
пакетов, предоставляемых набором инструментов Gorilla, называется sessions и
обеспечивает поддержку безопасного создания сеансов и управления ими. Именно
этот пакет я собираюсь использовать для добавления поддержки сеансов в этой главе.
Запустите команду, показанную в листинге 34-30, в папке platform, чтобы загрузить и
установить пакет sessions.
go get github.com/gorilla/sessions
Листинг 34-30 Установка пакета

Отсрочка записи данных ответа
Использование файлов cookie для сеансов представляет собой проблему конвейерного
подхода, который я использовал для обработки запросов. Сеансы получаются перед
выполнением метода обработчика, изменяются во время выполнения, а затем файл
cookie сеанса обновляется после завершения метода обработчика. Это проблема,
потому что обработчик запишет данные в ResponseWriter, после чего невозможно

обновить куки в заголовке. Добавьте файл кода с именем deferredwriter.go в папку
конвейера с содержимым, показанным в листинге 34-31. (Это средство записи похоже
на то, которое я создал для вызова обработчиков в шаблонах. Я предпочитаю
определять отдельные типы при перехвате данных запроса и ответа, потому что
способ использования перехваченных данных может меняться со временем.)
package pipeline
import (
"net/http"
"strings"
)
type DeferredResponseWriter struct {
http.ResponseWriter
strings.Builder
statusCode int
}
func (dw *DeferredResponseWriter) Write(data []byte) (int, error) {
return dw.Builder.Write(data)
}
func (dw *DeferredResponseWriter) FlushData() {
if (dw.statusCode == 0) {
dw.statusCode = http.StatusOK
}
dw.ResponseWriter.WriteHeader(dw.statusCode)
dw.ResponseWriter.Write([]byte(dw.Builder.String()))
}
func (dw *DeferredResponseWriter) WriteHeader(statusCode int) {
dw.statusCode = statusCode
}
Листинг 34-31 Содержимое файла deferredwriter.go в папке pipeline

DeferredResponseWriter — это оболочка вокруг ResponseWriter, которая не
записывает ответ до тех пор, пока не будет вызван метод FlushData, до которого
данные хранятся в памяти. В листинге 34-32 DeferredResponseWriter используется
при создании контекста, передаваемого компонентам промежуточного слоя.
...
func (pl RequestPipeline) ProcessRequest(req *http.Request,
resp http.ResponseWriter) error {
deferredWriter := &DeferredResponseWriter{ ResponseWriter:
ctx := ComponentContext {
Request: req,
ResponseWriter: deferredWriter,
}
pl(&ctx)

resp }

if (ctx.error == nil) {
deferredWriter.FlushData()
}
return ctx.error
}
...
Листинг 34-32 Использование модифицированного модуля записи в файле pipe.go в папке pipeline

Это изменение позволяет устанавливать заголовки ответов, когда запрос
возвращается по конвейеру.

Создание интерфейса сеанса, службы и промежуточного программного
обеспечения
Я собираюсь предоставить доступ к сеансам как к сервису и использовать интерфейс,
чтобы другие части платформы не зависели напрямую от пакета инструментов Gorilla,
что позволяет легко использовать другой пакет сеансов, если это необходимо.
Создайте папку platform/sessions и добавьте файл с именем session.go с
содержимым, показанным в листинге 34-33.
package sessions
import (
"context"
"platform/services"
gorilla "github.com/gorilla/sessions"
)
const SESSION__CONTEXT_KEY string = "pro_go_session"
func RegisterSessionService() {
err := services.AddScoped(func(c context.Context) Session {
val := c.Value(SESSION__CONTEXT_KEY)
if s, ok := val.(*gorilla.Session); ok {
return &SessionAdaptor{ gSession: s}
} else {
panic("Cannot get session from context ")
}
})
if (err != nil) {
panic(err)
}
}
type Session interface {
GetValue(key string) interface{}
GetValueDefault(key string, defVal interface{}) interface{}
SetValue(key string, val interface{})
}
type SessionAdaptor struct {

}

gSession *gorilla.Session

func (adaptor *SessionAdaptor) GetValue(key string) interface{} {
return adaptor.gSession.Values[key]
}
func (adaptor *SessionAdaptor) GetValueDefault(key string,
defVal interface{}) interface{} {
if val, ok := adaptor.gSession.Values[key]; ok {
return val
}
return defVal
}
func (adaptor *SessionAdaptor) SetValue(key string, val interface{}) {
if val == nil {
adaptor.gSession.Values[key] = nil
} else {
switch typedVal := val.(type) {
case int, float64, bool, string:
adaptor.gSession.Values[key] = typedVal
default:
panic("Sessions only support int, float64, bool, and
string values")
}
}
}
Листинг 34-33 Содержимое файла session.go в папке sessions

Чтобы избежать конфликта имен, я импортировал пакет инструментов Gorilla,
используя имя gorilla. Интерфейс Session определяет методы для получения и
установки значений сеанса, и этот интерфейс реализован и сопоставлен с функциями
Gorilla структурой SessionAdaptor. Функция RegisterSessionService регистрирует
одноэлементную службу, которая получает сеанс из пакета Gorilla из текущего
Context и заключает его в SessionAdaptor.
Любые данные, связанные с сеансом, будут сохранены в файле cookie. Чтобы
избежать проблем со структурами и срезами, метод SetValue будет принимать только
значения int, float64, bool и string, а также поддержку nil для удаления значения
из сеанса.
Компонент промежуточного программного обеспечения будет отвечать за создание
сеанса при передаче запроса по конвейеру и за сохранение сеанса при обратном пути.
Добавьте файл с именем session_middleware.go в папку platform/sessions с
содержимым, показанным в листинге 34-34.
Примечание
Я использую самый простой вариант хранения сеансов, что означает, что данные
сеанса сохраняются в cookie-файле ответа, отправляемом в браузеры. Это

ограничивает диапазон типов данных, которые можно безопасно хранить в сеансе,
и подходит только для сеансов, в которых хранятся небольшие объемы данных.
Доступны дополнительные хранилищасеансов, которые хранят данные в базе
данных,
что
может
решить
эти
проблемы.
См.
https://github.com/gorilla/sessions для получения списка доступных пакетов
хранилища.
package sessions
import (
"context"
"time"
"platform/config"
"platform/pipeline"
gorilla "github.com/gorilla/sessions"
)
type SessionComponent struct {
store *gorilla.CookieStore
config.Configuration
}
func (sc *SessionComponent) Init() {
cookiekey, found := sc.Configuration.GetString("sessions:key")
if !found {
panic("Session key not found in configuration")
}
if sc.GetBoolDefault("sessions:cyclekey", true) {
cookiekey += time.Now().String()
}
sc.store = gorilla.NewCookieStore([]byte(cookiekey))
}
func (sc *SessionComponent) ProcessRequest(ctx *pipeline.ComponentContext,
next func(*pipeline.ComponentContext)) {
session, _ := sc.store.Get(ctx.Request, SESSION__CONTEXT_KEY)
c := context.WithValue(ctx.Request.Context(), SESSION__CONTEXT_KEY,
session)
ctx.Request = ctx.Request.WithContext(c)
next(ctx)
session.Save(ctx.Request, ctx.ResponseWriter)
}
Листинг 34-34 Содержимое файла session_middleware.go в папке sessions

Метод Init создает хранилище файлов cookie, что является одним из способов,
которыми пакет Gorilla поддерживает сохранение сеансов. Метод ProcessRequest
получает сессию из хранилища перед передачей запроса по конвейеру со next
функцией параметра. Сеанс сохраняется в хранилище, когда запрос возвращается по
конвейеру.

Если параметр конфигурации session:cyclekey имеет значение true, то имя,
используемое для файлов cookie сеанса, будет включать время инициализации
компонента промежуточного программного обеспечения. Это полезно во время
разработки, поскольку это означает, что сеансы сбрасываются при каждом запуске
приложения.

Создание обработчика, использующего сеансы
Чтобы обеспечить простую проверку работы функции сеанса, добавьте файл с именем
counter_handler.go в папку placeholder с содержимым, показанным в листинге 3435.
package placeholder
import (
"fmt"
"platform/sessions"
)
type CounterHandler struct {
sessions.Session
}
func (c CounterHandler) GetCounter() string {
counter := c.Session.GetValueDefault("counter", 0).(int)
c.Session.SetValue("counter", counter + 1)
return fmt.Sprintf("Counter: %v", counter)
}
Листинг 34-35 Содержимое файла counter_handler.go в папке placeholder

Обработчик объявляет свою зависимость от Session, определяя поле структуры,
которое будет заполнено при создании экземпляра структуры для обработки запроса.
Метод GetCounter получает значение с именем counter из сеанса, увеличивает его и
обновляет сеанс перед использованием значения в качестве ответа.

Настройка приложения
Чтобы настроить службу сеанса и конвейер запросов, внесите изменения, показанные
в листинге 34-36, в файл startup.go в папке placeholder.
package placeholder
import (
"platform/http"
"platform/pipeline"
"platform/pipeline/basic"
"platform/services"
"sync"
"platform/http/handling"
"platform/sessions"
)

func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
&sessions.SessionComponent{},
//&SimpleMessageComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", NameHandler{}},
handling.HandlerEntry{ "", DayHandler{}},
handling.HandlerEntry{ "", CounterHandler{}},
).AddMethodAlias("/", NameHandler.GetNames),
)
}
func Start() {
sessions.RegisterSessionService()
results, err := services.Call(http.Serve, createPipeline())
if (err == nil) {
(results[0].(*sync.WaitGroup)).Wait()
} else {
panic(err)
}
}
Листинг 34-36 Настройка сеансов в файле startup.go в папке placeholder

Наконец, добавьте параметр конфигурации, показанный в листинге 34-37, в файл
config.json. Пакет сеанса Gorilla использует ключ для защиты данных сеанса. В
идеале это должно храниться за пределами папки проекта, чтобы случайно не попасть
в общедоступный репозиторий исходного кода, но для простоты я включил его в файл
конфигурации.
{
"logging" : {
"level": "debug"
},
"main" : {
"message" : "Hello from the config file"
},
"files": {
"path": "placeholder/files"
},
"templates": {
"path": "placeholder/*.html",
"reload": true
},
"sessions": {
"key": "MY_SESSION_KEY",
"cyclekey": true
}

}
Листинг 34-37 Определение ключа сеанса в файле config.json в папке platform

Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000/counter. Каждый раз, когда вы перезагружаете браузер,
значение, хранящееся в сеансе, будет увеличиваться, как показано на рисунке 34-7.

Рисунок 34-7 Использование сессий

Добавление авторизации пользователя
Последняя функция, необходимая для платформы, — поддержка авторизации с
возможностью ограничения доступа к URL-адресам для определенных пользователей.
В этом разделе я определяю интерфейсы, описывающие пользователей, и добавляю
поддержку использования этих интерфейсов для управления доступом.
Важно не путать авторизацию с аутентификацией и управлением пользователями.
Авторизация — это процесс принудительного управления доступом, который является
темой этого раздела.
Аутентификация — это процесс получения и проверки учетных данных
пользователя, чтобы их можно было идентифицировать для авторизации. Управление
пользователями — это процесс управления данными пользователя, включая пароли и
другие учетные данные.
В этой книге я создаю только заполнитель для аутентификации и вообще не
занимаюсь управлением пользователями. В реальных проектах аутентификацию и
управление пользователями должен обеспечивать проверенный сервис, которых
доступно множество. Эти сервисы предоставляют API-интерфейсы HTTP, которые
легко использовать с помощью стандартной библиотеки Go, функции которой для
выполнения HTTP-запросов были описаны в главе 25.

Определение основных типов авторизации
Создайте папку platform/authorization/identity и добавьте файл с именем
user.go с содержимым, показанным в листинге 34-38.
package identity
type User interface {
GetID() int
GetDisplayName() string

InRole(name string) bool
IsAuthenticated() bool
}
Листинг 34-38 Содержимое файла user.go в папке authorization/identity

User интерфейс будет представлять аутентифицированного пользователя, чтобы
можно было оценить запросы к ограниченным ресурсам. Чтобы создать реализацию
User интерфейса по умолчанию, которая будет полезна для приложений с простыми
требованиями к авторизации, добавьте файл с именем basic_user.go в папку
authorization/identity с содержимым, показанным в листинге 34-39.
package identity
import "strings"
var UnauthenticatedUser User = &basicUser{}
func NewBasicUser(id int, name string, roles ...string) User {
return &basicUser {
Id: id,
Name: name,
Roles: roles,
Authenticated: true,
}
}
type basicUser struct {
Id int
Name string
Roles []string
Authenticated bool
}
func (user *basicUser) GetID() int {
return user.Id
}
func (user *basicUser) GetDisplayName() string {
return user.Name
}
func (user *basicUser) InRole(role string) bool {
for _, r := range user.Roles {
if strings.EqualFold(r, role) {
return true
}
}
return false
}

func (user *basicUser) IsAuthenticated() bool {
return user.Authenticated
}
Листинг 34-39 Содержимое файла basic_user.go в папке authorization/identity

Функция NewBasicUser создает простую реализацию User интерфейса, а
переменная UnauthenticatedUser будет использоваться для представления
пользователя, не вошедшего в приложение.
Добавьте
файл
с
именем
signin_mgr.go
в
папку
platform/authorization/identity с содержимым, показанным в листинге 34-40.
package identity
type SignInManager interface {

}

SignIn(user User) error
SignOut(user User) error
Листинг 34-40 Содержимое файла signin_mgr.go в папке authorization/identity

Интерфейс SignInManager будет использоваться для определения службы,
которую приложение будет использовать для входа пользователя в приложение и
выхода из него. Подробная информация о том, как пользователь аутентифицируется,
остается на усмотрение приложения.
Добавьте
файл
с
именем
user_store.go
в
папку
platform/authorization/identity с содержимым, показанным в листинге 34-41.
package identity
type UserStore interface {
GetUserByID(id int) (user User, found bool)
GetUserByName(name string) (user User, found bool)
}
Листинг 34-41 Содержимое файла user_store.go в папке authorization/identity

Хранилище пользователей обеспечивает доступ к пользователям, известным
приложению, которых можно найти по идентификатору или имени.
Далее мне нужен интерфейс, который будет использоваться для описания
требования контроля доступа. Добавьте файл с именем auth_condition.go в папку
platform/authorization/identity с содержимым, показанным в листинге 34-42.
package identity
type AuthorizationCondition interface {
Validate(user User) bool

}
Листинг 34-42 Содержимое файла auth_condition.go в папке authorization/identity

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

Реализация интерфейсов платформы
Следующим шагом будет реализация интерфейсов, которые платформа будет
предоставлять для авторизации. Добавьте файл с именем sessionsignin.go в папку
platform/authorization с содержимым, показанным в листинге 34-43.
package authorization
import (
"platform/authorization/identity"
"platform/services"
"platform/sessions"
"context"
)
const USER_SESSION_KEY string = "USER"
func RegisterDefaultSignInService() {
err
:=
services.AddScoped(func(c
identity.SignInManager {
return &SessionSignInMgr{ Context : c}
})
if (err != nil) {
panic(err)
}
}

context.Context)

type SessionSignInMgr struct {
context.Context
}
func (mgr *SessionSignInMgr) SignIn(user identity.User) (err error) {
session, err := mgr.getSession()
if err == nil {
session.SetValue(USER_SESSION_KEY, user.GetID())
}
return
}
func (mgr *SessionSignInMgr) SignOut(user identity.User) (err error) {
session, err := mgr.getSession()
if err == nil {
session.SetValue(USER_SESSION_KEY, nil)
}

return
}
func (mgr *SessionSignInMgr) getSession() (s sessions.Session, err error)
{
err = services.GetServiceForContext(mgr.Context, &s)
return
}
Листинг 34-43 Содержимое файла sessionsignin.go в папке authorization

Структура SessionSignInMgr реализует интерфейс SignInManager, сохраняя
идентификатор вошедшего пользователя в сеансе и удаляя его, когда пользователь
выходит из системы. Использование сеансов гарантирует, что пользователь останется
в системе до тех пор, пока он не выйдет из системы или пока не истечет срок действия
сеанса. Функция RegisterDefaultSignInService создает службу с заданной
областью для интерфейса SignInManager, которая разрешается с помощью структуры
SessionSignInMgr.
Чтобы предоставить службу, которая представляет вошедшего в систему
пользователя,
добавьте
файл
с
именем
user_service.go
в
папку
platform/authorization с содержимым, показанным в листинге 34-44.
package authorization
import (
"platform/services"
"platform/sessions"
"platform/authorization/identity"
)
func RegisterDefaultUserService() {
err := services.AddScoped(func(session sessions.Session,
store identity.UserStore) identity.User {
userID, found := session.GetValue(USER_SESSION_KEY).(int)
if found {
user, userFound := store.GetUserByID(userID)
if (userFound) {
return user
}
}
return identity.UnauthenticatedUser
})
if (err != nil) {
panic(err)
}
}
Листинг 34-44 Содержимое файла user_service.go в папке authorization

Функция RegisterDefaultUserService создает службу с заданной областью для
User интерфейса, которая считывает значение, хранящееся в текущем сеансе, и

использует его для запроса службы UserStore.
Чтобы создать простое условие доступа, которое проверяет, находится ли
пользователь в роли, добавьте файл с именем role_condition.go в папку
platform/authorization с содержимым, показанным в листинге 34-45.
package authorization
import ("platform/authorization/identity")
func NewRoleCondition(roles ...string) identity.AuthorizationCondition {
return &roleCondition{ allowedRoles: roles}
}
type roleCondition struct {
allowedRoles []string
}
func (c *roleCondition) Validate(user identity.User) bool {
for _, allowedRole := range c.allowedRoles {
if user.InRole(allowedRole) {
return true
}
}
return false
}
Листинг 34-45 Содержимое файла role_condition.go в папке authorization

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

Реализация контроля доступа
Следующим шагом является добавление поддержки для определения ограничения
доступа и применения его к запросам. Добавьте файл с именем auth_middleware.go в
папку platform/authorization с содержимым, показанным в листинге 34-46.
package authorization
import (
"net/http"
"platform/authorization/identity"
"platform/config"
"platform/http/handling"
"platform/pipeline"
"strings"
"regexp"
)
func
NewAuthComponent(prefix
identity.AuthorizationCondition,

string,

condition

requestHandlers ...interface{}) *AuthMiddlewareComponent {
entries := []handling.HandlerEntry {}
for _, handler := range requestHandlers {
entries = append(entries, handling.HandlerEntry{prefix, handler})
}
router := handling.NewRouter(entries...)

}

return &AuthMiddlewareComponent{
prefix: "/" + prefix ,
condition: condition,
RequestPipeline: pipeline.CreatePipeline(router),
fallbacks: map[*regexp.Regexp]string {},
}

type AuthMiddlewareComponent struct {
prefix string
condition identity.AuthorizationCondition
pipeline.RequestPipeline
config.Configuration
authFailURL string
fallbacks map[*regexp.Regexp]string
}
func (c *AuthMiddlewareComponent) Init() {

c.authFailURL,
c.Configuration.GetString("authorization:failUrl")
}

_

=

func (*AuthMiddlewareComponent) ImplementsProcessRequestWithServices() {}
func (c *AuthMiddlewareComponent) ProcessRequestWithServices(
context *pipeline.ComponentContext,
next func(*pipeline.ComponentContext),
user identity.User) {
if strings.HasPrefix(context.Request.URL.Path, c.prefix) {
for expr, target := range c.fallbacks {
if expr.MatchString(context.Request.URL.Path) {
http.Redirect(context.ResponseWriter, context.Request,
target, http.StatusSeeOther)
return
}
}
if c.condition.Validate(user) {
c.RequestPipeline.ProcessRequest(context.Request,
context.ResponseWriter)
} else {
if c.authFailURL != "" {
http.Redirect(context.ResponseWriter, context.Request,

c.authFailURL, http.StatusSeeOther)
} else if user.IsAuthenticated() {
context.ResponseWriter.WriteHeader(http.StatusForbidden)
} else {
context.ResponseWriter.WriteHeader(http.StatusUnauthorized)
}
}
} else {
next(context)
}
}
func (c *AuthMiddlewareComponent) AddFallback(target string,
patterns ...string) *AuthMiddlewareComponent {
for _, p := range patterns {
c.fallbacks[regexp.MustCompile(p)] = target
}
return c
}
Листинг 34-46 Содержимое файла auth_middleware.go в папке authorization

Структура AuthMiddlewareComponent — это промежуточный компонент, который
создает ветвь в конвейере запросов с маршрутизатором URL-адресов, обработчики
которого получают запрос только при выполнении условия авторизации.

Реализация функций заполнителя приложения
Следуя шаблону, установленному для более ранних функций, я собираюсь создать
базовые реализации функций авторизации, которые будет предоставлять приложение,
использующее платформу. Добавьте файл с именем placeholder_store.go на
platform/placeholder с содержимым, показанным в листинге 34-47.
package placeholder
import (
"platform/services"
"platform/authorization/identity"
"strings"
)
func RegisterPlaceholderUserStore() {
err := services.AddSingleton(func () identity.UserStore {
return &PlaceholderUserStore{}
})
if (err != nil) {
panic(err)
}
}
var users = map[int]identity.User {
1: identity.NewBasicUser(1, "Alice", "Administrator"),

}

2: identity.NewBasicUser(2, "Bob"),

type PlaceholderUserStore struct {}
func (store *PlaceholderUserStore) GetUserByID(id int) (identity.User,
bool) {
user, found := users[id]
return user, found
}
func
(store
*PlaceholderUserStore)
GetUserByName(name
(identity.User, bool) {
for _, user := range users {
if strings.EqualFold(user.GetDisplayName(), name) {
return user, true
}
}
return nil, false
}

string)

Листинг 34-47 Содержимое файла placeholder_store.go в папке placeholder

Структура PlaceholderUserStore реализует интерфейс UserStore со статически
определенными данными для двух пользователей, Alice и Bob, и используется
функцией RegisterPlaceholderUserStore для создания одноэлементной службы.

Создание обработчика аутентификации
Чтобы разрешить простую аутентификацию, добавьте файл с именем
authentication_handler.go в папку-заполнитель с содержимым, показанным в
листинге 34-48.
package placeholder
import (
"platform/http/actionresults"
"platform/authorization/identity"
"fmt"
)
type AuthenticationHandler struct {
identity.User
identity.SignInManager
identity.UserStore
}
func (h AuthenticationHandler) GetSignIn() actionresults.ActionResult {
return actionresults.NewTemplateAction("signin.html",
fmt.Sprintf("Signed in as: %v", h.User.GetDisplayName()))
}

type Credentials struct {
Username string
Password string
}
func
(h
AuthenticationHandler)
PostSignIn(creds
Credentials)
actionresults.ActionResult {
if creds.Password == "mysecret" {
user, ok := h.UserStore.GetUserByName(creds.Username)
if (ok) {
h.SignInManager.SignIn(user)
return actionresults.NewTemplateAction("signin.html",
fmt.Sprintf("Signed in as: %v", user.GetDisplayName()))
}
}
return actionresults.NewTemplateAction("signin.html", "Access Denied")
}
func (h AuthenticationHandler) PostSignOut() actionresults.ActionResult {
h.SignInManager.SignOut(h.User)
return actionresults.NewTemplateAction("signin.html", "Signed out")
}
Листинг 34-48 Содержимое файла authentication_handler.go в папке placeholder

Этот обработчик запросов имеет встроенный пароль — mysecret — для всех
пользователей. Метод GetSignIn отображает шаблон для сбора имени пользователя и
пароля. Метод PostSignIn проверяет пароль и удостоверяется, что в магазине есть
пользователь с указанным именем, прежде чем выполнять вход пользователя в
приложение. Метод PostSignOut подписывает пользователя из приложения. Чтобы
создать шаблон, используемый обработчиком, добавьте файл с именем signin.html в
папку placeholder с содержимым, показанным в листинге 34-49.
{{ layout "layout.html" }}
{{ if ne . "" }}
{{. }}
{{ end }}


Username:



Password:



Sign In
Sign Out



Листинг 34-49 Содержимое файла signin.html в папке placeholder

Шаблон отображает базовую HTML-форму с сообщением, предоставленным
методом обработчика, который ее отображает.

Настройка приложения
Остается только настроить приложение для создания защищенного обработчика и
настроить функции авторизации, как показано в листинге 34-50.
package placeholder
import (
"platform/http"
"platform/pipeline"
"platform/pipeline/basic"
"platform/services"
"sync"
"platform/http/handling"
"platform/sessions"
"platform/authorization"
)
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
&sessions.SessionComponent{},
//&SimpleMessageComponent{},
authorization.NewAuthComponent(
"protected",
authorization.NewRoleCondition("Administrator"),
CounterHandler{},
),
handling.NewRouter(
handling.HandlerEntry{ "", NameHandler{}},
handling.HandlerEntry{ "", DayHandler{}},
//handling.HandlerEntry{ "", CounterHandler{}},
handling.HandlerEntry{ "", AuthenticationHandler{}},
).AddMethodAlias("/", NameHandler.GetNames),
)
}
func Start() {
sessions.RegisterSessionService()
authorization.RegisterDefaultSignInService()
authorization.RegisterDefaultUserService()
RegisterPlaceholderUserStore()
results, err := services.Call(http.Serve, createPipeline())

}

if (err == nil) {
(results[0].(*sync.WaitGroup)).Wait()
} else {
panic(err)
}

Листинг 34-50 Настройка приложения в файле startup.go в папке placeholder

Изменения создают ветвь конвейера с префиксом /protected, которая доступна
только пользователям, которым назначена роль Administrator. CounterHandler,
определенный ранее в этой главе, является единственным обработчиком ветки.
AuthenticationHandler добавляется в основную ветвь конвейера.
Скомпилируйте и запустите приложение и используйте браузер для запроса
http://localhost:5000/protected/counter. Это защищенный метод обработчика, и,
поскольку зарегистрированного пользователя нет, будет показан результат, показанный
на рисунке 34-8.

Рисунок 34-8 Неаутентифицированный запрос

Ответ 401 отправляется, когда пользователь, не прошедший проверку
подлинности, запрашивает защищенный ресурс и известен как ответ на вызов,
который часто используется для предоставления пользователю возможности войти в
систему.
Затем запросите
http://localhost:5000/signin
, введите bob в поле Username, введите mysecret в поле Password и нажмите Sign In,
как
показано
на
рисунке
34-9.
Запросите

http://localhost:5000/protected/counter, и вы получите ответ 403, который
отправляется, когда пользователь, уже представивший свои учетные данные,
запрашивает доступ к защищенному ресурсу.

Рисунок 34-9 Неавторизованный запрос

Наконец, запросите http://localhost:5000/signin, введите alice в поле
Username и mysecret в поле Password и нажмите Sign In, как показано на рисунке 3410. Запросите http://localhost:5000/protected/counter, и вы получите ответ от
обработчика, также показанного на рисунке 34-10, поскольку Alice находится в роли
Adminstrator.

Рисунок 34-10 Авторизованный запрос

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

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

Создание проекта SportsStore
Я собираюсь создать приложение, использующее проект платформы, созданный в главах
32, но определенное в собственном проекте. Откройте командную строку и используйте
ее для создания папки с именем sportsstore в той же папке, что и папка platform.
Перейдите в папку sportsstore и выполните команду, показанную в листинге 35-1.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех остальных глав этой
книги — с https://github.com/apress/pro-go. См. Главу 2 о том, как получить
помощь, если у вас возникнут проблемы с запуском примеров.
go mod init sportsstore
Листинг 35-1 Инициализация проекта

Эта команда создает файл go.mod. Чтобы объявить зависимость от проекта
платформы, выполните команды, показанные в листинге 35-2, в папке sportsstore.
go mod edit -require="platform@v1.0.0"
go mod edit -replace="platform@v1.0.0"="../platform"
go get -d "platform@v1.0.0"
Листинг 35-2 Создание зависимости

Откройте файл go.mod, и вы увидите действие этих команд, как показано в листинге
35-3.
module sportsstore
go 1.17
require platform v1.0.0
require (
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
)
replace platform v1.0.0 => ../platform

Листинг 35-3 Действие команд go в файле go.mod в папке sportsstore

Директива require объявляет зависимость от модуля platform. В реальных проектах
это можно указать как URL-адрес вашего репозитория контроля версий, например URLадрес GitHub. Этот проект не будет передан системе контроля версий, поэтому я просто
использовал название platform.
Директива replace указывает локальный путь, по которому можно найти модуль
platform. Когда инструменты Go устраняют зависимость от пакета в модуле platform,
они делают это с использованием папки platform, которая находится на том же уровне,
что и папка sportsstore.
Проект platform имеет зависимости от сторонних пакетов, которые необходимо
разрешить, прежде чем их можно будет использовать. Это было сделано командой go get,
создавшей директиву require, которая объявляет косвенные зависимости от пакетов,
используемых для реализации сеансов в главе 34.

Настройка приложения
Добавьте файл с именем config.json в папку sportsstore и используйте его для
определения параметров конфигурации, показанных в листинге 35-4.
{

}

"logging" : {
"level": "debug"
},
"files": {
"path": "files"
},
"templates": {
"path": "templates/*.html",
"reload": true
},
"sessions": {
"key": "MY_SESSION_KEY",
"cyclekey": true
}
Листинг 35-4 Содержимое файла config.json в папке sportsstore

Затем добавьте файл с именем main.go в папку sportsstore с содержимым,
показанным в листинге 35-5.
package main
import (
)

"platform/services"
"platform/logging"

func writeMessage(logger logging.Logger) {
logger.Info("SportsStore")
}

func main() {
services.RegisterDefaultServices()
services.Call(writeMessage)
}
Листинг 35-5 Содержимое файла main.go в папке sportsstore

Скомпилируйте и выполните проект с помощью команды, показанной в листинге 35-6,
в папке sportsstore.
go run .
Листинг 35-6 Компиляция и выполнение проекта

Метод main устанавливает службы platform по умолчанию и вызывает writeMessage,
выводя следующий результат:
07:55:03 INFO SportsStore

Запуск модели данных
Почти у всех проектов есть какая-то модель данных, и именно с нее я обычно начинаю
разработку. Мне нравится начинать с нескольких простых типов данных, а затем начинать
работать над тем, чтобы сделать их доступными для остальной части проекта. По мере
добавления функций в приложение я возвращаюсь к модели данных и расширяю ее
возможности.
Создайте папку sportsstore/models и добавьте в нее файл с именем product.go с
содержимым, показанным в листинге 35-7.
package models
type Product struct {
ID int
Name string
Description string
Price float64
*Category
}
Листинг 35-7 Содержимое файла product.go в папке models

Я предпочитаю определять один тип в каждом файле вместе со всеми связанными
функциями конструктора или методами, связанными с этим типом. Чтобы создать тип
данных для встроенного поля Category, добавьте файл с именем category.go в папку
моделей с содержимым, показанным в листинге 35-8.
package models
type Category struct {
ID int
CategoryName string
}
Листинг 35-8 Содержимое файла category.go в папке models

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

Определение интерфейса репозитория
Мне нравится использовать репозиторий как способ отделить источник данных в
приложении от кода, который их потребляет. Добавьте файл с именем repository.go в
папку sportsstore/models с содержимым, показанным в листинге 35-9.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
GetCategories() []Category
}

Seed()
Листинг 35-9 Содержимое файла repository.go в папке models

Я создам сервис для интерфейса Repository, который позволит мне легко менять
источник данных, используемых в приложении.
Обратите внимание, что методы GetProduct, GetProducts и GetCategories,
определенные в листинге 35-9, не возвращают указатели. Я предпочитаю использовать
значения, чтобы код, использующий данные, не вносил изменения с помощью указателей,
влияющих на данные, управляемые репозиторием. Этот подход означает, что значения
данных будут дублироваться, но гарантирует отсутствие странных эффектов, вызванных
случайными изменениями через общую ссылку. Иными словами, я не хочу, чтобы
репозиторий предоставлял доступ к данным без обмена ссылками с кодом, который
использует эти данные.

Реализация (временного) репозитория
Я буду хранить данные SportsStore в реляционной базе данных, но я предпочитаю начать
с простой реализации репозитория в памяти, которую я использую до тех пор, пока не
будут реализованы некоторые основные функции приложения.
По мере разработки проекта неизбежны изменения в подходе, и если я начну с базы
данных для репозитория, то мне не захочется вносить изменения в написанные мной
SQL-запросы. Это означает, что в конечном итоге я адаптирую код приложения, чтобы
обойти ограничения SQL, что, как я знаю, не имеет смысла, но я также знаю, что я все
равно это сделаю. Вы можете быть более дисциплинированным, но я получаю наилучшие
результаты, работая с простым репозиторием в памяти, а затем пишу SQL только тогда,
когда я понимаю, какой будет окончательная форма данных.

Создайте папку sportsstore/models/repo и добавьте в нее файл с именем
memory_repo.go с содержимым, показанным в листинге 35-10.
package repo
import (
"platform/services"
"sportsstore/models"
)
func RegisterMemoryRepoService() {
services.AddSingleton(func() models.Repository {
repo := &MemoryRepo{}
repo.Seed()
return repo
})
}
type MemoryRepo struct {
products []models.Product
categories []models.Category
}
func (repo *MemoryRepo) GetProduct(id int) (product models.Product) {
for _, p := range repo.products {
if (p.ID == id) {
product = p
return
}
}
return
}
func (repo *MemoryRepo) GetProducts() (results []models.Product) {
return repo.products
}
func (repo *MemoryRepo) GetCategories() (results []models.Category) {
return repo.categories
}
Листинг 35-10 Содержимое memory_repo.go в папке models/repo

Структура MemoryRepo определяет большую часть функций, необходимых для
реализации интерфейса репозитория, сохраняя значения в срезе. Чтобы реализовать метод
Seed, добавьте файл с именем memory_repo_seed.go в папку repo с содержимым,
показанным в листинге 35-11.
package repo
import (
"fmt"
"math/rand"
"sportsstore/models"

)
func (repo *MemoryRepo) Seed() {
repo.categories = make([]models.Category, 3)
for i := 0; i < 3; i++ {
catName := fmt.Sprintf("Category_%v", i + 1)
repo.categories[i]= models.Category{ID: i + 1, CategoryName: catName}
}

}

for i := 0; i < 20; i++ {
name := fmt.Sprintf("Product_%v", i + 1)
price := rand.Float64() * float64(rand.Intn(500))
cat := &repo.categories[rand.Intn(len(repo.categories))]
repo.products = append(repo.products, models.Product{
ID: i + 1,
Name: name, Price: price,
Description: fmt.Sprintf("%v (%v)", name, cat.CategoryName),
Category: cat,
})
}
Листинг 35-11 Содержимое файла memory_repo_seed.go в папке models/repo

Я определил этот метод отдельно, чтобы не указывать код заполнения при добавлении
функций в репозиторий.

Отображение списка продуктов
Первым шагом в отображении контента является отображение списка продуктов для
продажи. Создайте папку sportsstore/store и добавьте в нее файл с именем
product_handler.go с содержимым, показанным в листинге 35-12.
package store
import (
"sportsstore/models"
"platform/http/actionresults"
)
type ProductHandler struct {
Repository models.Repository
}
type ProductTemplateContext struct {
Products []models.Product
}
func (handler ProductHandler) GetProducts() actionresults.ActionResult {
return actionresults.NewTemplateAction("product_list.html",
ProductTemplateContext {
Products: handler.Repository.GetProducts(),
})

}
Листинг 35-12 Содержимое файла product_handler.go в папке store

Метод GetProducts отображает шаблон с именем product_list.html, передавая
значение ProductTemplateContext, которое я буду использовать для предоставления
дополнительной информации в шаблон позже.
Подсказка
Маршруты не генерируются для методов, которые продвигаются из анонимных
встроенных полей структуры, чтобы случайно не создавать маршруты и не раскрывать
внутреннюю работу обработчиков запросов для HTTP-запросов. Одним из следствий
этого решения является то, что оно также исключает методы, определенные
структурой, которая имеет то же имя, что и продвинутый метод. Именно по этой
причине я присвоил имя полю Products, определенному структурой ProductHandler.
Если бы я этого не сделал, то метод GetProducts не использовался бы для генерации
маршрута, потому что он совпадает с именем метода, определенного интерфейсом
models.Repository.

Создание шаблона и макета
Чтобы определить шаблон, создайте папку sportsstore/templates и добавьте в нее файл
с именем product_list.html с содержимым, показанным в листинге 35-13.
{{ layout "store_layout.html" }}
{{ range .Products }}

{{.ID}}, {{ .Name }}, {{ printf "$%.2f" .Price }}, {{ .CategoryName
}}

{{ end }}
Листинг 35-13 Содержимое файла product_list.html в папке templates

Макет использует выражение range для поля Product структуры, предоставленной
обработчиком, для создания элемента div для каждого Product в Repository.
Чтобы создать макет, указанный в листинге 35-13, добавьте файл с именем
store_layout.html в папку sportsstore/templates с содержимым, показанным в
листинге 35-14..




SportsStore


{{ body }}


Листинг 35-14 Содержимое файла store_layout.html в папке templates

Настройка приложения
Чтобы зарегистрировать службы и создать конвейер, требуемый приложением
SportsStore, замените содержимое файла main.go тем, что показано в листинге 35-15.
package main
import (
"sync"
"platform/http"
"platform/http/handling"
"platform/services"
"platform/pipeline"
"platform/pipeline/basic"
"sportsstore/store"
"sportsstore/models/repo"
)
func registerServices() {
services.RegisterDefaultServices()
repo.RegisterMemoryRepoService()
}
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts),
)
}
func main() {
registerServices()
results, err := services.Call(http.Serve, createPipeline())
if (err == nil) {
(results[0].(*sync.WaitGroup)).Wait()
} else {
panic(err)
}
}
Листинг 35-15 Замена содержимого файла main.go в папке sportsstore

Службы по умолчанию регистрируются вместе с хранилищем памяти. Конвейер
содержит основные компоненты, созданные в главе 34, с маршрутизатором, настроенным
с помощью ProductHandler.
Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000, который даст ответ, показанный на рисунке 35-1.

Рисунок 35-1 Отображение списка продуктов

Работа с запросами разрешений брандмауэра Windows
Как объяснялось в предыдущих главах, Windows будет запрашивать разрешения
брандмауэра каждый раз, когда проект компилируется с помощью команды go run,
чего можно избежать, используя простой сценарий PowerShell. Напомню, вот
содержимое скрипта, который я сохраняю как buildandrun.ps1:
$file = "./sportsstore.exe"
&go build -o $file
if ($LASTEXITCODE -eq 0) {
&$file
}
Чтобы собрать и выполнить проект, используйте команду ./buildandrun.ps1 в
папке sportsstore.

Добавление пагинации
Вывод на рисунке 35-1 показывает, что все продукты в репозитории отображаются в
одном списке. Следующим шагом является добавление поддержки разбиения на
страницы, чтобы пользователю было представлено небольшое количество продуктов и он
мог перемещаться между страницами. Мне нравится вносить изменения в репозиторий, а
затем работать до тех пор, пока не будет достигнут шаблон, отображающий данные. В
листинге 35-16 к интерфейсу Repository добавлен метод, который позволяет
запрашивать страницу значений Product.
package models
type Repository interface {
GetProduct(id int) Product

GetProducts() []Product
int)

GetProductPage(page, pageSize int) (products []Product, totalAvailable

GetCategories() []Category
Seed()
}
Листинг 35-16 Добавление метода в файл репозитория.go в папке models

Метод GetProductPage возвращает срез Product и общее количество элементов в
репозитории. Перечисление 35-17 реализует новый метод в репозитории памяти.
package repo
import (
"platform/services"
"sportsstore/models"
"math"
)
func RegisterMemoryRepoService() {
services.AddSingleton(func() models.Repository {
repo := &MemoryRepo{}
repo.Seed()
return repo
})
}
type MemoryRepo struct {
products []models.Product
categories []models.Category
}
func (repo *MemoryRepo) GetProduct(id int) (product models.Product) {
for _, p := range repo.products {
if (p.ID == id) {
product = p
return
}
}
return
}
func (repo *MemoryRepo) GetProducts() (results []models.Product) {
return repo.products
}
func (repo *MemoryRepo) GetCategories() (results []models.Category) {
return repo.categories
}

func (repo *MemoryRepo) GetProductPage(page, pageSize int) ([]models.Product,
int) {
return getPage(repo.products, page, pageSize), len(repo.products)
}
func getPage(src []models.Product, page, pageSize int) []models.Product {
start := (page -1) * pageSize
if page > 0 && len(src) > start {
end := (int)(math.Min((float64)(len(src)), (float64)(start +
pageSize)))
return src[start : end]
}
return []models.Product{}
}
Листинг 35-17 Реализация метода в файле memory_repo.go в папке models/repo

Листинг 35-18 обновляет обработчик запроса, чтобы он выбирал страницу данных и
передал ее шаблону вместе с дополнительными полями структуры, необходимыми для
поддержки разбиения на страницы.
package store
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
"math"
)
const pageSize = 4
type ProductHandler struct {
Repository models.Repository
URLGenerator handling.URLGenerator
}
type ProductTemplateContext struct {
Products []models.Product
Page int
PageCount int
PageNumbers []int
PageUrlFunc func(int) string
}
func
(handler
ProductHandler)
GetProducts(page
actionresults.ActionResult {
prods, total := handler.Repository.GetProductPage(page, pageSize)
pageCount := int(math.Ceil(float64(total) / float64(pageSize)))
return actionresults.NewTemplateAction("product_list.html",
ProductTemplateContext {
Products: prods,
Page: page,
PageCount: pageCount,

int)

PageNumbers: handler.generatePageNumbers(pageCount),
PageUrlFunc: handler.createPageUrlFunction(),
}

})

func (handler ProductHandler) createPageUrlFunction() func(int) string {
return func(page int) string {
url,
_
handler.URLGenerator.GenerateUrl(ProductHandler.GetProducts, page)
return url
}
}
func (handler ProductHandler) generatePageNumbers(pageCount
[]int) {
pages = make([]int, pageCount)
for i := 0; i < pageCount; i++ {
pages[i] = i + 1
}
return
}

int)

:=

(pages

Листинг 35-18 Обновление метода обработчика в файле product_handler.go в папке store

В листинге 35-18 появилось много новых операторов, потому что обработчик должен
предоставить гораздо больше информации шаблону для поддержки нумерации страниц.
Метод GetProducts был изменен, чтобы принимать параметр, который используется для
получения страницы данных. Дополнительные поля, определенные для структуры,
передаваемой в шаблон, включают в себя выбранную страницу, функцию для создания
URL-адресов для перехода на страницу и срез, содержащий последовательность чисел
(что необходимо, поскольку шаблоны могут использовать диапазоны, но не циклы for для
создания контента). Листинг 35-19 обновляет шаблон для использования новой
информации.
{{ layout "store_layout.html" }}
{{ $context := . }}
{{ range .Products }}

{{.ID}}, {{ .Name }}, {{ printf "$%.2f" .Price }}, {{ .CategoryName
}}

{{ end }}
{{ range .PageNumbers}}
{{ if eq $context.Page .}}
{{ . }}
{{ else }}
{{ . }}
{{ end }}
{{ end }}
Листинг 35-19 Поддержка нумерации страниц в файле product_list.html в папке templates

Я определил переменную $context, чтобы всегда иметь легкий доступ к значению
структуры, переданному в шаблон методом обработчика. Новое выражение range
перечисляет список номеров страниц и отображает ссылку навигации для всех из них,
кроме текущей выбранной страницы. URL-адрес для ссылки создается путем вызова
функции, назначенной полю PageUrlFunc контекстной структуры.
Затем необходимо изменить псевдонимы, установленные для системы
маршрутизации, чтобы URL-адрес по умолчанию и URL-адрес /products инициировали
перенаправление на первую страницу продуктов, как показано в листинге 35-20.
...
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 1).
AddMethodAlias("/products", store.ProductHandler.GetProducts, 1),
)
}
...
Листинг 35-20 Обновление псевдонимов в файле main.go в папке sportsstore

Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000. Вам будут представлены продукты, отображаемые на четырех
страницах, с навигационными ссылками, которые запрашивают другие страницы, как
показано на рисeyrt 35-2.

Рисунок 35-2 Добавление поддержки пагинации

Стилизация содержимого шаблона
Прежде чем добавлять какие-либо дополнительные функции в приложение, я собираюсь
рассмотреть внешний вид продуктов в списке. Я собираюсь использовать Bootstrap,
популярный CSS-фреймворк, который мне нравится использовать. Bootstrap применяет
стили, используя атрибуты class HTML-элементов, и подробно описан на
https://getbootstrap.com.

Установка CSS-файла Bootstrap
В Go нет хорошего способа установки пакетов за пределами экосистемы Go. Чтобы
добавить файл CSS в проект, создайте папку sportsstore/files и с помощью командной
строки запустите команду, показанную в листинге 35-21, в папке sportsstore.
curl
https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.1/css/bootstrap.min.css
--output files/bootstrap.min.css
Листинг 35-21 Загрузка таблицы стилей CSS

Если вы используете Windows, используйте команду PowerShell, показанную в
листинге 35-22.
Invoke-WebRequest
-Uri
`
"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.1/css/bootstrap.min.css"
`
-OutFile "files/bootstrap.min.css"
Листинг 35-22 Загрузка таблицы стилей CSS в Windows

Обновление макета
Добавьте элемент, показанный в листинге 35-23, в файл store_layout.html в папке
templates.




SportsStore




SPORTS STORE



{{ template "left_column" . }}


{{ template "right_column" . }}




Листинг 35-23 Добавление Bootstrap в файл store_layout.html в папке templates

Новые элементы добавляют элемент link для CSS-файла Bootstrap и используют
функции Bootstrap для создания заголовка и двухколоночного макета. Содержимое
столбцов получается из шаблонов с именами left_column и right_column.

Стилизация содержимого шаблона
Роль шаблона product_list.html должна измениться, чтобы соответствовать ожиданиям
макета и определить шаблоны для левого и правого столбцов в макете, как показано в
листинге 35-24.
{{ layout "store_layout.html" }}
{{ define "left_column" }}
Put something useful here
{{end}}
{{ define "right_column" }}
{{ $context := . }}
{{ range $context.Products }}



{{ .Name }}

{{ printf "$%.2f" .Price }}



{{ .Description }}

{{ end }}
{{ template "page_buttons.html" $context }}
{{end}}
Листинг 35-24 Создание содержимого столбца в файле product_list.html в папке templates

Новая структура определяет заполнитель для левого столбца и создает список
стилизованных продуктов в правом столбце.
Я определил отдельный шаблон для кнопок пагинации. Добавьте файл с именем
page_buttons.html в папку templates с содержимым, показанным в листинге 35-25.
{{ $context := . }}

{{ range .PageNumbers}}
{{ if eq $context.Page .}}
{{ . }}
{{ else }}
{{ . }}
{{ end }}
{{ end }}

Листинг 35-25 Содержимое файла page_buttons.html в папке templates

Скомпилируйте и запустите проект и запросите http://localhost:5000. Вы увидите
стилизованное содержимое, показанное на рисунке 35-3.

Рисунок 35-3 Стилизация содержимого

Добавление поддержки фильтрации категорий
Следующим шагом является замена содержимого заполнителя в левом столбце кнопками,
позволяющими пользователю выбирать категорию, по которой следует фильтровать
продукты, отображаемые в списке. Для начала добавьте метод, показанный в листинге 3526, в интерфейс Repository.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
GetProductPage(page, pageSize int) (products []Product, totalAvailable
int)
GetProductPageCategory(categoryId int, page, pageSize int) (products
[]Product,
totalAvailable int)
GetCategories() []Category
Seed()
}
Листинг 35-26 Добавление метода в файл репозитория.go в папке models

Новый метод позволяет указать категорию при запросе страницы. Листинг 35-27
реализует новый метод в репозитории памяти.
package repo

import (
"platform/services"
"sportsstore/models"
"math"
)
func RegisterMemoryRepoService() {
services.AddSingleton(func() models.Repository {
repo := &MemoryRepo{}
repo.Seed()
return repo
})
}
type MemoryRepo struct {
products []models.Product
categories []models.Category
}
func (repo *MemoryRepo) GetProduct(id int) (product models.Product) {
for _, p := range repo.products {
if (p.ID == id) {
product = p
return
}
}
return
}
func (repo *MemoryRepo) GetProducts() (results []models.Product) {
return repo.products
}
func (repo *MemoryRepo) GetCategories() (results []models.Category) {
return repo.categories
}
func (repo *MemoryRepo) GetProductPage(page, pageSize int) ([]models.Product,
int) {
return getPage(repo.products, page, pageSize), len(repo.products)
}
func (repo *MemoryRepo) GetProductPageCategory(category int, page,
pageSize int) (products []models.Product, totalAvailable int) {
if category == 0 {
return repo.GetProductPage(page, pageSize)
} else {
filteredProducts := make([]models.Product, 0, len(repo.products))
for _, p := range repo.products {
if p.Category.ID == category {
filteredProducts = append(filteredProducts, p)
}
}

return

getPage(filteredProducts,

page,

pageSize),

len(filteredProducts)
}
}
func getPage(src []models.Product, page, pageSize int) []models.Product {
start := (page -1) * pageSize
if page > 0 && len(src) > start {
end := (int)(math.Min((float64)(len(src)), (float64)(start +
pageSize)))
return src[start : end]
}
return []models.Product{}
}
Листинг 35-27 Реализация метода в файле memory_repository.go в папке models

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

Обновление обработчика запросов
Следующим шагом является изменение метода обработчика запроса, чтобы он получал
параметр категории и использовал его для получения отфильтрованных данных, которые
затем передаются в шаблон вместе с дополнительными данными контекста,
необходимыми для создания кнопок навигации, которые позволяют выбрать другую
категорию. выбрано, как показано в листинге 35-28..
package store
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
"math"
)
const pageSize = 4
type ProductHandler struct {
Repository models.Repository
URLGenerator handling.URLGenerator
}
type ProductTemplateContext struct {
Products []models.Product
Page int
PageCount int
PageNumbers []int
PageUrlFunc func(int) string
SelectedCategory int
}
func (handler ProductHandler) GetProducts(category,

page int) actionresults.ActionResult {
prods, total := handler.Repository.GetProductPageCategory(category,
page, pageSize)
pageCount := int(math.Ceil(float64(total) / float64(pageSize)))
return actionresults.NewTemplateAction("product_list.html",
ProductTemplateContext {
Products: prods,
Page: page,
PageCount: pageCount,
PageNumbers: handler.generatePageNumbers(pageCount),
PageUrlFunc: handler.createPageUrlFunction(category),
SelectedCategory: category,
})
}
func (handler ProductHandler) createPageUrlFunction(category int) func(int)
string {
return func(page int) string {
url,
_
:=
handler.URLGenerator.GenerateUrl(ProductHandler.GetProducts,
category, page)
return url
}
}
func (handler ProductHandler) generatePageNumbers(pageCount
[]int) {
pages = make([]int, pageCount)
for i := 0; i < pageCount; i++ {
pages[i] = i + 1
}
return
}

int)

(pages

Листинг 35-28 Добавление поддержки фильтрации категорий в файле product_handler.go в папке store

Мне также пришлось обновить существующую функцию, которая генерирует URLадреса для выбора страницы, и ввести функцию, которая генерирует URL-адреса для
выбора новой категории.

Создание обработчика категории
Причина, по которой я добавил поддержку вызова обработчиков из шаблонов,
заключалась в том, что я мог отображать автономный контент, такой как кнопки
категорий. Добавьте файл с именем category_handler.go в папку sportsstore/store с
содержимым, показанным в листинге 35-29.
package store
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
)

type CategoryHandler struct {
Repository models.Repository
URLGenerator handling.URLGenerator
}
type categoryTemplateContext struct {
Categories []models.Category
SelectedCategory int
CategoryUrlFunc func(int) string
}
func
(handler
CategoryHandler)
GetButtons(selected
actionresults.ActionResult {
return actionresults.NewTemplateAction("category_buttons.html",
categoryTemplateContext {
Categories: handler.Repository.GetCategories(),
SelectedCategory: selected,
CategoryUrlFunc: handler.createCategoryFilterFunction(),
})
}

int)

func (handler CategoryHandler) createCategoryFilterFunction() func(int)
string {
return func(category int) string {
url,
_
:=
handler.URLGenerator.GenerateUrl(ProductHandler.GetProducts,
category, 1)
return url
}
}
Листинг 35-29 Содержимое файла category_handler.go в папке store

Набор категорий, для которых требуются кнопки, обработчик получает через
репозиторий, полученный как сервис. Выбранная категория получается черезпараметр
метода-обработчика.
Чтобы создать шаблон, отображаемый методом обработчика GetButtons, добавьте
файл с именем category_buttons.html в папку templates с содержимым, показанным в
листинге 35-30.
{{ $context := . }}

All
{{ range $context.Categories }}
{{ .CategoryName
}}
{{ end }}

Листинг 35-30 Содержимое файла category_buttons.html в папке templates

Обычно я предпочитаю помещать полные элементы в предложения блоков
if/else/end, но, как показывает этот шаблон, вы можете использовать условие, чтобы
выбрать только ту часть элемента, которая отличается, в данном случае это атрибут class.
Хотя дублирования меньше, я нахожу это более трудным для чтения, но оно служит для
демонстрации того, что вы можете использовать систему шаблонов так, как это
соответствует вашим личным предпочтениям.

Отображение навигации по категориям в шаблоне списка товаров
В листинге 35-31 показаны изменения, необходимые для шаблона, в котором перечислены
продукты, чтобы включить функции фильтра категорий.
{{ layout "store_layout.html" }}
{{ define "left_column" }}
{{ $context := . }}
{{ handler "category" "getbuttons" $context.SelectedCategory}}
{{end}}
{{ define "right_column" }}
{{ $context := . }}
{{ range $context.Products }}



{{ .Name }}

{{ printf "$%.2f" .Price }}



{{ .Description }}

{{ end }}
{{ template "page_buttons.html" $context }}
{{end}}
Листинг 35-31 Отображение категорий в файле product_list.html в папке templates

Изменения заменяют сообщение-заполнитель ответом от метода GetButtons,
определенного в листинге 35-30.

Регистрация обработчика и обновление псевдонимов

Последнее изменение заключается в обновлении псевдонимов, которые сопоставляют
URL-адреса с методом обработчика, как показано в листинге 35-32.
...
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
handling.HandlerEntry{ "", store.CategoryHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 0, 1).
AddMethodAlias("/products[/]?[A-z0-9]*?",
store.ProductHandler.GetProducts, 0, 1),
)
}
...
Листинг 35-32 Обновление псевдонимов маршрутов в файле main.go в папке

Скомпилируйте и выполните проект и запросите http://localhost:5000, и вы
увидите кнопки категорий и сможете выбирать продукты из одной категории, как
показано на рисунке 35-4.

Рисунок 35-4 Фильтрация по категории

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

36. SportsStore: корзина и база данных
В этой главе я продолжаю разработку приложения SportsStore, добавляя поддержку
корзины покупок и добавляя базу данных вместо временного репозитория,
созданного в главе 35.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех остальных
глав этой книги — с https://github.com/apress/pro-go. См. Главу 2 о том,
как получить помощь, если у вас возникнут проблемы с запуском примеров.

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

Определение модели корзины и репозитория
Чтобы определить тип данных тележки, создайте папку sportsstore/store/cart
и добавьте в нее файл с именем cart.go с содержимым, показанным в листинге
36-1.
package cart
import "sportsstore/models"
type CartLine struct {
models.Product
Quantity int
}
func (cl *CartLine) GetLineTotal() float64 {
return cl.Price * float64(cl.Quantity)
}
type Cart interface {
AddProduct(models.Product)
GetLines() []*CartLine
RemoveLineForProduct(id int)
GetItemCount() int
GetTotal() float64

Reset()
}
type BasicCart struct {
lines []*CartLine
}
func (cart *BasicCart) AddProduct(p models.Product) {
for _, line := range cart.lines {
if (line.Product.ID == p.ID) {
line.Quantity++
return
}
}
cart.lines = append(cart.lines, &CartLine{
Product: p, Quantity: 1,
})
}
func (cart *BasicCart) GetLines() []*CartLine {
return cart.lines
}
func (cart *BasicCart) RemoveLineForProduct(id int) {
for index, line := range cart.lines {
if (line.Product.ID == id) {
cart.lines = append(cart.lines[0: index], cart.lines[index
+ 1:]...)
}
}
}
func (cart *BasicCart) GetItemCount() (total int) {
for _, l := range cart.lines {
total += l.Quantity
}
return
}
func (cart *BasicCart) GetTotal() (total float64) {
for _, line := range cart.lines {
total += float64(line.Quantity) * line.Product.Price
}
return
}
func (cart *BasicCart) Reset() {
cart.lines = []*CartLine{}
}

Листинг 36-1 Содержимое файла cart.go в папке store/cart

Интерфейс Cart будет предоставляться как служба, и я определил структуру
BasicCart, которая реализует методы Cart с использованием среза. Чтобы
определить службу, добавьте файл с именем cart_service.go в папку
sportsstore/store/cart с содержимым, показанным в листинге 36-2.
package cart
import (
"platform/services"
"platform/sessions"
"sportsstore/models"
"encoding/json"
"strings"
)
const CART_KEY string = "cart"
func RegisterCartService() {
services.AddScoped(func(session sessions.Session) Cart {
lines := []*CartLine {}
sessionVal := session.GetValue(CART_KEY)
if strVal, ok := sessionVal.(string); ok {
json.NewDecoder(strings.NewReader(strVal)).Decode(&lines)
}
return &sessionCart{
BasicCart: &BasicCart{ lines: lines},
Session: session,
}
})
}
type sessionCart struct {
*BasicCart
sessions.Session
}
func (sc *sessionCart) AddProduct(p models.Product) {
sc.BasicCart.AddProduct(p)
sc.SaveToSession()
}
func (sc *sessionCart) RemoveLineForProduct(id int) {
sc.BasicCart.RemoveLineForProduct(id)
sc.SaveToSession()
}
func (sc *sessionCart) SaveToSession() {

}

builder := strings.Builder{}
json.NewEncoder(&builder).Encode(sc.lines)
sc.Session.SetValue(CART_KEY, builder.String())

func (sc *sessionCart) Reset() {
sc.lines = []*CartLine{}
sc.SaveToSession()
}
Листинг 36-2 Содержимое файла cart_service.go в папке store/cart

Структура sessionCart реагирует на изменения, добавляя JSON-представление
своих значений CartLine в сеанс. Функция RegisterCartService создает службу
Cart с ограниченной областью действия, которая создает sessionCart и заполняет
ее строки из данных сеанса JSON.

Создание обработчика запроса корзины
Добавьте файл с именем cart_handler.go в папку sportsstore/store с
содержимым, показанным в листинге 36-3.
package store
import (
"platform/http/actionresults"
"platform/http/handling"
"sportsstore/models"
"sportsstore/store/cart"
)
type CartHandler struct {
models.Repository
cart.Cart
handling.URLGenerator
}
type CartTemplateContext struct {
cart.Cart
ProductListUrl string
CartUrl string
CheckoutUrl string
RemoveUrl string
}
func (handler CartHandler) GetCart() actionresults.ActionResult {
return
actionresults.NewTemplateAction("cart.html",
CartTemplateContext {
Cart: handler.Cart,

ProductListUrl:
handler.mustGenerateUrl(ProductHandler.GetProducts, 0, 1),
RemoveUrl:
handler.mustGenerateUrl(CartHandler.PostRemoveFromCart),
})
}
type CartProductReference struct {
ID int
}
func (handler CartHandler) PostAddToCart(ref CartProductReference)
actionresults.ActionResult {
p := handler.Repository.GetProduct(ref.ID)
handler.Cart.AddProduct(p)
return actionresults.NewRedirectAction(
handler.mustGenerateUrl(CartHandler.GetCart))
}
func (handler CartHandler) PostRemoveFromCart(ref CartProductReference)
actionresults.ActionResult {
handler.Cart.RemoveLineForProduct(ref.ID)
return actionresults.NewRedirectAction(
handler.mustGenerateUrl(CartHandler.GetCart))
}
func (handler CartHandler) mustGenerateUrl(method interface{}, data
...interface{}) string {
url, err := handler.URLGenerator.GenerateUrl(method, data...)
if (err != nil) {
panic(err)
}
return url
}
Листинг 36-3 Содержимое файла cart_handler.go в папке store

Метод GetCart отображает шаблон, отображающий содержимое корзины
пользователя. Будет вызван метод PostAddToCart для добавления товара в корзину,
после чего браузер будет перенаправлен на метод GetCart. Чтобы создать шаблон,
используемый методом GetCart, добавьте файл с именем cart.html в папку
шаблонов с содержимым, показанным в листинге 36-4.
{{ layout "simple_layout.html" }}
{{ $context := . }}

Your cart




QuantityItem
Price
Subtotal




{{ range $context.Cart.GetLines }}

{{ .Quantity }}
{{ .Name }}
{{ printf "$%.2f" .Price }}



{{ printf "$%.2f" .GetLineTotal }}





Remove




{{ end }}



Total:

{{ printf "$%.2f" $context.Cart.GetTotal }}






Continue shopping



Листинг 36-4 Содержимое файла cart.html в папке templates

Этот шаблон создает таблицу HTML со строками для каждого из продуктов,
выбранных пользователем. Также есть кнопка, которая возвращает пользователя к
списку продуктов, чтобы можно было сделать дальнейший выбор. Чтобы создать
макет, используемый для этого шаблона, добавьте файл с именем
simple_layout.html в папку templates с содержимым, показанным в листинге
36-5.




SportsStore






SPORTS STORE



{{ body }}


Листинг 36-5 Содержимое файла simple_layout.html в папке templates

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

Добавление товаров в корзину
Каждый продукт будет отображаться с кнопкой Add To Cart, которая отправит
запрос методу PostAddToCart, созданному в листинге 36-3. Сначала добавьте
элементы, показанные в листинге 36-6, которые определяют кнопку и форму,
которую она отправляет.
{{ layout "store_layout.html" }}
{{ define "left_column" }}
{{ $context := . }}
{{ handler "category" "getbuttons" $context.SelectedCategory}}
{{end}}
{{ define "right_column" }}
{{ $context := . }}
{{ range $context.Products }}




{{ .Name }}

{{ printf "$%.2f" .Price }}





{{ .Description }}


Add To Cart




{{ end }}
{{ template "page_buttons.html" $context }}
{{end}}
Листинг 36-6 Добавление формы в файл product_list.html в папке templates

Чтобы предоставить шаблону URL-адрес, который используется в форме,
внесите изменения, показанные в листинге 36-7, в его обработчик.
package store
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
"math"
)
const pageSize = 4
type ProductHandler struct {
Repository models.Repository
URLGenerator handling.URLGenerator
}
type ProductTemplateContext struct {
Products []models.Product
Page int

PageCount int
PageNumbers []int
PageUrlFunc func(int) string
SelectedCategory int
AddToCartUrl string
}
func (handler ProductHandler) GetProducts(category,
page int) actionresults.ActionResult {
prods, total := handler.Repository.GetProductPageCategory(category,
page, pageSize)
pageCount := int(math.Ceil(float64(total) / float64(pageSize)))
return actionresults.NewTemplateAction("product_list.html",
ProductTemplateContext {
Products: prods,
Page: page,
PageCount: pageCount,
PageNumbers: handler.generatePageNumbers(pageCount),
PageUrlFunc: handler.createPageUrlFunction(category),
SelectedCategory: category,
AddToCartUrl: mustGenerateUrl(handler.URLGenerator,
CartHandler.PostAddToCart),
})
}
func (handler ProductHandler) createPageUrlFunction(category
func(int) string {
return func(page int) string {
url,
_
handler.URLGenerator.GenerateUrl(ProductHandler.GetProducts,
category, page)
return url
}
}

int)
:=

func (handler ProductHandler) generatePageNumbers(pageCount int) (pages
[]int) {
pages = make([]int, pageCount)
for i := 0; i < pageCount; i++ {
pages[i] = i + 1
}
return
}
func
mustGenerateUrl(generator
handling.URLGenerator,
interface{}) string {
url, err := generator.GenerateUrl(target)
if (err != nil) {
panic(err)
}

target

return url;
}
Листинг 36-7 Добавление данных контекста в файл product_handler.go в папке store

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

Настройка приложения
Последним шагом для запуска базовой функции корзины является настройка
служб, промежуточного программного обеспечения и обработчика, необходимых
для сеансов и корзины, как показано в листинге 36-8.
package main
import (
"sync"
"platform/http"
"platform/http/handling"
"platform/services"
"platform/pipeline"
"platform/pipeline/basic"
"sportsstore/store"
"sportsstore/models/repo"
"platform/sessions"
"sportsstore/store/cart"
)
func registerServices() {
services.RegisterDefaultServices()
repo.RegisterMemoryRepoService()
sessions.RegisterSessionService()
cart.RegisterCartService()
}
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
&sessions.SessionComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
handling.HandlerEntry{ "", store.CategoryHandler{}},
handling.HandlerEntry{ "", store.CartHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 0,
1).

}

)

AddMethodAlias("/products[/]?[A-z0-9]*?",
store.ProductHandler.GetProducts, 0, 1),

func main() {
registerServices()
results, err := services.Call(http.Serve, createPipeline())
if (err == nil) {
(results[0].(*sync.WaitGroup)).Wait()
} else {
panic(err)
}
}
Листинг 36-8 Настройка приложения для корзины в файле main.go в папке sportsstore

Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000. Продукты показаны с помощью кнопки Add To Cart, при
нажатии которой продукт добавляется в корзину и перенаправляет браузер для
отображения содержимого корзины, как показано на рисунке 36-1.

Рисунок 36-1 Создание корзины магазина

Добавление виджета «Сводка корзины»
Пользователи ожидают увидеть сводную информацию о выбранных ими
продуктах при просмотре списка доступных продуктов. Добавьте метод,
показанный в листинге 36-9, в обработчик запроса CartHandler.
package store
import (
"platform/http/actionresults"
"platform/http/handling"

)

"sportsstore/models"
"sportsstore/store/cart"

type CartHandler struct {
models.Repository
cart.Cart
handling.URLGenerator
}
type CartTemplateContext struct {
cart.Cart
ProductListUrl string
CartUrl string
}
func (handler CartHandler) GetCart() actionresults.ActionResult {
return
actionresults.NewTemplateAction("cart.html",
CartTemplateContext {
Cart: handler.Cart,
ProductListUrl:
handler.mustGenerateUrl(ProductHandler.GetProducts, 0, 1),
})
}
func (handler CartHandler) GetWidget() actionresults.ActionResult {
return actionresults.NewTemplateAction("cart_widget.html",
CartTemplateContext {
Cart: handler.Cart,
CartUrl: handler.mustGenerateUrl(CartHandler.GetCart),
})
}
// ...statements omitted for brevity...
Листинг 36-9 Добавление метода в файл cart_handler.go в папке store

Чтобы определить шаблон, используемый новым методом, добавьте файл с
именем cart_widget.html в папку шаблонов с содержимым, показанным в
листинге 36-10.
{{ $context := . }}
{{ $count := $context.Cart.GetItemCount }}

{{ if gt $count 0 }}
Your cart:
{{ $count }} item(s)
{{ printf "$%.2f" $context.Cart.GetTotal }}
{{ else }}
(empty cart)

{{ end }}




Листинг 36-10 Содержимое файла cart_widget.html в папке templates

Вызов обработчика и добавление таблицы стилей значков CSS
В листинге 36-10 вызывается метод GetWidget для вставки виджета корзины в
макет. Для шаблона виджета корзины требуется значок корзины покупок, который
предоставляется отличным пакетом Font Awesome. В главе 35 я скопировал CSSфайл Bootstrap, чтобы его можно было обслуживать, используя функции
статических файлов, предоставляемые веб-платформой, но для пакета Font
Awesome требуется несколько файлов, поэтому в листинге 36-11 добавлен элемент
ссылки с URL-адресом. для сети распространения контента. (Это означает, что вы
должны быть в сети, чтобы увидеть значки. См. https://fontawesome.com для
получения подробной информации о том, как загрузить файлы, которые можно
установить в папку sportsstore/files.)




SportsStore







SPORTS STORE

{{ handler "cart" "getwidget" }}






{{ template "left_column" . }}


{{ template "right_column" . }}





Листинг 36-11 Добавление ссылки на таблицу стилей в файл store_layout.html в папке templates

Скомпилируйте и запустите проект, и вы увидите виджет, отображаемый в
заголовке страницы. Виджет укажет, что корзина пуста. Нажмите одну из кнопок
Add To Cart, а затем нажмите кнопку Continue Shopping, чтобы увидеть результат
выбора продукта, показанный на рисунке 36-2.

Рисунок 36-2 Отображение виджета корзины

Использование репозитория базы данных
Большинство основных функций реализовано, и пришло время отказаться от
временного репозитория, который я создал в главе 35, и заменить его тем, который
использует постоянную базу данных. Я собираюсь использовать SQLite.
Используйте командную строку для запуска команды, показанной в листинге 3612, в папке sportsstore, чтобы загрузить и установить драйвер SQLite, который
также включает среду выполнения SQLite.
go get modernc.org/sqlite
Листинг 36-12 Установка драйвера SQLite и пакета базы данных

Создание типов репозиториев
Добавьте файл с именем sql_repo.go в папку models/repo с содержимым,
показанным в листинге 36-13, в котором определяются основные типы
репозитория SQL.
package repo

import (
"database/sql"
"platform/config"
"platform/logging"
"context"
)
type SqlRepository struct {
config.Configuration
logging.Logger
Commands SqlCommands
*sql.DB
context.Context
}
type SqlCommands struct {
Init,
Seed,
GetProduct,
GetProducts,
GetCategories,
GetPage,
GetPageCount,
GetCategoryPage,
GetCategoryPageCount *sql.Stmt
}
Листинг 36-13 Содержимое файла sql_repo.go в папке models/repo

Структура SqlRepository будет использоваться для реализации интерфейса
Repository и будет предоставляться остальной части приложения в качестве
службы. Эта структура определяет поле *sql.DB, обеспечивающее доступ к базе
данных, и поле Commands, представляющее собой набор полей *sql.Stmt, которые
будут заполнены подготовленными операторами, необходимыми для реализации
функций интерфейса Repository.

Открытие базы данных и загрузка команд SQL
В главе 26 я определил команды SQL как строки Go. В реальных проектах я
предпочитаю определять команды SQL в текстовых файлах с расширением .sql,
что означает, что мой редактор может выполнять проверку синтаксиса. Это
означает, что мне нужно открыть базу данных, а затем найти и обработать файлы
SQL, соответствующие полям, определенным структурой SqlCommands,
определенной в листинге 36-13. Добавьте файл с именем sql_loader.go в папку
models/repo с содержимым, показанным в листинге 36-14.
package repo
import (

)

"os"
"database/sql"
"reflect"
"platform/config"
"platform/logging"
_ "modernc.org/sqlite"

func openDB(config config.Configuration, logger logging.Logger) (db
*sql.DB,
commands *SqlCommands, needInit bool) {
driver := config.GetStringDefault("sql:driver_name", "sqlite")
connectionStr, found := config.GetString("sql:connection_str")
if !found {
logger.Panic("Cannot read SQL connection string from config")
return
}
if _, err := os.Stat(connectionStr); os.IsNotExist(err) {
needInit = true
}
var err error
if db, err = sql.Open(driver, connectionStr); err == nil {
commands = loadCommands(db, config, logger)
} else {
logger.Panic(err.Error())
}
return
}
func loadCommands(db *sql.DB, config config.Configuration,
logger logging.Logger) (commands *SqlCommands) {
commands = &SqlCommands {}
commandVal := reflect.ValueOf(commands).Elem()
commandType := reflect.TypeOf(commands).Elem()
for i := 0; i < commandType.NumField(); i++ {
commandName := commandType.Field(i).Name
logger.Debugf("Loading SQL command: %v", commandName)
stmt := prepareCommand(db, commandName, config, logger)
commandVal.Field(i).Set(reflect.ValueOf(stmt))
}
return commands
}
func
prepareCommand(db
*sql.DB,
command
string,
config
config.Configuration,
logger logging.Logger) *sql.Stmt {
filename, found := config.GetString("sql:commands:" + command)
if !found {
logger.Panicf("Config does not contain location for SQL
command: %v",

}

command)
}
data, err := os.ReadFile(filename)
if err != nil {
logger.Panicf("Cannot read SQL command file: %v", filename)
}
statement, err := db.Prepare(string(data))
if (err != nil) {
logger.Panicf(err.Error())
}
return statement

Листинг 36-14 Содержимое файла sql_loader.go в папке models/repo

Функция openDB считывает имя драйвера базы данных и строку подключения
из системы конфигурации и открывает базу данных перед вызовом функции
loadCommands. Функция loadCommands использует рефлексию для получения
списка полей, определенных структурой SqlCommands, и вызывает команду
prepareCommand для каждого из них. Функция prepareCommand получает имя
файла, содержащего SQL для команды из системы конфигурации, считывает
содержимое файла и создает подготовленный оператор, который присваивается
полю SqlCommands.

Определение начального числа и операторов инициализации
Для каждой функции, требуемой интерфейсом Repository, мне нужно определить
файл SQL, содержащий запрос, и определить метод Go, который будет его
выполнять. Я собираюсь начать с команд Seed и Init. Команда Seed требуется для
интерфейса репозитория, но функция Init специфична для структуры
SqlRepository и будет использоваться для создания схемы базы данных. Добавьте
файл с именем sql_initseed.go в папку models/repo с содержимым, показанным
в листинге 36-15.
Обратите внимание, что все запросы, используемые репозиторием, используют
методы, принимающие аргумент context.Context (ExecContext, QueryContext и
т. д.). Платформа, созданная в главах 32–34, передает значения Context
компонентам промежуточного программного обеспечения и обработчикам
запросов, поэтому я использовал их при выполнении запросов к базе данных.
package repo
func (repo *SqlRepository) Init() {
if _, err := repo.Commands.Init.ExecContext(repo.Context); err !=
nil {
repo.Logger.Panic("Cannot exec init command")
}
}

func (repo *SqlRepository) Seed() {
if _, err := repo.Commands.Seed.ExecContext(repo.Context); err !=
nil {
repo.Logger.Panic("Cannot exec seed command")
}
}
Листинг 36-15 Содержимое файла sql_initseed.go в папке models/repo

Чтобы создать SQL-команды, используемые этими методами, создайте папку
sportsstore/sql и добавьте в нее файл с именем init_db.sql с содержимым,
показанным в листинге 36-16.
DROP TABLE IF EXISTS Products;
DROP TABLE IF EXISTS Categories;
CREATE TABLE IF NOT EXISTS Categories (
Id INTEGER NOT NULL PRIMARY KEY,
);

Name TEXT

CREATE TABLE IF NOT EXISTS Products (
Id INTEGER NOT NULL PRIMARY KEY,
Name TEXT, Description TEXT,
Category INTEGER, Price decimal(8, 2),
CONSTRAINT CatRef FOREIGN KEY(Category) REFERENCES Categories (Id)
);
Листинг 36-16 Содержимое файла init_db.sql в папке sql

Этот файл содержит операторы, которые удаляют и воссоздают таблицы
Categories и Products. Добавьте файл seed_db.sql в папку sportsstore/sql с
содержимым, показанным в листинге 36-17.
INSERT INTO Categories(Id, Name) VALUES
(1, "Watersports"), (2, "Soccer"), (3, "Chess");
INSERT INTO Products(Id, Name, Description, Category, Price) VALUES
(1, "Kayak", "A boat for one person", 1, 275),
(2, "Lifejacket", "Protective and fashionable", 1, 48.95),
(3, "Soccer Ball", "FIFA-approved size and weight", 2, 19.50),
(4, "Corner Flags", "Give your playing field a professional
touch", 2, 34.95),
(5, "Stadium", "Flat-packed 35,000-seat stadium", 2, 79500),
(6, "Thinking Cap", "Improve brain efficiency by 75%", 3, 16),
(7, "Unsteady Chair", "Secretly give your opponent a
disadvantage", 3, 29.95),
(8, "Human Chess Board", "A fun game for the family", 3, 75),
(9, "Bling-Bling King", "Gold-plated, diamond-studded King", 3,
1200);
Листинг 36-17 Содержимое файла seed_db.sql в папке sql

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

Определение основных запросов
Чтобы завершить репозиторий, мне нужно проработать методы, требуемые
интерфейсом Repository, определить реализацию этого метода на Go и SQLзапрос, который он будет использовать. Добавьте файл с именем
sql_basic_methods.go в папку models/repo с содержимым, показанным в
листинге 36-18.
package repo
import "sportsstore/models"
func (repo *SqlRepository) GetProduct(id int) (p models.Product) {
row := repo.Commands.GetProduct.QueryRowContext(repo.Context, id)
if row.Err() == nil {
var err error
if p, err = scanProduct(row); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec GetProduct command: %v",
row.Err().Error())
}
return
}
func (repo *SqlRepository) GetProducts() (results []models.Product) {
rows, err := repo.Commands.GetProducts.QueryContext(repo.Context)
if err == nil {
if results, err = scanProducts(rows); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
return
}
} else {
repo.Logger.Panicf("Cannot exec GetProducts command: %v", err)
}
return
}
func (repo *SqlRepository) GetCategories() []models.Category {
results := make([]models.Category, 0, 10)
rows, err := repo.Commands.GetCategories.QueryContext(repo.Context)
if err == nil {
for rows.Next() {
c := models.Category{}
if err := rows.Scan(&c.ID, &c.CategoryName); err != nil {

repo.Logger.Panicf("Cannot scan data: %v", err.Error())
}
results = append(results, c)
}
} else {
repo.Logger.Panicf("Cannot exec GetCategories command: %v",
err)
}
return results
}
Листинг 36-18 Содержимое файла sql_basic_methods.go в папке models/repo

В листинге 36-18 реализованы методы GetProduct, GetProducts и
GetCategories. Чтобы определить функции, которые сканируют значения Product
из результатов SQL, добавьте файл с именем sql_scan.go в папку models/repo с
содержимым, показанным в листинге 36-19.
package repo
import (
"database/sql"
"sportsstore/models"
)
func scanProducts(rows *sql.Rows) (products []models.Product,
error) {
products = make([]models.Product, 0, 10)
for rows.Next() {
p := models.Product{ Category: &models.Category{}}
err = rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price,
&p.Category.ID, &p.Category.CategoryName)
if (err == nil) {
products = append(products, p)
} else {
return
}
}
return
}

err

func scanProduct(row *sql.Row) (p models.Product, err error) {
p = models.Product{ Category: &models.Category{}}
err = row.Scan(&p.ID, &p.Name, &p.Description, &p.Price,
&p.Category.ID,
&p.Category.CategoryName)
return p, err
}
Листинг 36-19 Содержимое файла sql_scan.go в папке models/repo

Функция scanProducts сканирует значения при наличии нескольких строк, а
функция scanProduct делает то же самое для результатов с одной строкой.

Определение файлов SQL для базовых запросов
Теперь идет процесс определения файлов SQL для каждого запроса. Добавьте
файл с именем get_product.sql в папку sportsstore/sql с содержимым,
показанным в листинге 36-20.
SELECT
Products.Id,
Products.Name,
Products.Price,
Categories.Id, Categories.Name
FROM Products, Categories
WHERE Products.Category = Categories.Id
AND Products.Id = ?

Products.Description,

Листинг 36-20 Содержимое файла get_product.sql в папке sql

Этот запрос создает одну строку, содержащую сведения о продукте с
указанным Id. Добавьте файл с именем get_products.sql в папку
sportsstore/sql с содержимым, показанным в листинге 36-21.
SELECT
Products.Id,
Products.Name,
Products.Price,
Categories.Id, Categories.Name
FROM Products, Categories
WHERE Products.Category = Categories.Id
ORDER BY Products.Id

Products.Description,

Листинг 36-21 Содержимое файла get_products.sql в папке sql

Этот запрос создает строки для всех продуктов в базе данных. Затем добавьте
файл с именем get_categories.sql в папку sportsstore/sql с содержимым,
показанным в листинге 36-22.
SELECT Categories.Id, Categories.Name
FROM Categories ORDER BY Categories.Id
Листинг 36-22 Содержимое файла get_categories.sql в папке sql

Этот запрос выбирает все строки в папке Categories.

Определение постраничных запросов
Методы для постраничных данных более сложны, поскольку они должны
выполнять один запрос для страницы данных и один запрос для получения общего
количества доступных результатов. Добавьте файл с именем sql_page_methods.go
в папку sportsstore/models/repo с содержимым, показанным в листинге 36-23.
package repo

import "sportsstore/models"
func (repo *SqlRepository) GetProductPage(page,
pageSize int) (products []models.Product, totalAvailable int) {
rows, err := repo.Commands.GetPage.QueryContext(repo.Context,
pageSize, (pageSize * page) - pageSize)
if err == nil {
if products, err = scanProducts(rows); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
return
}
} else {
repo.Logger.Panicf("Cannot exec GetProductPage command: %v",
err)
return
}
row := repo.Commands.GetPageCount.QueryRowContext(repo.Context)
if row.Err() == nil {
if err := row.Scan(&totalAvailable); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec GetPageCount command: %v",
row.Err().Error())
}
return
}
func (repo *SqlRepository) GetProductPageCategory(categoryId int, page,
pageSize int) (products []models.Product, totalAvailable int) {
if (categoryId == 0) {
return repo.GetProductPage(page, pageSize)
}
rows,
err
:=
repo.Commands.GetCategoryPage.QueryContext(repo.Context, categoryId,
pageSize, (pageSize * page) - pageSize)
if err == nil {
if products, err = scanProducts(rows); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
return
}
} else {
repo.Logger.Panicf("Cannot exec GetProductPage command: %v",
err)
return
}
row
:=
repo.Commands.GetCategoryPageCount.QueryRowContext(repo.Context,
categoryId)
if row.Err() == nil {

if err := row.Scan(&totalAvailable); err != nil {
repo.Logger.Panicf("Cannot scan data: %v", err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec GetCategoryPageCount command:
%v",
row.Err().Error())
}
return
}
Листинг 36-23 Содержимое файла sql_page_methods.go в папке models/repo

Чтобы определить основной
SQL-запрос, используемый методом
GetProductPage, добавьте файл с именем get_product_page.sql в папку
sportsstore/sql с содержимым, показанным в листинге 36-24.
SELECT
Products.Id,
Products.Name,
Products.Price,
Categories.Id, Categories.Name
FROM Products, Categories
WHERE Products.Category = Categories.Id
ORDER BY Products.Id
LIMIT ? OFFSET ?

Products.Description,

Листинг 36-24 Содержимое файла get_product_page.sql в папке sql

Чтобы определить запрос, используемый для получения общего количества
продуктов в базе данных, добавьте файл с именем get_page_count.sql в папку
sportsstore/sql с содержимым, показанным в листинге 36-25.
SELECT COUNT (Products.Id)
FROM Products, Categories
WHERE Products.Category = Categories.Id;
Листинг 36-25 Содержимое файла get_page_count.sql в папке sql

Чтобы
определить
основной
запрос,
используемый
методом
GetProductPageCategory,
добавьте
файл
с
именем
get_category_product_page.sql в папку sportsstore/sql с содержимым,
показанным в листинге 36-26.
SELECT
Products.Id,
Products.Name,
Products.Price,
Categories.Id, Categories.Name
FROM Products, Categories
WHERE Products.Category = Categories.Id AND
?
ORDER BY Products.Id
LIMIT ? OFFSET ?

Products.Description,

Products.Category =

Листинг 36-26 Содержимое файла get_category_product_page.sql в папке sql

Чтобы определить запрос, определяющий количество товаров в определенной
категории, добавьте файл с именем get_category_product_page_count.sql в
папку sportsstore/sql с содержимым, показанным в листинге 36-27.
SELECT COUNT (Products.Id)
FROM Products, Categories
WHERE Products.Category = Categories.Id AND Products.Category = ?
Листинг 36-27 Содержимое файла get_category_product_page_count.sql в папке sql

Определение службы репозитория SQL
Чтобы определить функцию, которая будет регистрировать службу репозитория,
добавьте файл с именем sql_service.go в папку sportssstore/models/repo с
содержимым, показанным в листинге 36-28.
package repo
import (
"sync"
"context"
"database/sql"
"platform/services"
"platform/config"
"platform/logging"
"sportsstore/models"
)
func RegisterSqlRepositoryService() {
var db *sql.DB
var commands *SqlCommands
var needInit bool
loadOnce := sync.Once {}
resetOnce := sync.Once {}
services.AddScoped(func
(ctx
context.Context,
config.Configuration,
logger logging.Logger) models.Repository {
loadOnce.Do(func () {
db, commands, needInit = openDB(config, logger)
})
repo := &SqlRepository{
Configuration: config,
Logger: logger,
Commands: *commands,
DB: db,
Context: ctx,
}
resetOnce.Do(func() {

config

if needInit || config.GetBoolDefault("sql:always_reset",
true) {

repo.Init()
repo.Seed()

}
})
return repo
}

})

Листинг 36-28 Содержимое файла sql_service.go в папке models/repo

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

Настройка приложения для использования репозитория SQL
В листинге 36-29 определены параметры конфигурации, которые указывают
расположение файлов SQL. Код, который загружает эти файлы, будет паниковать,
если эти файлы не могут быть загружены, поэтому важно убедиться, что
указанные пути совпадают с теми, которые использовались для создания файлов.
{

"logging" : {
"level": "debug"
},
"files": {
"path": "files"
},
"templates": {
"path": "templates/*.html",
"reload": true
},
"sessions": {
"key": "MY_SESSION_KEY",
"cyclekey": true
},
"sql": {
"connection_str": "store.db",
"always_reset": true,
"commands": {
"Init":
"sql/init_db.sql",
"Seed":
"sql/seed_db.sql",
"GetProduct":
"sql/get_product.sql",
"GetProducts":
"sql/get_products.sql",
"GetCategories":
"sql/get_categories.sql",
"GetPage":
"sql/get_product_page.sql",

"GetPageCount":
"GetCategoryPage":

"sql/get_page_count.sql",
"sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql"
}
}
}
Листинг 36-29 Определение параметров конфигурации в файле config.json в папке sportsstore

Последним изменением является регистрация репозитория SQL, чтобы он
использовался для разрешения зависимостей в интерфейсе репозитория, и
закомментирование оператора, регистрирующего временный репозиторий, как
показано в листинге 36-30.
...
func registerServices() {
services.RegisterDefaultServices()
//repo.RegisterMemoryRepoService()
repo.RegisterSqlRepositoryService()
sessions.RegisterSessionService()
cart.RegisterCartService()
}
...
Листинг 36-30 Изменение службы репозитория в файле main.go в папке sportsstore

Скомпилируйте и выполните проект и используйте браузер для запроса
http://localhost:5000, и вы увидите данные, которые считываются из базы
данных, как показано на рисунке 36-3.

Рисунок 36-3 Использование данных из базы данных

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

37. SportsStore: оформление заказа и
администрирование
В этой главе я продолжаю разработку приложения SportsStore, добавляя
процесс оформления заказа и приступая к работе над функциями
администрирования.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/pro-go. См.
Главу 2 о том, как получить помощь, если у вас возникнут проблемы с
запуском примеров.

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

Определение модели
Чтобы определить тип, который будет представлять сведения о доставке
пользователя и выбранные продукты, добавьте файл с именем order.go в
папку models с содержимым, показанным в листинге 37-1.
package models
type Order struct {
ID int
ShippingDetails
Products []ProductSelection
Shipped bool
}

type ShippingDetails struct {
Name string `validation:"required"`
StreetAddr string `validation:"required"`
City string `validation:"required"`
State string `validation:"required"`
Zip string `validation:"required"`
Country string `validation:"required"`
}
type ProductSelection struct{
Quantity int
Product
}
Листинг 37-1 Содержимое файла order.go в папке models

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

Расширение репозитория
Следующим шагом является расширение репозитория, чтобы его можно было
использовать для хранения и извлечения заказов. Добавьте методы, показанные
в листинге 37-2, в файл репозиторий.go в папке sportsstore/models.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
GetProductPage(page,
totalAvailable int)

pageSize

int)

(products

[]Product,

GetProductPageCategory(categoryId int, page, pageSize int)
(products []Product,
totalAvailable int)
GetCategories() []Category
GetOrder(id int) Order
GetOrders() []Order
SaveOrder(*Order)

}

Seed()

Листинг 37-2 Добавление методов интерфейса в файл репозитория.go в папке models

В листинге 37-3 показаны изменения, необходимые для файла SQL, которые
создают новые таблицы для хранения данных заказа.
DROP
DROP
DROP
DROP

TABLE
TABLE
TABLE
TABLE

IF
IF
IF
IF

EXISTS
EXISTS
EXISTS
EXISTS

OrderLines;
Orders;
Products;
Categories;

CREATE TABLE IF NOT EXISTS Categories (
Id INTEGER NOT NULL PRIMARY KEY,
);

Name TEXT

CREATE TABLE IF NOT EXISTS Products (
Id INTEGER NOT NULL PRIMARY KEY,
Name TEXT, Description TEXT,
Category INTEGER, Price decimal(8, 2),
CONSTRAINT CatRef FOREIGN KEY(Category) REFERENCES Categories
(Id)
);
CREATE TABLE IF NOT EXISTS OrderLines (
Id INTEGER NOT NULL PRIMARY KEY,
OrderId INT, ProductId INT, Quantity INT,
CONSTRAINT OrderRef FOREIGN KEY(ProductId) REFERENCES Products
(Id)
CONSTRAINT OrderRef FOREIGN KEY(OrderId) REFERENCES Orders (Id)
);
CREATE TABLE IF NOT EXISTS Orders (
Id INTEGER NOT NULL PRIMARY KEY,
Name TEXT NOT NULL,
StreetAddr TEXT NOT NULL,
City TEXT NOT NULL,
Zip TEXT NOT NULL,
Country TEXT NOT NULL,
Shipped BOOLEAN
);
Листинг 37-3 Добавление таблиц в файл init_db.sql в папку sql

Чтобы определить некоторые начальные данные, добавьте операторы,
показанные в листинге 37-4, в файл seed_db.sql в папке sportsstore/sql.

INSERT INTO Categories(Id, Name) VALUES
(1, "Watersports"), (2, "Soccer"), (3, "Chess");
INSERT INTO Products(Id, Name, Description, Category, Price) VALUES
(1, "Kayak", "A boat for one person", 1, 275),
(2, "Lifejacket", "Protective and fashionable", 1, 48.95),
(3, "Soccer Ball", "FIFA-approved size and weight", 2,
19.50),
(4, "Corner Flags", "Give your playing field a professional
touch", 2, 34.95),
(5, "Stadium", "Flat-packed 35,000-seat stadium", 2, 79500),
(6, "Thinking Cap", "Improve brain efficiency by 75%", 3,
16),
(7, "Unsteady Chair", "Secretly give your opponent a
disadvantage", 3, 29.95),
(8, "Human Chess Board", "A fun game for the family", 3,
75),
(9, "Bling-Bling King", "Gold-plated, diamond-studded King",
3, 1200);
INSERT INTO Orders(Id, Name, StreetAddr, City, Zip, Country,
Shipped) VALUES
(1, "Alice", "123 Main St", "New Town", "12345", "USA",
false),
(2, "Bob", "The Grange", "Upton", "UP12 6YT", "UK", false);
INSERT INTO OrderLines(Id, OrderId, ProductId, Quantity) VALUES
(1, 1, 1, 1), (2, 1, 2, 2), (3, 1, 8, 1), (4, 2, 5, 2);
Листинг 37-4 Добавление начальных данных в файл seed_db.sql в папке sql

Отключение временного репозитория
Временный репозиторий, созданный в главе 35, больше не определяет все
методы, указанные в интерфейсе Repository. В реальном проекте я обычно
переключаюсь обратно на репозиторий памяти при добавлении новой функции,
например заказов, а затем снова переключаюсь на SQL, как только понимаю,
что требуется. Но для этого проекта я просто закомментирую код, создающий
сервис в памяти, как показано в листинге 37-5, чтобы он не вызывал ошибки
компилятора.
package repo
import (
//
"platform/services"
"sportsstore/models"
"math"
)

// func RegisterMemoryRepoService() {
//
services.AddSingleton(func() models.Repository {
//
repo := &MemoryRepo{}
//
repo.Seed()
//
return repo
//
})
// }
type MemoryRepo struct {
products []models.Product
categories []models.Category
}
// ...other statements omitted for brevity...
Листинг 37-5 Комментирующий код в файле memory_repo.go в папке models/repo

Определение методов и команд репозитория
Следующим шагом является определение и реализация новых методов
Repository и файлов SQL, на которые они будут опираться. В листинге 37-6 к
структуре, используемой для загрузки файлов SQL для базы данных,
добавлены новые команды.
package repo
import (
"database/sql"
"platform/config"
"platform/logging"
"context"
)
type SqlRepository struct {
config.Configuration
logging.Logger
Commands SqlCommands
*sql.DB
context.Context
}
type SqlCommands struct {
Init,
Seed,
GetProduct,
GetProducts,
GetCategories,
GetPage,

}

GetPageCount,
GetCategoryPage,
GetCategoryPageCount,
GetOrder,
GetOrderLines,
GetOrders,
GetOrdersLines,
SaveOrder,
SaveOrderLine *sql.Stmt

Листинг 37-6 Добавление команд в файл sql_repo.go в папке models/repo

Определение файлов SQL
Добавьте файл с именем get_order.sql в папку sportsstore/sql с
содержимым, показанным в листинге 37-7.
SELECT Orders.Id, Orders.Name, Orders.StreetAddr,
Orders.Zip,
Orders.Country, Orders.Shipped
FROM Orders
WHERE Orders.Id = ?

Orders.City,

Листинг 37-7 Содержимое файла get_order.sql в папке sql

Этот запрос извлекает детали заказа. Чтобы определить запрос, который
будет получать сведения о заказанных продуктах, добавьте файл с именем
get_order_lines.sql в папку sportsstore/sql с содержимым, показанным в
листинге 37-8.
SELECT
OrderLines.Quantity,
Products.Id,
Products.Description,
Products.Price, Categories.Id, Categories.Name
FROM Orders, OrderLines, Products, Categories
WHERE Orders.Id = OrderLines.OrderId
AND OrderLines.ProductId = Products.Id
AND Products.Category = Categories.Id
AND Orders.Id = ?
ORDER BY Products.Id

Products.Name,

Листинг 37-8 Содержимое файла get_order_lines.sql в папке sql

Чтобы определить запрос, который будет получать все заказы в базе
данных, добавьте файл с именем get_orders.sql в папку sportsstore/sql с
содержимым, показанным в листинге 37-9.
SELECT Orders.Id, Orders.Name, Orders.StreetAddr,
Orders.Zip, Orders.Country, Orders.Shipped

Orders.City,

FROM Orders
ORDER BY Orders.Shipped, Orders.Id
Листинг 37-9 Содержимое папки get_orders.sql в папке sql

Чтобы определить запрос, который будет получать все сведения о
продуктах, связанных со всеми заказами, добавьте файл с именем
get_orders_lines.sql в папку sportsstore/sql с содержимым, показанным в
листинге 37-10.
SELECT Orders.Id, OrderLines.Quantity, Products.Id, Products.Name,
Products.Description,
Products.Price,
Categories.Id,
Categories.Name
FROM Orders, OrderLines, Products, Categories
WHERE Orders.Id = OrderLines.OrderId
AND OrderLines.ProductId = Products.Id
AND Products.Category = Categories.Id
ORDER BY Orders.Id
Листинг 37-10 Содержимое файла get_orders_lines.sql в папке sql

Чтобы определить оператор, в котором будет храниться заказ, добавьте
файл с именем save_order.sql в папку sportsstore/sql с содержимым,
показанным в листинге 37-11.
INSERT INTO Orders(Name, StreetAddr, City, Zip, Country, Shipped)
VALUES (?, ?, ?, ?, ?, ?)
Листинг 37-11 Содержимое файла save_order.sql в папке sql

Чтобы определить оператор, в котором будут храниться сведения о выборе
продукта, связанного с заказом, добавьте файл с именем save_order_line.sql
в папку sportsstore/sql с содержимым, показанным в листинге 37-12.
INSERT INTO OrderLines(OrderId, ProductId, Quantity)
VALUES (?, ?, ?)
Листинг 37-12 Содержимое файла save_order_line.sql в папке sql

В листинге 37-13 добавлены параметры конфигурации для новых файлов
SQL.
...
"sql": {
"connection_str": "store.db",
"always_reset": true,
"commands": {
"Init":
"sql/init_db.sql",
"Seed":
"sql/seed_db.sql",

"GetProduct":
"GetProducts":
"GetCategories":
"GetPage":
"GetPageCount":
"GetCategoryPage":

"sql/get_product.sql",
"sql/get_products.sql",
"sql/get_categories.sql",
"sql/get_product_page.sql",
"sql/get_page_count.sql",
"sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql",
"GetOrder": "sql/get_order.sql",
"GetOrderLines": "sql/get_order_lines.sql",
"GetOrders": "sql/get_orders.sql",
"GetOrdersLines": "sql/get_orders_lines.sql",
"SaveOrder": "sql/save_order.sql",
"SaveOrderLine": "sql/save_order_line.sql"
}
}
...
Листинг 37-13 Добавление настроек конфигурации в файл config.json в папке sportsstore

Реализация методов репозитория
Добавьте
файл
с
именем
sql_orders_one.go
в
папку
sportsstore/models/repo с содержимым, показанным в листинге 37-14.
package repo
import "sportsstore/models"
func (repo *SqlRepository) GetOrder(id int) (order models.Order) {
order = models.Order { Products: []models.ProductSelection {}}
row := repo.Commands.GetOrder.QueryRowContext(repo.Context, id)
if row.Err() == nil {
err := row.Scan(&order.ID, &order.Name, &order.StreetAddr,
&order.City,
&order.Zip, &order.Country, &order.Shipped)
if (err != nil) {
repo.Logger.Panicf("Cannot scan order data: %v",
err.Error())
return
}
lineRows,
err
:=
repo.Commands.GetOrderLines.QueryContext(repo.Context, id)
if (err == nil) {
for lineRows.Next() {
ps := models.ProductSelection {
Product: models.Product{ Category:
&models.Category{}},
}

err = lineRows.Scan(&ps.Quantity, &ps.Product.ID,
&ps.Product.Name,

&ps.Product.Description,&ps.Product.Price,
&ps.Product.Category.ID,
&ps.Product.Category.CategoryName)
if err == nil {
order.Products = append(order.Products, ps)
} else {
repo.Logger.Panicf("Cannot scan order line data:
%v",
err.Error())
}
}
} else {
repo.Logger.Panicf("Cannot exec GetOrderLines command:
%v", err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec GetOrder command: %v",
row.Err().Error())
}
return
}
Листинг 37-14 Содержимое файла sql_orders_one.go в папке models/repo

Этот метод запрашивает в базе данных заказ, а затем снова запрашивает
сведения о выборе продуктов, связанных с этим заказом. Затем добавьте файл с
именем sql_orders_all.go в папку sportsstore/models/repo с содержимым,
показанным в листинге 37-15.
package repo
import "sportsstore/models"
func (repo *SqlRepository) GetOrders() []models.Order {
orderMap := make(map[int]*models.Order, 10)
orderRows,
err
:=
repo.Commands.GetOrders.QueryContext(repo.Context)
if err != nil {
repo.Logger.Panicf("Cannot exec GetOrders command: %v",
err.Error())
}
for orderRows.Next() {
order := models.Order { Products: []models.ProductSelection
{}}
err := orderRows.Scan(&order.ID, &order.Name,
&order.StreetAddr, &order.City,

&order.Zip, &order.Country, &order.Shipped)
if (err != nil) {
repo.Logger.Panicf("Cannot scan order data: %v",
err.Error())
return []models.Order {}
}
orderMap[order.ID] = &order
}
lineRows,
err
:=
repo.Commands.GetOrdersLines.QueryContext(repo.Context)
if (err != nil) {
repo.Logger.Panicf("Cannot exec GetOrdersLines command: %v",
err.Error())
}
for lineRows.Next() {
var order_id int
ps := models.ProductSelection {
Product: models.Product{ Category: &models.Category{} },
}
err = lineRows.Scan(&order_id, &ps.Quantity, &ps.Product.ID,
&ps.Product.Name, &ps.Product.Description,
&ps.Product.Price,
&ps.Product.Category.ID,
&ps.Product.Category.CategoryName)
if err == nil {
orderMap[order_id].Products
=
append(orderMap[order_id].Products, ps)
} else {
repo.Logger.Panicf("Cannot scan order line data: %v",
err.Error())
}
}
orders := make([]models.Order, 0, len(orderMap))
for _, o := range orderMap {
orders = append(orders, *o)
}
return orders
}
Листинг 37-15 Содержимое файла sql_orders_all.go в папке models/repo

Этот метод запрашивает базу данных для всех заказов и связанных с ними
продуктов. Чтобы реализовать последний метод, добавьте файл с именем
sql_orders_save.go в папку sportsstore/models/repo с содержимым,
показанным в листинге 37-16.
package repo

import "sportsstore/models"
func (repo *SqlRepository) SaveOrder(order *models.Order) {
tx, err := repo.DB.Begin()
if err != nil {
repo.Logger.Panicf("Cannot create transaction: %v",
err.Error())
return
}
result, err := tx.StmtContext(repo.Context,
repo.Commands.SaveOrder).Exec(order.Name, order.StreetAddr,
order.City,
order.Zip, order.Country, order.Shipped)
if err != nil {
repo.Logger.Panicf("Cannot exec SaveOrder command: %v",
err.Error())
tx.Rollback()
return
}
id, err := result.LastInsertId()
if err != nil {
repo.Logger.Panicf("Cannot get inserted ID: %v",
err.Error())
tx.Rollback()
return
}
statement
:=
tx.StmtContext(repo.Context,
repo.Commands.SaveOrderLine)
for _, sel := range order.Products {
_, err := statement.Exec(id, sel.Product.ID, sel.Quantity)
if err != nil {
repo.Logger.Panicf("Cannot exec SaveOrderLine command:
%v", err.Error())
tx.Rollback()
return
}
}
err = tx.Commit()
if err != nil {
repo.Logger.Panicf("Transaction cannot be committed: %v",
err.Error())
err = tx.Rollback()
if err != nil {
repo.Logger.Panicf("Transaction cannot be rolled back:
%v", err.Error())
}
}
order.ID = int(id)

}
Листинг 37-16 Содержимое файла sql_orders_save.go в папке models/repo

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

Создание обработчика запросов и шаблонов
Следующим шагом является определение обработчика запросов, который
позволит пользователю предоставить информацию о доставке и оформить
заказ. Как отмечалось в начале этой главы, сохранение заказа завершит процесс
оформления заказа, хотя в реальных интернет-магазинах пользователю будет
предложено произвести оплату. Добавьте файл с именем order_handler.go в
папку sportsstore/store с содержимым, показанным в листинге 37-17.
package store
import (
"encoding/json"
"platform/http/actionresults"
"platform/http/handling"
"platform/sessions"
"platform/validation"
"sportsstore/models"
"sportsstore/store/cart"
"strings"
)
type OrderHandler struct {
cart.Cart
sessions.Session
Repository models.Repository
URLGenerator handling.URLGenerator
validation.Validator
}
type OrderTemplateContext struct {
models.ShippingDetails
ValidationErrors [][]string
CancelUrlstring
}
func (handler OrderHandler) GetCheckout() actionresults.ActionResult
{
context := OrderTemplateContext {}

jsonData := handler.Session.GetValueDefault("checkout_details",
"")

if jsonData != nil {

json.NewDecoder(strings.NewReader(jsonData.
(string))).Decode(&context)
}
context.CancelUrl = mustGenerateUrl(handler.URLGenerator,
CartHandler.GetCart)
return actionresults.NewTemplateAction("checkout.html", context)
}
func
(handler
OrderHandler)
PostCheckout(details
models.ShippingDetails) actionresults.ActionResult {
valid, errors := handler.Validator.Validate(details)
if (!valid) {
ctx := OrderTemplateContext {
ShippingDetails: details,
ValidationErrors: [][]string {},
}
for _, err := range errors {
ctx.ValidationErrors = append(ctx.ValidationErrors,
[]string { err.FieldName, err.Error.Error()})
}
builder := strings.Builder{}
json.NewEncoder(&builder).Encode(ctx)
handler.Session.SetValue("checkout_details",
builder.String())
redirectUrl := mustGenerateUrl(handler.URLGenerator,
OrderHandler.GetCheckout)
return actionresults.NewRedirectAction(redirectUrl)
} else {
handler.Session.SetValue("checkout_details", "")
}
order := models.Order {
ShippingDetails: details,
Products: []models.ProductSelection {},
}
for _, cl := range handler.Cart.GetLines() {
order.Products
=
append(order.Products,
models.ProductSelection {
Quantity: cl.Quantity,
Product: cl.Product,
})
}
handler.Repository.SaveOrder(&order)
handler.Cart.Reset()

targetUrl,
_
handler.URLGenerator.GenerateUrl(OrderHandler.GetSummary,
order.ID)
return actionresults.NewRedirectAction(targetUrl)
}

:=

func
(handler
OrderHandler)
GetSummary(id
int)
actionresults.ActionResult {
targetUrl,
_
:=
handler.URLGenerator.GenerateUrl(ProductHandler.GetProducts,
0, 1)
return actionresults.NewTemplateAction("checkout_summary.html",
struct {
ID int
TargetUrl string
}{ ID: id, TargetUrl: targetUrl})
}
Листинг 37-17 Содержимое файла order_handler.go в папке магазина

Этот обработчик определяет три метода. Метод GetCheckout отобразит
HTML-форму, позволяющую пользователю ввести данные о доставке, и
отобразит все ошибки проверки, возникшие в результате предыдущих попыток
оформления заказа.
Метод PostCheckout является целью формы, отображаемой методом
GetCheckout. Этот метод проверяет данные, предоставленные пользователем, и
при наличии ошибок перенаправляет браузер обратно к методу GetCheckout. Я
использую сеанс для передачи данных из метода PostCheckout в метод
GetCheckout, кодируя и декодируя данные как JSON, чтобы их можно было
сохранить в файле cookie сеанса.
Если ошибок проверки нет, метод PostCheckout создает Order, используя
сведения о доставке, предоставленные пользователем, и сведения о продукте,
полученные из Cart, которую обработчик получает в качестве услуги. Order
хранится с использованием репозитория, а браузер перенаправляется на метод
GetSummary, который отображает шаблон, отображающий сводку.
Чтобы создать шаблон сведений о доставке, добавьте файл с именем
checkout.html в папку sportsstore/templates с содержимым, показанным в
листинге 37-18.
{{ layout "simple_layout.html" }}
{{ $context := .}}
{{ $details := .ShippingDetails }}

Check out now
Please enter your details, and we'll ship your goods right away!


{{ if gt (len $context.ValidationErrors) 0}}

{{ range $context.ValidationErrors }}

{{ index . 0 }}: {{ index . 1 }}

{{ end }}

{{ end }}

Ship to

Name:



Street Address:



City:



State:



Zip:



Country:



Cancel

Submit



class="btn

btn-primary

m-1"

Листинг 37-18 Содержимое файла checkout.html в папке templates

Чтобы создать шаблон, отображаемый в конце процесса оформления заказа,
добавьте
файл
с
именем
checkout_summary.html
в
папку
sportsstore/templates с содержимым, показанным в листинге 37-19.
{{ layout "simple_layout.html" }}
{{ $context := . }}

Thanks!
Thanks for placing order #{{ $context.ID }}
We'll ship your goods as soon as possible.

Return to Store


Листинг 37-19 Содержимое файла checkout_summary.html в папке templates

Этот шаблон включает ссылку, которая вернет пользователя к списку
продуктов. Метод PostCheckout сбрасывает корзину пользователя, позволяя
пользователю снова начать процесс покупки.

Интеграция процесса оформления заказа
Чтобы пользователь мог начать процесс оформления заказа из сводки корзины,
внесите изменения, показанные в листинге 37-20.
...
func (handler CartHandler) GetCart() actionresults.ActionResult {
return
actionresults.NewTemplateAction("cart.html",
CartTemplateContext {
Cart: handler.Cart,
ProductListUrl:
handler.mustGenerateUrl(ProductHandler.GetProducts, 0, 1),
RemoveUrl:
handler.mustGenerateUrl(CartHandler.PostRemoveFromCart),
CheckoutUrl:
handler.mustGenerateUrl(OrderHandler.GetCheckout),
})
}
...

Листинг 37-20 Добавление свойства контекста в файл cart_handler.go в папке store

Это изменение задает значение свойства context, чтобы дать шаблону
URL-адрес для нацеливания на обработчик проверки. В листинге 37-21
добавлена ссылка, использующая URL.
...


Continue shopping

Checkout

...
Листинг 37-21 Добавление элемента в файл cart.html в папку templates

Регистрация обработчика запросов
В листинге 37-22 обработчик запросов регистрируется, чтобы он мог получать
запросы.
...
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
&sessions.SessionComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
handling.HandlerEntry{ "", store.CategoryHandler{}},
handling.HandlerEntry{ "", store.CartHandler{}},
handling.HandlerEntry{ "", store.OrderHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 0,
1).
AddMethodAlias("/products[/]?[A-z0-9]*?",
store.ProductHandler.GetProducts, 0, 1),
)
}
...
Листинг 37-22 Регистрация нового обработчика в файле main.go в папке sportsstore

Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000. Добавьте товары в корзину и нажмите кнопку
Checkout, после чего появится форма, показанная на рисунке 37-1.

Рисунок 37-1 Процесс оформления заказа

Создание функций администрирования
В приложении SportsStore есть базовый процесс перечисления продуктов и
оформления заказа, и теперь пришло время создать функции
администрирования. Я собираюсь начать с некоторых базовых шаблонов и
обработчиков, которые создают замещающий контент.
Создайте папку sportsstore/admin и добавьте в нее файл main_handler.go
с содержимым, показанным в листинге 37-23.
package admin
import (
"platform/http/actionresults"
"platform/http/handling"
)
var sectionNames = []string { "Products", "Categories", "Orders",
"Database"}
type AdminHandler struct {
handling.URLGenerator
}
type AdminTemplateContext struct {
Sections []string
ActiveSection string
SectionUrlFunc func(string) string

}
func
(handler
AdminHandler)
GetSection(section
string)
actionresults.ActionResult {
return
actionresults.NewTemplateAction("admin.html",
AdminTemplateContext {
Sections: sectionNames,
ActiveSection: section,
SectionUrlFunc: func(sec string) string {
sectionUrl,
_
:=
handler.GenerateUrl(AdminHandler.GetSection, sec)
return sectionUrl
},
})
}
Листинг 37-23 Содержимое файла main_handler.go в папке admin

Целью этого обработчика является отображение шаблона общих функций
администрирования с кнопками для перемещения между различными
разделами функций. Добавьте файл с именем admin.html в папку
sportsstore/templates с содержимым, показанным в листинге 37-24.
{{ $context := . }}




SportsStore






SPORTS STORE
Administration






{{ range $context.Sections }}

{{ else }}

class="btn btn-outline-info">
{{ end }}
{{ . }}

{{ end }}



{{ if eq $context.ActiveSection ""}}

Welcome to the SportsStore Administration
Features

{{ else }}
{{ handler $context.ActiveSection "getdata" }}
{{ end }}




Листинг 37-24 Содержимое файла admin.html в папке templates

Этот шаблон использует другую цветовую схему для обозначения функций
администрирования и отображает макет из двух столбцов с кнопками разделов
с одной стороны и выбранной функцией администрирования с другой.
Выбранная функция отображается с помощью функции handler.
Добавьте файл с именем products_handler.go в папку sportsstore/admin
с содержимым, показанным в листинге 37-25.
package admin
type ProductsHandler struct {}
func (handler ProductsHandler) GetData() string {
return "This is the products handler"
}
Листинг 37-25 Содержимое файла products_handler.go в папке admin

Добавьте файл с именем category_handler.go в папку sportsstore/admin
с содержимым, показанным в листинге 37-26.
package admin
type CategoriesHandler struct {}
func (handler CategoriesHandler) GetData() string {

return "This is the categories handler"
}
Листинг 37-26 Содержимое файла category_handler.go в папке admin

Добавьте файл с именем orders_handler.go в папку sportsstore/admin с
содержимым, показанным в листинге 37-27.
package admin
type OrdersHandler struct {}
func (handler OrdersHandler) GetData() string {
return "This is the orders handler"
}
Листинг 37-27 Содержимое файла orders_handler.go в папке admin

Чтобы завершить набор обработчиков, добавьте файл с именем
database_handler.go в папку sportsstore/admin с содержимым, показанным
в листинге 37-28.
package admin
type DatabaseHandler struct {}
func (handler DatabaseHandler) GetData() string {
return "This is the database handler"
}
Листинг 37-28 Содержимое файла database_handler.go в папке admin

Я
добавлю
элементы
управления
доступом
для
функций
администрирования в главе 38, а сейчас я собираюсь зарегистрировать новые
обработчики, чтобы к ним мог получить доступ любой, как показано в
листинге 37-29.
package main
import (
"sync"
"platform/http"
"platform/http/handling"
"platform/services"
"platform/pipeline"
"platform/pipeline/basic"
"sportsstore/store"
"sportsstore/models/repo"

)

"platform/sessions"
"sportsstore/store/cart"
"sportsstore/admin"

func registerServices() {
services.RegisterDefaultServices()
//repo.RegisterMemoryRepoService()
repo.RegisterSqlRepositoryService()
sessions.RegisterSessionService()
cart.RegisterCartService()
}
func createPipeline() pipeline.RequestPipeline {
return pipeline.CreatePipeline(
&basic.ServicesComponent{},
&basic.LoggingComponent{},
&basic.ErrorComponent{},
&basic.StaticFileComponent{},
&sessions.SessionComponent{},
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
handling.HandlerEntry{ "", store.CategoryHandler{}},
handling.HandlerEntry{ "", store.CartHandler{}},
handling.HandlerEntry{ "", store.OrderHandler{}},
handling.HandlerEntry{ "admin", admin.AdminHandler{}},
handling.HandlerEntry{ "admin",
admin.ProductsHandler{}},
handling.HandlerEntry{ "admin",
admin.CategoriesHandler{}},
handling.HandlerEntry{ "admin", admin.OrdersHandler{}},
handling.HandlerEntry{ "admin",
admin.DatabaseHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts,
0, 1).
AddMethodAlias("/products[/]?[A-z0-9]*?",
store.ProductHandler.GetProducts, 0, 1).
AddMethodAlias("/admin[/]?",
admin.AdminHandler.GetSection, ""),
)
}
func main() {
registerServices()
results, err := services.Call(http.Serve, createPipeline())
if (err == nil) {
(results[0].(*sync.WaitGroup)).Wait()
} else {

panic(err)
}
}
Листинг 37-29 Регистрация обработчиков администрирования в файле main.go в папке sportsstore

Скомпилируйте и запустите проект и используйте браузер для запроса
http://localhost:5000/admin, что даст ответ, показанный на рисунке 37-2.
Нажатие кнопок навигации в левом столбце вызывает различные обработчики
в правом столбце.

Рисунок 37-2 Начало работы над функциями администрирования

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

Расширение репозитория
Первый шаг — расширить Repository, чтобы я мог вносить изменения в базу
данных. В листинге 37-30 к интерфейсу Repository добавлен новый метод.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
SaveProduct(*Product)
GetProductPage(page,
totalAvailable int)

pageSize

int)

(products

[]Product,

GetProductPageCategory(categoryId int, page, pageSize int)
(products []Product,
totalAvailable int)
GetCategories() []Category
GetOrder(id int) Order
GetOrders() []Order
SaveOrder(*Order)
Seed()
}
Листинг 37-30 Определение метода в файле repository.go в папке models

Чтобы определить SQL, который будет использоваться для хранения новых
продуктов, добавьте файл с именем save_product.sql в папку
sportsstore/sql с содержимым, показанным в листинге 37-31.
INSERT INTO Products(Name, Description, Category, Price)
VALUES (?, ?, ?, ?)
Листинг 37-31 Содержимое файла save_product.sql в папке sql

Чтобы определить SQL, который будет использоваться для изменения
существующих продуктов, добавьте файл с именем update_product.sql в
папку sportsstore/sql с содержимым, показанным в листинге 37-32.
UPDATE Products
SET Name = ?, Description = ?, Category = ?, Price =?
WHERE Id == ?
Листинг 37-32 Содержимое файла update_product.sql в папке sql

В листинге 37-33 добавлены новые команды, обеспечивающие доступ к
файлам SQL для изменения данных о продукте.
package repo
import (
"database/sql"
"platform/config"
"platform/logging"
"context"
)
type SqlRepository struct {
config.Configuration
logging.Logger

Commands SqlCommands
*sql.DB
context.Context
}
type SqlCommands struct {
Init,
Seed,
GetProduct,
GetProducts,
GetCategories,
GetPage,
GetPageCount,
GetCategoryPage,
GetCategoryPageCount,
GetOrder,
GetOrderLines,
GetOrders,
GetOrdersLines,
SaveOrder,
SaveOrderLine,
SaveProduct,
UpdateProduct *sql.Stmt
}
Листинг 37-33 Добавление команд в файл sql_repo.go в папке models/repo

В листинге 37-34 добавлены параметры конфигурации, указывающие
расположение файлов SQL для новых команд.
...
"sql": {
"connection_str": "store.db",
"always_reset": true,
"commands": {
"Init":
"sql/init_db.sql",
"Seed":
"sql/seed_db.sql",
"GetProduct":
"sql/get_product.sql",
"GetProducts":
"sql/get_products.sql",
"GetCategories":
"sql/get_categories.sql",
"GetPage":
"sql/get_product_page.sql",
"GetPageCount":
"sql/get_page_count.sql",
"GetCategoryPage":
"sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql",
"GetOrder":
"sql/get_order.sql",
"GetOrderLines":
"sql/get_order_lines.sql",
"GetOrders":
"sql/get_orders.sql",

"GetOrdersLines":
"SaveOrder":
"SaveOrderLine":
"SaveProduct":
"UpdateProduct":

"sql/get_orders_lines.sql",
"sql/save_order.sql",
"sql/save_order_line.sql",
"sql/save_product.sql",
"sql/update_product.sql"

}
}
...
Листинг 37-34 Добавление настроек конфигурации в файл config.json в папке sportsstore

Чтобы использовать команды SQL для реализации метода репозитория,
добавьте
файл
с
именем
sql_products_save.go
в
папку
sportsstore/models/repo с содержимым, показанным в листинге 37-35.
package repo
import "sportsstore/models"
func (repo *SqlRepository) SaveProduct(p *models.Product) {
if (p.ID == 0) {
result,
err
:=
repo.Commands.SaveProduct.ExecContext(repo.Context, p.Name,
p.Description, p.Category.ID, p.Price)
if err == nil {
id, err := result.LastInsertId()
if err == nil {
p.ID = int(id)
return
} else {
repo.Logger.Panicf("Cannot get inserted ID: %v",
err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec SaveProduct command:
%v", err.Error())
}
} else {
result,
err
:=
repo.Commands.UpdateProduct.ExecContext(repo.Context, p.Name,
p.Description, p.Category.ID, p.Price, p.ID)
if err == nil {
affected, err := result.RowsAffected()
if err == nil && affected != 1 {
repo.Logger.Panicf("Got unexpected rows affected:
%v", affected)
} else if err != nil {

repo.Logger.Panicf("Cannot get rows affected: %v",
err)
}
} else {
repo.Logger.Panicf("Cannot exec Update command: %v",
err.Error())
}
}
}
Листинг 37-35 Содержимое файла sql_products_save.go в папке models/repo

Если ID свойство Product, полученное этим методом, равно нулю, то
данные добавляются в базу данных; в противном случае выполняется
обновление.

Реализация обработчика запросов продуктов
Следующим шагом является удаление ответа-заполнителя из обработчика
запроса и добавление реальной функциональности, которая позволит
администратору просматривать и редактировать данные о Product. Замените
содержимое файла products_handler.go в папке sportsstore/admin
содержимым, показанным в листинге 37-36. (Убедитесь, что вы редактируете
файл в папке admin, а не файл в папке store с таким же именем.)
package admin
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
"platform/sessions"
)
type ProductsHandler struct {
models.Repository
handling.URLGenerator
sessions.Session
}
type ProductTemplateContext struct {
Products []models.Product
EditId int
EditUrl string
SaveUrl string
}
const PRODUCT_EDIT_KEY string = "product_edit"

func (handler ProductsHandler) GetData() actionresults.ActionResult {
return actionresults.NewTemplateAction("admin_products.html",
ProductTemplateContext {
Products: handler.GetProducts(),
EditId: handler.Session.GetValueDefault(PRODUCT_EDIT_KEY, 0).
(int),
EditUrl: mustGenerateUrl(handler.URLGenerator,
ProductsHandler.PostProductEdit),
SaveUrl: mustGenerateUrl(handler.URLGenerator,
ProductsHandler.PostProductSave),
})
}
type EditReference struct {
ID int
}
func (handler ProductsHandler) PostProductEdit(ref EditReference)
actionresults.ActionResult {
handler.Session.SetValue(PRODUCT_EDIT_KEY, ref.ID)
return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Products"))
}
type ProductSaveReference struct {
Id int
Name, Description string
Category int
Price float64
}
func (handler ProductsHandler) PostProductSave(
p ProductSaveReference) actionresults.ActionResult {
handler.Repository.SaveProduct(&models.Product{
ID: p.Id, Name: p.Name, Description: p.Description,
Category: &models.Category{ ID: p.Category },
Price: p.Price,
})
handler.Session.SetValue(PRODUCT_EDIT_KEY, 0)
return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Products"))
}
func mustGenerateUrl(gen handling.URLGenerator, target interface{},
data ...interface{}) string {

url, err := gen.GenerateUrl(target, data...)
if (err != nil) {
panic(err)
}
return url
}
Листинг 37-36 Добавление функций в файл products_handler.go в папке admin

Метод GetData отображает шаблон с именем admin_products.html с
данными контекста, которые содержат значения Product в базе данных,
значение int, используемое для обозначения ID продукта, который
пользователь хочет изменить, и URL-адреса, используемые для навигации.
Чтобы создать шаблон, добавьте файл с именем admin_products.html в папку
sportsstore/templates с содержимым, показанным в листинге 37-37.
{{ $context := . }}



IDNameDescription
CategoryPrice




{{ range $context.Products }}
{{ if ne $context.EditId .ID}}

{{ .ID }}
{{ .Name }}


{{ .Description }}


{{ .CategoryName }}
{{ printf "$%.2f" .Price }}





Edit





{{ else }}









{{ handler "categories" "getselect"
.Category.ID }}




Save




{{ end }}
{{ end }}

{{ if eq $context.EditId 0}}

Add New
Product


-



{{ handler "categories" "getselect" 0 }}




Save





{{ end }}

Листинг 37-37 Содержимое файла admin_products.html в папке templates

Этот шаблон создает HTML-шаблон, содержащий все продукты, а также
встроенный редактор для изменения существующих продуктов и еще один для
создания новых продуктов. Для обеих задач требуется элемент select, который
позволяет пользователю выбрать категорию, которая создается путем вызова
метода, определенного в CategoriesHandler. Листинг 37-38 добавляет этот
метод в обработчик запросов.
package admin
import (
"platform/http/actionresults"
"sportsstore/models"
)
type CategoriesHandler struct {
models.Repository
}
func (handler CategoriesHandler) GetData() string {
return "This is the categories handler"
}
func
(handler
CategoriesHandler)
GetSelect(current
int)
actionresults.ActionResult {
return actionresults.NewTemplateAction("select_category.html",
struct {
Current int

Categories []models.Category
}{ Current: current, Categories: handler.GetCategories()})
}
Листинг 37-38 Добавление поддержки для элемента Select в файле category_handler.go в папке
admin

Чтобы определить шаблон, используемый методом GetSelect, добавьте
файл с именем select_category.html в папку sportsstore/templates с
содержимым, показанным в листинге 37-39.
{{ $context := . }}

Select a category
{{ range $context.Categories }}

{{.CategoryName}}

{{ end }}

Листинг 37-39 Содержимое файла select_category.html в папке templates

Скомпилируйте и запустите проект, используйте браузер для запроса
http://localhost:5000/admin и нажмите кнопку Products. Вы увидите
список продуктов, который был прочитан из базы данных. Нажмите одну из
кнопок Edit, чтобы выбрать продукт для редактирования, введите новые
значения в поля формы и нажмите кнопку Submit, чтобы сохранить изменения
в базе данных, также показанные на рисунке 37-3.

Рисунок 37-3 Editing a product

Примечание
Приложение SportsStore настроено на сброс базы данных при каждом
запуске, а это означает, что любые изменения, которые вы вносите в базу
данных, будут отброшены. Я отключил эту функцию при подготовке
приложения к развертыванию в главе 38.
Если для редактирования не выбран ни один продукт, можно использовать
форму в нижней части таблицы для создания новых продуктов в базе данных,
как показано на рисунке 37-4.

Рисунок 37-4 Добавление продукта

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

Расширение репозитория
В листинге 37-40 к интерфейсу Repository добавлен метод, который будет
хранить Category.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
SaveProduct(*Product)
GetProductPage(page,
totalAvailable int)

pageSize

int)

(products

[]Product,

GetProductPageCategory(categoryId int, page, pageSize int)
(products []Product,
totalAvailable int)
GetCategories() []Category
SaveCategory(*Category)
GetOrder(id int) Order
GetOrders() []Order
SaveOrder(*Order)
Seed()
}
Листинг 37-40 Добавление метода в файл репозитория.go в папке models

Чтобы определить SQL, который будет использоваться для хранения новых
категорий в базе данных, добавьте файл с именем save_category.sql в папку
sportsstore/sql с содержимым, показанным в листинге 37-41.
INSERT INTO Categories(Name) VALUES (?)
Листинг 37-41 Содержимое файла save_category.sql в папке sql

Чтобы определить SQL, который будет использоваться для изменения
существующих категорий, добавьте файл с именем update_category.sql в
папку sportsstore/sql с содержимым, показанным в листинге 37-42.
UPDATE Categories SET Name = ? WHERE Id == ?
Листинг 37-42 Содержимое файла update_category.sql в папке sql

В листинге 37-43 добавлены новые команды, обеспечивающие доступ к
файлам SQL.
...
type SqlCommands struct {
Init,
Seed,
GetProduct,
GetProducts,
GetCategories,
GetPage,
GetPageCount,
GetCategoryPage,
GetCategoryPageCount,
GetOrder,
GetOrderLines,
GetOrders,
GetOrdersLines,
SaveOrder,
SaveOrderLine,
SaveProduct,
UpdateProduct,
SaveCategory,
UpdateCategory *sql.Stmt
}
...
Листинг 37-43 Добавление команд в файл sql_repo.go в папке models/repo

В листинге 37-44 добавлены параметры конфигурации, указывающие
расположение файлов SQL для новых команд.
...
"sql": {
"connection_str": "store.db",
"always_reset": true,
"commands": {
"Init":
"sql/init_db.sql",
"Seed":
"sql/seed_db.sql",
"GetProduct":
"sql/get_product.sql",
"GetProducts":
"sql/get_products.sql",
"GetCategories":
"sql/get_categories.sql",
"GetPage":
"sql/get_product_page.sql",
"GetPageCount":
"sql/get_page_count.sql",
"GetCategoryPage":
"sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql",
"GetOrder":
"sql/get_order.sql",

"GetOrderLines":
"GetOrders":
"GetOrdersLines":
"SaveOrder":
"SaveOrderLine":
"SaveProduct":
"UpdateProduct":
"SaveCategory":
"UpdateCategory":

"sql/get_order_lines.sql",
"sql/get_orders.sql",
"sql/get_orders_lines.sql",
"sql/save_order.sql",
"sql/save_order_line.sql",
"sql/save_product.sql",
"sql/update_product.sql",
"sql/save_category.sql",
"sql/update_category.sql"

}
}
...
Листинг 37-44 Добавление настроек конфигурации в файл config.json в папке sportsstore

Чтобы реализовать новый метод интерфейса, добавьте файл с именем
sql_category_save.go в папку sportsstore/models/repo с содержимым,
показанным в листинге 37-45.
package repo
import "sportsstore/models"
func (repo *SqlRepository) SaveCategory(c *models.Category) {
if (c.ID == 0) {
result,
err
:=
repo.Commands.SaveCategory.ExecContext(repo.Context,
c.CategoryName)
if err == nil {
id, err := result.LastInsertId()
if err == nil {
c.ID = int(id)
return
} else {
repo.Logger.Panicf("Cannot get inserted ID: %v",
err.Error())
}
} else {
repo.Logger.Panicf("Cannot exec SaveCategory command:
%v", err.Error())
}
} else {
result,
err
:=
repo.Commands.UpdateCategory.ExecContext(repo.Context,
c.CategoryName, c.ID)
if err == nil {
affected, err := result.RowsAffected()
if err == nil && affected != 1 {

repo.Logger.Panicf("Got unexpected rows affected:

%v", affected)
} else if err != nil {
repo.Logger.Panicf("Cannot get rows affected: %v",
err)
}
} else {
repo.Logger.Panicf("Cannot exec UpdateCategory command:
%v", err.Error())
}
}
}
Листинг 37-45 Содержимое файла sql_category_save.go в папке models/repo

Если свойство ID полученной этим методом Category равно нулю, то
данные добавляются в базу данных; в противном случае выполняется
обновление.

Реализация обработчика запроса категории
Замените содержимое файла category_handler.go в папке sportsstore/admin
кодом, показанным в листинге 37-46.
package admin
import (
"sportsstore/models"
"platform/http/actionresults"
"platform/http/handling"
"platform/sessions"
)
type CategoriesHandler struct {
models.Repository
handling.URLGenerator
sessions.Session
}
type CategoryTemplateContext struct {
Categories []models.Category
EditId int
EditUrl string
SaveUrl string
}
const CATEGORY_EDIT_KEY string = "category_edit"

func (handler CategoriesHandler) GetData() actionresults.ActionResult
{
return actionresults.NewTemplateAction("admin_categories.html",
CategoryTemplateContext {
Categories: handler.Repository.GetCategories(),
EditId:
handler.Session.GetValueDefault(CATEGORY_EDIT_KEY, 0).(int),
EditUrl: mustGenerateUrl(handler.URLGenerator,
CategoriesHandler.PostCategoryEdit),
SaveUrl: mustGenerateUrl(handler.URLGenerator,
CategoriesHandler.PostCategorySave),
})
}
func (handler CategoriesHandler) PostCategoryEdit(ref EditReference)
actionresults.ActionResult {
handler.Session.SetValue(CATEGORY_EDIT_KEY, ref.ID)
return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Categories"))
}
func (handler CategoriesHandler) PostCategorySave(
c models.Category) actionresults.ActionResult {
handler.Repository.SaveCategory(&c)
handler.Session.SetValue(CATEGORY_EDIT_KEY, 0)
return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Categories"))
}
func
(handler
CategoriesHandler)
GetSelect(current
int)
actionresults.ActionResult {
return actionresults.NewTemplateAction("select_category.html",
struct {
Current int
Categories []models.Category
}{ Current: current, Categories: handler.GetCategories()})
}
Листинг 37-46 Замена содержимого файла category_handler.go в папке admin

Чтобы определить шаблон, используемый этим обработчиком, добавьте
файл с именем admin_categories.html в папку sportsstore/templates с
содержимым, показанным в листинге 37-47.
{{ $context := . }}


IDName

{{ range $context.Categories }}
{{ if ne $context.EditId .ID}}

{{ .ID }}
{{ .CategoryName }}




Edit




{{ else }}









Save




{{end }}
{{ end }}

{{ if eq $context.EditId 0}}

Add New
Category



-




Save





{{ end }}

Листинг 37-47 Содержимое файла admin_categories.html в папке templates

Скомпилируйте и запустите проект, используйте браузер для запроса
http://localhost:5000/admin и нажмите кнопку Categories. Вы увидите
список категорий, который был прочитан из базы данных, и сможете
редактировать и создавать категории, как показано на рисунке 37-5.

Рисунок 37-5 Управление категориями

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

38. SportsStore: завершение и развертывание
В этой главе я завершаю разработку приложения SportsStore и готовлю его к
развертыванию.
Подсказка
Вы можете загрузить пример проекта для этой главы — и для всех
остальных глав этой книги — с https://github.com/apress/pro-go. См.
Главу 2 о том, как получить помощь, если у вас возникнут проблемы с
запуском примеров.

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

Расширение репозитория
Для реализации функций администрирования требуются два новых метода
репозитория, как показано в листинге 38-1.
package models
type Repository interface {
GetProduct(id int) Product
GetProducts() []Product
SaveProduct(*Product)
GetProductPage(page,
totalAvailable int)

pageSize

int)

(products

[]Product,

GetProductPageCategory(categoryId int, page, pageSize int)
(products []Product,
totalAvailable int)
GetCategories() []Category
SaveCategory(*Category)
GetOrder(id int) []Order

GetOrders() Order
SaveOrder(*Order)
SetOrderShipped(*Order)
Seed()
Init()
}
Листинг 38-1 Добавление интерфейса в файл репозитория.go в папке models

Метод SetOrderShipped будет использоваться для обновления
существующего Order, чтобы указать, когда он был отправлен. Метод Init
соответствует имени метода, уже определенному реализацией интерфейса SQL,
и будет использоваться, чтобы позволить администратору подготовить базу
данных к первому использованию после ее развертывания.
Чтобы определить SQL, который будет использоваться для обновления
существующих заказов, добавьте файл с именем update_order.sql в папку
sportsstore/sql с содержимым, показанным в листинге 38-2.
UPDATE Orders SET Shipped = ? WHERE Id == ?
Листинг 38-2 Содержимое файла update_order.sql в папке sql

В листинге 38-3 добавлена новая команда, чтобы к SQL, определенному в
листинге 38-2, можно было обращаться так же, как и к другим операторам SQL.
...
type SqlCommands struct {
Init,
Seed,
GetProduct,
GetProducts,
GetCategories,
GetPage,
GetPageCount,
GetCategoryPage,
GetCategoryPageCount,
GetOrder,
GetOrderLines,
GetOrders,
GetOrdersLines,
SaveOrder,
SaveOrderLine,
UpdateOrder,
SaveProduct,
UpdateProduct,
SaveCategory,
UpdateCategory *sql.Stmt

}
...
Листинг 38-3 Добавление новой команды в файл sql_repo.go в папке models/repo

В листинге 38-4 добавлен параметр конфигурации,
расположение SQL, необходимого для новой команды.

указывающий

...
"sql": {
"connection_str": "store.db",
"always_reset": true,
"commands": {
"Init": "sql/init_db.sql",
"Seed": "sql/seed_db.sql",
"GetProduct": "sql/get_product.sql",
"GetProducts": "sql/get_products.sql",
"GetCategories": "sql/get_categories.sql",
"GetPage": "sql/get_product_page.sql",
"GetPageCount": "sql/get_page_count.sql",
"GetCategoryPage": "sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql",
"GetOrder": "sql/get_order.sql",
"GetOrderLines": "sql/get_order_lines.sql",
"GetOrders": "sql/get_orders.sql",
"GetOrdersLines": "sql/get_orders_lines.sql",
"SaveOrder": "sql/save_order.sql",
"SaveOrderLine": "sql/save_order_line.sql",
"SaveProduct":
"sql/save_product.sql",
"UpdateProduct":
"sql/update_product.sql",
"SaveCategory":
"sql/save_category.sql",
"UpdateCategory":
"sql/update_category.sql",
"UpdateOrder":
"sql/update_order.sql"
}
}
...
Листинг 38-4 Добавление параметра конфигурации в файл config.json в папке sportsstore

Чтобы реализовать метод репозитория, добавьте файл с именем
sql_order_update.go в папку sportsstore/models/repo с содержимым,
показанным в листинге 38-5.
package repo
import "sportsstore/models"

func (repo *SqlRepository) SetOrderShipped(o *models.Order) {
result,
err
:=
repo.Commands.UpdateOrder.ExecContext(repo.Context,
o.Shipped, o.ID)
if err == nil {
rows, err :=result.RowsAffected()
if err != nil {
repo.Logger.Panicf("Cannot get updated ID: %v",
err.Error())
} else if rows != 1 {
repo.Logger.Panicf("Got unexpected rows affected: %v",
rows)
}
} else {
repo.Logger.Panicf("Cannot exec UpdateOrder command: %v",
err.Error())
}
}
Листинг 38-5 Содержимое файла sql_order_update.go в папке models/repo

Реализация обработчиков запросов
Чтобы добавить поддержку управления заказами, замените содержимое файла
orders_handler.go в папке sportsstore/admin содержимым, показанным в
листинге 38-6.
package admin
import (
"platform/http/actionresults"
"platform/http/handling"
"sportsstore/models"
)
type OrdersHandler struct {
models.Repository
handling.URLGenerator
}
func (handler OrdersHandler) GetData() actionresults.ActionResult {
return actionresults.NewTemplateAction("admin_orders.html",
struct {
Orders []models.Order
CallbackUrl string
}{
Orders: handler.Repository.GetOrders(),
CallbackUrl: mustGenerateUrl(handler.URLGenerator,

}

})

OrdersHandler.PostOrderToggle),

func (handler OrdersHandler) PostOrderToggle(ref
actionresults.ActionResult {
order := handler.Repository.GetOrder(ref.ID)
order.Shipped = !order.Shipped
handler.Repository.SetOrderShipped(&order)

EditReference)

return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Orders"))
}
Листинг 38-6 Новое содержимое файла orders_handler.go в папке admin

Единственное изменение, которое будет разрешено для заказов, — это
изменить значение поля Shipped, указывающее, что заказ был отправлен.
Замените содержимое файла database_handler.go содержимым, показанным в
листинге 38-7.
package admin
import (
"platform/http/actionresults"
"platform/http/handling"
"sportsstore/models"
)
type DatabaseHandler struct {
models.Repository
handling.URLGenerator
}
func (handler DatabaseHandler) GetData() actionresults.ActionResult {
return actionresults.NewTemplateAction("admin_database.html",
struct {
InitUrl, SeedUrl string
}{
InitUrl: mustGenerateUrl(handler.URLGenerator,
DatabaseHandler.PostDatabaseInit),
SeedUrl: mustGenerateUrl(handler.URLGenerator,
DatabaseHandler.PostDatabaseSeed),
})
}
func
(handler
DatabaseHandler)
actionresults.ActionResult {

PostDatabaseInit()

handler.Repository.Init()

return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Database"))
}
func
(handler
DatabaseHandler)
actionresults.ActionResult {
handler.Repository.Seed()

PostDatabaseSeed()

return
actionresults.NewRedirectAction(mustGenerateUrl(handler.URLGenerator,
AdminHandler.GetSection, "Database"))
}
Листинг 38-7 Новое содержимое файла database_handler.go в папке admin

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

Создание шаблонов
Чтобы создать шаблон, используемый для управления заказами, добавьте файл
с именем admin_orders.html в папку sportsstore/templates с содержимым,
показанным в листинге 38-8.
{{ $context := .}}

IDNameAddress

{{ range $context.Orders }}

{{ .ID }}
{{ .Name }}
{{ .StreetAddr }}, {{ .City }}, {{ .State }},
{{ .Country }}, {{ .Zip }}



{{ if .Shipped }}

Ship Order


type="submit">



{{ else }}
0 {
return actionresults.NewJsonAction(h.processData(p))
} else {
return &StatusCodeResult{ http.StatusBadRequest }
}
}
func (h RestHandler) processData(p ProductReference) models.Product
{
product := p.Product
product.Category = &models.Category {
ID: p.CategoryID,
}
h.Repository.SaveProduct(&product)
return h.Repository.GetProduct(product.ID)
}
Листинг 38-18 Содержимое файла rest_handler.go в папке store

Структура StatusCodeResult — это результат действия, который
отправляет код состояния HTTP, что полезно для веб-служб. Обработчик
запросов определяет методы, которые позволяют извлекать один продукт и все
продукты с помощью запросов GET, создавать новые продукты с помощью
запросов POST и изменять существующие продукты с помощью запросов PUT.
В листинге 38-19 регистрируется новый обработчик с префиксом /api.
...
handling.NewRouter(
handling.HandlerEntry{ "", store.ProductHandler{}},
handling.HandlerEntry{ "", store.CategoryHandler{}},
handling.HandlerEntry{ "", store.CartHandler{}},
handling.HandlerEntry{ "", store.OrderHandler{}},
handling.HandlerEntry{ "", admin.AuthenticationHandler{}},
handling.HandlerEntry{ "api", store.RestHandler{}},
).AddMethodAlias("/", store.ProductHandler.GetProducts, 0, 1).
AddMethodAlias("/products[/]?[A-z0-9]*?",
store.ProductHandler.GetProducts, 0, 1),
...
Листинг 38-19 Регистрация обработчика в файле main.go в папке sportsstore

Скомпилируйте и запустите проект. Откройте новую командную строку и
выполните команду, показанную в листинге 38-20, чтобы добавить новый
продукт в базу данных.
curl --header "Content-Type: application/json" --request POST --data
'{"name" : "Jet Engine","description": "Paddling is hard work",
"price":650, "categoryid":1}' http://localhost:5000/api/product
Листинг 38-20 Добавление нового продукта

Если вы используете Windows, откройте новое окно PowerShell и
выполните команду, показанную в листинге 38-21.
Invoke-RestMethod http://localhost:5000/api/product -Method POST Body (@{ Name="Jet Engine"; Description="Paddling is hard work";
Price=650;
CategoryId=1
}
|
ConvertTo-Json)
-ContentType
"application/json"
Листинг 38-21 Добавление нового продукта в Windows

Чтобы увидеть эффект изменения, выполните команду, показанную в
листинге 38-22.
curl http://localhost:5000/api/product/10
Листинг 38-22 Запрос данных

Если вы используете Windows, выполните команду, показанную в листинге
38-23, в окне PowerShell.
Invoke-RestMethod http://localhost:5000/api/product/10
Листинг 38-23 Запрос данных в Windows

Вы также можете использовать браузер, чтобы увидеть эффект изменения.
Запрос http://localhost:5000/admin. Войдите в систему как пользователь
alice с паролем mysecret и нажмите кнопку Products. Последняя строка
таблицы будет содержать продукт, созданный с помощью веб-сервиса, как
показано на рисунке 38-3.

Рисунок 38-3 Проверка эффекта изменения базы данных

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

Установка сертификатов
Первый шаг — добавить сертификаты, которые будут использоваться для
HTTPS. Как объяснялось в главе 24, вы можете создать самозаверяющий
сертификат, если у вас нет реального доступного сертификата, или вы можете
использовать файлы сертификатов из репозитория GitHub для этой книги
(которые содержат самоподписанный сертификат, который я создал).

Настройка приложения
Наиболее важным изменением является изменение конфигурации приложения
для отключения функций, которые удобны при разработке, но которые не
следует использовать при развертывании, а также для включения HTTPS, как
показано в листинге 38-24.

{

"logging" : {
"level": "information"
},
"files": {
"path": "files"
},
"templates": {
"path": "templates/*.html",
"reload": false
},
"sessions": {
"key": "MY_SESSION_KEY",
"cyclekey": false
},
"sql": {
"connection_str": "store.db",
"always_reset": false,
"commands": {
"Init": "sql/init_db.sql",
"Seed": "sql/seed_db.sql",
"GetProduct": "sql/get_product.sql",
"GetProducts": "sql/get_products.sql",
"GetCategories": "sql/get_categories.sql",
"GetPage": "sql/get_product_page.sql",
"GetPageCount": "sql/get_page_count.sql",
"GetCategoryPage": "sql/get_category_product_page.sql",
"GetCategoryPageCount":
"sql/get_category_product_page_count.sql",
"GetOrder": "sql/get_order.sql",
"GetOrderLines": "sql/get_order_lines.sql",
"GetOrders": "sql/get_orders.sql",
"GetOrdersLines": "sql/get_orders_lines.sql",
"SaveOrder": "sql/save_order.sql",
"SaveOrderLine": "sql/save_order_line.sql",
"SaveProduct":
"sql/save_product.sql",
"UpdateProduct":
"sql/update_product.sql",
"SaveCategory":
"sql/save_category.sql",
"UpdateCategory":
"sql/update_category.sql",
"UpdateOrder":
"sql/update_order.sql"
}
},
"authorization": {
"failUrl": "/signin"
},
"http": {
"enableHttp": false,
"enableHttps": true,

"httpsPort": 5500,
"httpsCert": "certificate.cer",
"httpsKey": "certificate.key"
}
}
Листинг 38-24 Изменение настроек в файле config.json в папке sportsstore

Убедитесь, что значения, указанные вами для свойств httpsCert и
httpsKey, соответствуют именам ваших файлов сертификатов и что файлы
сертификатов находятся в папке sportsstore.

Сборка приложения
Контейнеры Docker работают под управлением Linux. Если вы используете
Windows, вы должны выбрать Linux в качестве цели сборки, выполнив
команды, показанные в листинге 38-25, в окне PowerShell, чтобы настроить
инструменты сборки Go. Это не требуется, если вы используете Linux.
$Env:GOOS = "linux"; $Env:GOARCH = "amd64"
Листинг 38-25 Установка Linux в качестве цели сборки

Запустите команду, показанную в листинге 38-26, в папке sportsstore,
чтобы собрать приложение.
go build
Листинг 38-26 Сборка приложения

Примечание
Если вы пользователь Windows, вы можете вернуться к обычной сборке
Windows с помощью следующей команды: $Env:GOOS = "windows";
$Env:GOARCH = "amd64". Но не запускайте эту команду, пока не завершите
процесс развертывания.

Установка рабочего стола Docker
Перейдите на docker.com, загрузите и установите пакет Docker Desktop.
Следуйте процессу установки, перезагрузите компьютер и выполните команду,
показанную в листинге 38-27, чтобы убедиться, что Docker установлен и
находится на вашем пути. (Похоже, что процесс установки Docker часто
меняется, поэтому я не буду подробно рассказывать об этом процессе.)
Примечание

Вам нужно будет создать учетную запись на docker.com, чтобы загрузить
установщик.
docker --version
Листинг 38-27 Проверка установки Docker Desktop

Creating the Docker Configuration Files
Чтобы создать конфигурацию Docker для приложения, создайте файл с именем
Dockerfile в папке sportsstore с содержимым, показанным в листинге 38-28.
FROM alpine:latest
COPY
COPY
COPY
COPY
COPY
COPY

sportsstore /app/
templates /app/templates
sql/* /app/sql/
files/* /app/files/
config.json /app/
certificate.* /app/

EXPOSE 5500
WORKDIR /app
ENTRYPOINT ["./sportsstore"]
Листинг 38-28 Содержимое файла Dockerfile в папке sportsstore

Эти инструкции копируют приложение и его вспомогательные файлы в
образ Docker и настраивают его выполнение. Следующим шагом является
создание образа с помощью инструкций, определенных в листинге 38-28.
Запустите команду, показанную в листинге 38-29, в папке sportsstore, чтобы
создать образ Docker.
docker build --tag go_sportsstore .
Листинг 38-29 Создание образа

Убедитесь, что вы остановили все другие экземпляры приложения, и
выполните команду, показанную в листинге 38-30, чтобы создать новый
контейнер из образа и выполнить его.
docker run -p 5500:5500 go_sportsstore
Листинг 38-30 Создание и запуск контейнера

Дайте контейнеру некоторое время для запуска, а затем используйте браузер
для запроса https://localhost:5500, что даст ответ, показанный на рисунке

38-4. Если вы использовали самозаверяющий сертификат, возможно, вам
придется пройти через предупреждение системы безопасности.

Рисунок 38-4 Запуск приложения в контейнере

Теперь приложение готово к развертыванию. Чтобы остановить контейнер
— и любой другой работающий контейнер — выполните команду, показанную
в листинге 38-31.
docker kill $(docker ps -q)
Листинг 38-31 Остановка контейнеров

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