автордың кітабын онлайн тегін оқу Golang для профи: Создаем профессиональные утилиты, параллельные серверы и сервисы
Переводчики С. Черников, Р. Чикин
Михалис Цукалос
Golang для профи: Создаем профессиональные утилиты, параллельные серверы и сервисы, 3-е изд.. — СПб.: Питер, 2023.
ISBN 978-5-4461-1999-8
© ООО Издательство "Питер", 2023
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Об авторе
Михалис Цукалос — системный инженер UNIX, который увлекается написанием технических текстов. Автор книг Go Systems Programming и Mastering Go1, как первого, так и второго изданий. Получил степень бакалавра математики в Университете Патр и степень магистра информационных технологий в Университетском колледже Лондона. Написал более 300 технических статей для различных журналов, включая Sys Admin, MacTech, Linux User and Developer, Usenix ;login:, Linux Format и Linux Journal. В круг научных интересов Михалиса входят временные ряды, базы данных и индексирование.
Вы можете связаться с автором по адресу https://www.mtsoukalos.eu/ и @mactsouk.
Поскольку написание книги — командная работа, я хотел бы поблагодарить за помощь сотрудников издательства Packt Publishing. Спасибо Амиту Рамадасу за ответы на все мои вопросы, Шайлешу Джайну — за то, что убедил начать работу над третьим изданием Mastering Go, Эдварду Докси — за полезные комментарии и предложения и Дереку Паркеру — за его хорошую работу.
Наконец, я хотел бы поблагодарить вас, читателя, за выбор этой книги. Надеюсь, она окажется вам полезной.
1 Цукалос М. Golang для профи. Работа с сетью, многопоточность, структуры данных и машинное обучение с Go. — СПб.: Питер, 2020.
Цукалос М. Golang для профи. Работа с сетью, многопоточность, структуры данных и машинное обучение с Go. — СПб.: Питер, 2020.
Михалис Цукалос — системный инженер UNIX, который увлекается написанием технических текстов. Автор книг Go Systems Programming и Mastering Go1, как первого, так и второго изданий. Получил степень бакалавра математики в Университете Патр и степень магистра информационных технологий в Университетском колледже Лондона. Написал более 300 технических статей для различных журналов, включая Sys Admin, MacTech, Linux User and Developer, Usenix ;login:, Linux Format и Linux Journal. В круг научных интересов Михалиса входят временные ряды, базы данных и индексирование.
О научном редакторе
Дерек Паркер — инженер-программист в Red Hat. Создатель отладчика Delve для Go и автор компилятора Go, компоновщика и стандартной библиотеки. Является автором проектов с открытым исходным кодом и специалистом по сопровождению ПО‚ работал над множеством проектов — от интерфейсного JavaScript до низкоуровневого ассемблерного кода.
Я хотел бы поблагодарить мою жену Эрику и двух наших замечательных детей за то, что они дали мне возможность работать над этим проектом.
Предисловие
Книга, которую вы сейчас читаете, называется «Golang для профи: Создаем профессиональные утилиты, параллельные серверы и сервисы» (3-е издание), и она вся о том, как стать лучшим разработчиком на Go! Если у вас есть второе издание, то не выбрасывайте его — Go не так уж сильно изменился, и второе издание по-прежнему полезно. Однако третье издание во многих аспектах лучше!
Добавлено много интересных новых тем, включая написание сервисов RESTful, работу с протоколом WebSocket и использование GitHub Actions и GitLab Actions для проектов Go, а также совершенно новая глава о дженериках и разработке множества полезных утилит. Кроме того, я постарался сделать это издание меньше, чем второе, и улучшить структуру книги, чтобы облегчить вам ее чтение и ускорить его.
Я также постарался включить нужное количество теории и практики — но только вы, читатель, можете сказать, насколько мне это удалось! Попробуйте выполнить упражнения в конце каждой главы и не стесняйтесь обращаться ко мне с идеями, которые могут еще больше улучшить будущие издания этой книги!
Для кого эта книга
Книга предназначена для программистов на Go среднего уровня, которые хотят улучшить свои навыки и перевести их на новый уровень. Она также будет полезна опытным разработчикам на других языках программирования, которые хотят изучить Go, не углубляясь в основы программирования.
Структура издания
Глава 1 начинается с рассказа об истории возникновения, важных свойствах и преимуществах Go. После этого описываются утилиты godoc и go doc и объясняется, как компилировать и исполнять программы на Go. Далее в главе рассказывается о выводе данных и получении пользовательского ввода, работе с аргументами командной строки и использовании файлов журнала. Наконец, мы разработаем базовую версию приложения для телефонной книги, которую будем совершенствовать в следующих главах.
В главе 2 рассматриваются основные типы данных Go, как числовые, так и нечисловые, а также массивы и срезы, позволяющие группировать данные одного типа. В ней также рассматриваются указатели Go, константы и работа с датами и временем. Последняя часть главы посвящена генерации случайных чисел и заполнению приложения телефонной книги случайными данными.
Глава 3 начинается с карт, после чего переходит к структурам и ключевому слову struct. Кроме того, в ней вы найдете информацию о регулярных выражениях, сопоставлении с образцами и работе с CSV-файлами. Наконец, мы улучшим приложение телефонной книги, добавив в него постоянное хранение данных.
Глава 4 посвящена рефлексии, интерфейсам и методам типов — функциям, прикрепленным к типам данных. В ней также описывается использование интерфейса sort.Interface для сортировки фрагментов, использование пустого интерфейса, а также представлены утверждения типа, переключатели типа и тип данных error. Дополнительно‚ прежде чем улучшать приложение телефонной книги‚ мы обсудим, как Go может имитировать некоторые объектно-ориентированные концепции.
Глава 5 посвящена пакетам, модулям и функциям, которые являются основными элементами пакетов. Среди прочего мы создадим пакет Go, позволяющий взаимодействовать с базой данных PostgreSQL, документацию для него‚ а также объясним не всегда простое использование ключевого слова defer. В этой главе также содержится информация о том‚ как использовать GitLab Runners и GitHub Actions для автоматизации, и о том, как создать образ Docker для двоичного файла Go.
Глава 6 посвящена системному программированию и содержит такие темы, как работа с аргументами командной строки, обработка сигналов UNIX, ввод и вывод файлов, интерфейсы io.Reader и io.Writer и использование пакетов viper и cobra. Кроме того, мы поговорим о работе с файлами JSON, XML и YAML, создадим удобную утилиту командной строки, позволяющую обнаруживать циклы в файловой системе UNIX, и обсудим встраивание файлов в двоичные файлы Go, а также функцию os.ReadDir(), тип os.DirEntry и пакет io/fs. Наконец, мы обновим приложение телефонной книги, чтобы можно было использовать данные JSON, и преобразуем его в соответствующую утилиту командной строки с помощью пакета cobra.
В главе 7 обсуждаются горутины, каналы и конвейеры. Мы рассмотрим различия между процессами, потоками и горутинами, пакет sync и то, как работает планировщик Go. Кроме того, мы исследуем использование ключевого слова select и обсудим различные «типы» каналов Go, а также общую память, мьютексы, типы sync.Mutex и sync.RWMutex. В остальной части главы мы рассмотрим пакеты context и semaphore, рабочие пулы и выясним, как устанавливать тайм-аут для горутин и выявлять состояние гонки.
В главе 8 обсуждаются пакет net/http, разработка веб-серверов и веб-сервисов, предоставление метрик в Prometheus, визуализация метрик в Grafana, создание веб-клиентов и файловых серверов. Мы также преобразуем приложение телефонной книги в веб-сервис и создадим для него клиент командной строки.
Глава 9 посвящена пакету net, TCP/IP и протоколам TCP и UDP, а также сокетам UNIX и протоколу WebSocket. В этой главе мы разработаем множество сетевых серверов и клиентов.
В главе 10 описана работа с REST API и сервисами RESTful. Мы выясним, как определять REST API и разрабатывать мощные параллельные серверы RESTful, а также утилиты командной строки, которые действуют как клиенты сервисов RESTful. Наконец, мы познакомимся со Swagger, позволяющим создавать документацию для REST API, и выясним, как загружать двоичные файлы.
В главе 11 обсуждаются тестирование, оптимизация и профилирование кода, а также кросс-компиляция, сравнительный анализ кода Go, создание примеров функций, использование директивы go:generate и поиск недоступного кода Go.
Глава 12 посвящена работе с gRPC в Go. gRPC — это альтернатива сервисам RESTful, разработанная Google. В этой главе вы узнаете, как определить методы и сообщения сервиса gRPC, как перевести их в код Go и как разработать сервер и клиент для этого сервиса gRPC.
Глава 13 посвящена дженерикам и тому, как использовать новый синтаксис для написания универсальных функций и определения универсальных типов данных. Дженерики появились в версии Go 1.18, которая, согласно циклу разработки Go, была официально выпущена в марте 2022 года.
В приложении рассказывается о работе сборщика мусора Go и показано, как этот компонент Go может повлиять на производительность вашего кода.
Как сделать книгу максимально полезной
Для работы с книгой требуется компьютер UNIX с относительно недавно установленной версией Go, то есть это может быть любая машина, работающая под управлением macOS X, macOS или Linux. Бо́льшая часть представленного кода также будет выполняться на компьютерах с Microsoft Windows без каких-либо изменений.
Чтобы извлечь из книги максимальную пользу, попробуйте как можно скорее применить знания из каждой главы в собственных программах и посмотреть, что работает, а что нет! Как я уже говорил, попытайтесь выполнить упражнения, приведенные в конце каждой главы, или реализуйте свои программные задачи.
Файлы с примерами кода
Пакет кода для книги размещен на GitHub по адресу https://github.com/mactsouk/mastering-Go-3rd. У нас есть и другие пакеты кода из нашего богатого каталога книг и видео, доступные по адресу https://github.com/PacktPublishing/. Не забудьте в них заглянуть!
Цветные иллюстрации
Мы также предоставляем PDF-файл с цветными оригинальными снимками экрана и схемами из этой книги. Вы можете скачать его здесь: https://static.packt-cdn.com/downloads/9781801079310_ColorImages.pdf.
Условные обозначения
В книге используются следующие условные обозначения.
Кодовые слова в тексте, имена папок, имена файлов и их расширения, пути и пользовательский ввод оформляются моноширинным шрифтом. Например: «Звезда этой главы — пакет net/http, который содержит функции, позволяющие разрабатывать мощные веб-серверы и веб-клиенты».
Блок кода оформляется следующим образом:
package main
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"
)
Когда мы хотим привлечь ваше внимание к определенной части блока кода, соответствующие строки или элементы выделяются моноширинным жирным шрифтом:
package main
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"
)
Любой ввод или вывод из командной строки оформляется следующим образом:
$ go run www.go
Using default port number: :8001
Served: localhost:8001
Шрифтом без засечек оформляются URL или слова, которые вы видите на экране, например в меню или диалоговых окнах.
Новые термины и важные слова выделены курсивом.
Этот рисунок указывает на предупреждения или важные примечания.
Этот рисунок указывает на советы и рекомендации.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
1. Краткое введение в Go
Представьте, что вы разработчик‚ которому нужно создать утилиту командной строки. Или что у вас есть REST API и вы хотите создать сервер RESTful, который реализует этот REST API. Первый вопрос, который придет вам в голову, скорее всего, будет в том, какой язык программирования выбрать.
Рекомендуется использовать тот язык, который вы знаете лучше всего. Однако эта книга предназначена для того, чтобы дать вам возможность использовать Go для решения всех этих и многих других задач и проектов. Главу мы начнем с объяснения того, что такое Go, далее расскажем его историю и покажем, как запускать код Go. Мы объясним некоторые основные характеристики Go, например‚ как определять переменные, управлять потоком ваших программ и получать пользовательский ввод, после чего применим некоторые из этих концепций, создав приложение телефонной книги для командной строки.
В этой главе:
• введение в Go;
• Hello World;
• запуск Go-кода;
• важные особенности Go;
• разработка утилиты which(1) в Go;
• вывод информации в лог;
• обзор Go-дженериков;
• разработка базового приложения телефонной книги.
Введение в Go
Go — это язык системного программирования с открытым исходным кодом, первоначально разработанный как внутренний проект Google и ставший общедоступным еще в 2009 году. Духовными отцами Go являются Роберт Гриземер, Кен Томсон и Роб Пайк.
Официальное название языка — Go, однако его иногда называют Golang. Официальная причина в том, что домен go.org был недоступен для регистрации и вместо него был выбран golang.org. Практическая же причина в том, что, когда вы запрашиваете в поисковой системе информацию, связанную с Go, слово Go обычно интерпретируется как глагол. Кроме того, официальный хештег Go в Twitter — #golang.
Go является языком программирования общего назначения, но в основном используется для написания системных инструментов, утилит командной строки, веб-сервисов и программного обеспечения, которое работает в сетях. С помощью Go также можно обучаться программированию‚ плюс он является хорошим кандидатом на первый язык программирования благодаря своей немногословности‚ четким идеям и принципам. Go может помочь в разработке следующих видов приложений:
• профессиональные веб-сервисы;
• сетевые инструменты и серверы, такие как Kubernetes и Istio;
• серверные системы;
• системные утилиты;
• эффективные утилиты командной строки, такие как docker и hugo;
• приложения, которые обмениваются данными в формате JSON;
• приложения, обрабатывающие данные из реляционных баз данных, баз данных NoSQL или других популярных систем хранения;
• компиляторы и интерпретаторы для разрабатываемых вами языков программирования;
• системы баз данных, такие как CockroachDB, и хранилища ключей/значений, такие как etcd.
Есть много вещей, которые Go делает лучше, чем другие языки программирования, например:
• компилятор Go по умолчанию может обнаруживать большой набор глупых ошибок, которые могут привести к ошибкам в программном коде;
• Go использует меньше круглых скобок, чем C, C++ или Java, и не использует точки с запятой, что делает внешний вид исходного кода Go более удобочитаемым и менее подверженным ошибкам;
• Go поставляется с богатой и надежной стандартной библиотекой;
• Go поддерживает параллелизм «из коробки» через горутины и каналы;
• горутины действительно легковесные. Вы можете с легкостью запустить тысячи горутин на любой современной машине, обойдясь без каких-либо проблем с производительностью;
• в отличие от C, Go поддерживает функциональное программирование;
• Go-код имеет обратную совместимость, то есть более новые версии компилятора Go принимают программы, созданные с использованием предыдущей версии языка, без каких-либо изменений. Эта гарантия совместимости распространяется только на основные версии Go. Например, нет никакой гарантии, что программа на Go 1.x будет скомпилирована в Go 2.x.
Теперь, когда вы знаете, что может Go и чем он хорош, вспомним его историю.
История Go
Как упоминалось ранее, Go начинался как внутренний проект Google, который стал общедоступным еще в 2009 году. Гриземер, Томсон и Пайк разработали Go как язык для профессиональных программистов, желающих создавать надежное, стабильное и эффективное программное обеспечение, которым легко управлять. Они разрабатывали Go с прицелом на его простоту, даже если она означала, что Go не суждено стать языком программирования для всех.
На рис. 1.1 показано, какие языки программирования прямо или косвенно повлияли на Go. К примеру‚ синтаксис Go выглядит как синтаксис C, в то время как концепция пакетов была создана под влиянием Modula-2.
Результатами стали язык программирования, инструменты и стандартная библиотека. Помимо синтаксиса и инструментов Go, вы получаете довольно богатую стандартную библиотеку и систему типов, которая призвана избавить вас от простых ошибок, таких как неявные преобразования типов, неиспользуемые переменные и пакеты. Go-компилятор отлавливает большинство этих простых ошибок и останавливает компиляцию, пока вы как-то их не исправите. Кроме того, Go-компилятор с трудом обнаруживает сложные ошибки, такие как состояние гонки.
Если вы устанавливаете Go впервые, то можете начать со страницы https://golang.org/dl/. Однако есть большая вероятность, что в вашем варианте UNIX уже имеется готовый к установке пакет для языка программирования Go, так что‚ возможно‚ у вас получится запустить Go с помощью вашего любимого менеджера пакетов.
Рис. 1.1. Языки программирования, повлиявшие на Go
Почему UNIX, а не Windows
Вы можете задаться вопросом, почему мы все время говорим о UNIX и даже не обсуждаем Microsoft Windows. Для этого есть две основные причины. Первая состоит в том, что большинство Go-программ будут работать на компьютерах с Windows без каких-либо изменений, поскольку Go переносим по сути‚ и это означает, что вам не следует беспокоиться об используемой операционной системе.
Однако вам может потребоваться внести небольшие изменения в код некоторых системных утилит, чтобы они заработали в Windows. Кроме того, по-прежнему будут существовать библиотеки, работающие только на компьютерах с Windows или только на компьютерах, отличных от Windows. Вторая причина заключается в том, что многие сервисы, написанные на Go, выполняются в среде Docker — образы Docker используют операционную систему Linux, а это означает, что вы должны программировать свои утилиты с учетом операционной системы Linux.
Что же касается пользовательского опыта, то UNIX и Linux очень похожи. Основное отличие состоит в том, что Linux — программное обеспечение с открытым исходным кодом, тогда как UNIX — проприетарное ПО.
Преимущества Go
Go обладает рядом важных преимуществ для разработчиков, начиная с того факта, что он был разработан и поддерживается настоящими программистами. Помимо этого, Go прост в освоении, особенно если вы уже знакомы с такими языками программирования, как C, Python или Java. Вдобавок ко всему код на Go выглядит красиво (по крайней мере, на мой взгляд), и это замечательно, особенно когда вы зарабатываете на жизнь программированием и вам приходится работать с кодом ежедневно. Go-код также легко читается и имеет поддержку Unicode «из коробки», а это означает, что вы можете легко вносить изменения в существующий код Go. Наконец, Go резервирует лишь 25 ключевых слов, что делает его очень простым для запоминания. Возможно ли такое в случае C++?
Go также поставляется с поддержкой простой модели параллелизма, которая реализована с использованием горутин и каналов. Go не только управляет потоками операционной системы за вас‚ но и имеет эффективную среду выполнения, позволяющую создавать облегченные рабочие модули (горутины), которые взаимодействуют друг с другом с помощью каналов. Хотя Go поставляется с богатой стандартной библиотекой, существуют действительно удобные пакеты Go, такие как cobra и viper. Они позволяют разрабатывать на Go сложные утилиты командной строки, такие как docker и hugo. Это в значительной степени подтверждается тем фактом, что в исполняемых двоичных файлах Go используется статическая компоновка. Иными словами, после создания они уже не зависят от каких-либо совместно используемых библиотек и содержат всю необходимую информацию.
Благодаря своей простоте Go-код предсказуем и не имеет странных побочных эффектов. Хотя Go и поддерживает указатели, он не поддерживает подобную C арифметику указателей, если‚ конечно‚ вы не используете пакет unsafe, который зачастую служит причиной множества ошибок и дыр в безопасности. Язык Go не является объектно-ориентированным, тем не менее Go-интерфейсы очень универсальны и позволяют имитировать некоторые возможности объектно-ориентированных языков, такие как полиморфизм, инкапсуляция и композиция.
Кроме того, последние версии Go предлагают поддержку универсальных паттернов (или дженериков), благодаря чему упрощается код при работе с несколькими типами данных. И последнее, но не менее важное: Go поставляется с поддержкой сборки мусора, а это означает, что ручное управление памятью не требуется.
Go — очень практичный и надежный язык программирования, однако он далеко не идеален.
• Хотя это скорее личное предпочтение, нежели фактический технический недостаток, но Go не имеет прямой поддержки объектно-ориентированного программирования, которое является распространенной парадигмой программирования.
• Горутины достаточно легковесны, однако не так эффективны, как потоки операционной системы. В зависимости от реализуемого приложения вполне допустимы некоторые редкие случаи, когда горутины не подойдут для решения задачи. Однако в большинстве случаев разработка вашего приложения с помощью горутин и каналов решит поставленные задачи.
• Сборка мусора выполняется достаточно быстро бо́льшую часть времени и почти для всех видов приложений. Тем не менее бывают случаи, когда вам требуется работать над распределением памяти вручную‚ но Go так не может. На практике это означает, что Go не позволит осуществить какое-либо управление памятью вручную.
Однако существует множество сценариев, когда вы можете выбрать Go‚ например:
• создание сложных утилит командной строки со множеством команд, подкоманд и параметров командной строки;
• создание приложений с высокой степенью параллелизма;
• разработка серверов, работающих с API, и клиентов, которые взаимодействуют путем обмена данными во множестве форматов, включая JSON, XML и CSV;
• разработка серверов и клиентов WebSocket;
• разработка серверов и клиентов gRCP;
• разработка надежных системных инструментов для UNIX и Windows;
• изучение программирования.
Далее мы рассмотрим ряд концепций и утилит‚ что послужит прочной основой, после чего создадим упрощенную версию утилиты which(1). В конце главы мы разработаем простое приложение для телефонной книги, которое будет развиваться по мере того, как в последующих главах мы будем продолжать знакомиться с особенностями Go.
Но сначала мы представим команду go doc, которая позволяет вам искать информацию о стандартной библиотеке Go, ее пакетах и их функциях. Затем‚ на примере программы Hello World!‚ мы покажем, как выполнить Go-код.
Утилиты go doc и godoc
Дистрибутив Go поставляется со множеством инструментов, которые облегчают вашу жизнь как программиста. Двумя такими инструментами являются подкоманда go doc и утилита godoc, которые позволяют вам просматривать документацию имеющихся функций и пакетов Go, не подключаясь к Интернету. Однако если вы предпочитаете просматривать документацию Go онлайн, то можете посетить https://pkg.go.dev/. Поскольку godoc не установлена по умолчанию, вам может потребоваться установить ее. Для этого выполните go get golang.org/x/tools/cmd/godoc.
Команда go doc может быть выполнена как обычное приложение командной строки, которое отображает свои выходные данные на терминале, а godoc — как приложение командной строки, которое запускает веб-сервер. В последнем случае вам понадобится браузер, чтобы просматривать документацию Go. Первая утилита аналогична команде UNIX man(1), но для функций и пакетов Go.
Число после названия программы или системного вызова UNIX указывает на раздел руководства, к которому относится данная страница. Большинство имен встречаются на страницах руководства только один раз (это значит, указывать номер раздела не требуется), однако существуют имена, которые можно найти в нескольких разделах, поскольку они имеют несколько значений, например crontab(1) и crontab(5). Так что если вы попытаетесь получить страницу руководства с многозначным названием, не указав номер раздела, то получите запись с наименьшим номером раздела.
Итак, чтобы найти информацию о функции Printf() пакета fmt, вам нужно выполнить следующую команду:
$ go doc fmt.Printf
Аналогичным образом вы можете найти информацию обо всем пакете fmt, выполнив следующую команду:
$ go doc fmt
Вторая утилита требует выполнения godoc с параметром -http:
$ godoc -http=:8001
Числовое значение в этой команде, которое в данном случае равно 8001, является номером порта, который будет прослушиваться HTTP-сервером. Поскольку мы опустили IP-адрес, godoc будет прослушивать все сетевые интерфейсы.
Вы можете выбрать любой доступный номер порта при условии, что у вас есть соответствующие привилегии. Однако обратите внимание, что порты под номерами 0–1023 предназначены для служебного пользования и могут быть задействованы только пользователем root, поэтому лучше выбрать другой порт при условии, что он не используется другим процессом.
Вы можете опустить знак равенства в представленной команде и поставить вместо него пробел. Итак, следующая команда полностью эквивалентна предыдущей:
$ godoc -http :8001
После этого вы должны перейти в своем браузере по адресу http://localhost:8001/, что даст возможность получить список доступных Go-пакетов и просмотреть их документацию. Если вы используете Go впервые, то документация по Go позволит изучить параметры и возвращаемые значения используемых функций. По мере своего путешествия по Go вы будете применять документацию Go для детального изучения функций и переменных, которые хотите использовать.
Hello World!
Ниже приведена версия программы Hello World на Go. Введите ее и сохраните как hw.go:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!")
}
Любой исходный код на Go начинается с объявления пакета package. В нашем случае название пакета — main, что имеет особое значение в Go. Ключевое слово import позволяет включать функционал из существующих пакетов. В нашем случае нам нужна только часть функций пакета fmt, который принадлежит стандартной библиотеке Go. Пакеты, которые не являются ее частью, импортируются с использованием их полного интернет-пути. Следующая важная вещь при создании исполняемого приложения — это функция main(). Go считает ее точкой входа в приложение и начинает выполнение приложения с кода, обнаруженного в функции main() пакета main.
hw.go — это Go-программа, которая работает сама по себе. Две характеристики делают ее автономным исходным файлом, который может генерировать исполняемый двоичный файл: имя пакета, которое должно быть main, и наличие функции main(). Более подробно мы обсудим Go-функции в следующем подразделе, но еще больше о функциях и методах (функциях‚ привязанных к конкретным типам данных) вы узнаете в главе 5.
Введение в функции
Каждое определение Go-функции начинается с ключевого слова func, за которым следуют название, сигнатура и реализация. Как и в случае пакета main, вы можете называть свои функции как угодно — существует глобальное правило Go, которое также применяется к именам функций и переменных и действует для всех пакетов, кроме main: все, что начинается со строчной буквы, считается закрытым и доступно только в текущем пакете. Больше информации о данном правиле мы узнаем в главе 5. Единственным исключением из него являются имена пакетов, которые могут начинаться как со строчных, так и с прописных букв. Хотя я это и сказал, но не помню ни одного Go-пакета, который начинался бы с заглавной буквы!
Теперь вы можете спросить, как функции организуются и предоставляются. Ответ: с помощью пакетов и в следующем подразделе мы немного поговорим об этом.
Введение в пакеты
Go-программы организованы в пакеты‚ и даже самая маленькая Go-программа должна поставляться в виде пакета. Ключевое слово package помогает задать имя нового пакета, которое может быть любым, за одним исключением: если вы создаете исполняемое приложение, а не просто пакет, который будет совместно использоваться другими приложениями или пакетами, то должны назвать свой пакет main. Больше информации о разработке Go-пакетов вы получите в главе 5.
Пакеты могут использоваться другими пакетами. Фактически повторное использование существующих пакетов — хорошая практика, которая избавляет от необходимости писать много кода или реализовывать существующую функциональность с нуля.
Ключевое слово import используется для импорта других Go-пакетов в ваши Go-программы и дает возможность использовать некоторые или все их функциональные возможности. Go-пакет может либо быть частью богатой стандартной библиотеки Go, либо поступать извне. Пакеты стандартной библиотеки Go импортируются по имени (os) без необходимости указывать имя хоста и путь, в то время как внешние пакеты импортируются с использованием их полных интернет-путей, таких как github.com/spf13/cobra.
Запуск Go-кода
Теперь нам нужно понять, как же запустить hw.go или любое другое Go-приложение. В двух следующих подразделах мы выясним, что есть два способа выполнения Go-кода: в виде скомпилированного языка с использованием go build или в виде языка сценариев с использованием go run. Подробно поговорим об этих двух способах запуска Go-кода.
Компиляция Go-кода
Чтобы скомпилировать Go-код и создать двоичный исполняемый файл, вам необходимо использовать команду go build. Она создает исполняемый файл, который вы можете распространять и выполнять вручную. Это означает, что команда go build требует дополнительного шага для запуска вашего кода.
Сгенерированный исполняемый файл автоматически называется по имени файла исходного кода за вычетом расширения .go. Следовательно, из исходного файла hw.go получится исполняемый файл hw. Если это не то, что вам нужно, то go build поддерживает параметр -o, который позволяет изменить имя файла и путь к сгенерированному исполняемому файлу. Например, если вы хотите назвать исполняемый файл HelloWorld, то вам следует выполнить go build -o HelloWorld hw.go. Если исходные файлы не предоставлены, то go build ищет пакет main в текущем каталоге.
После этого вам необходимо самостоятельно запустить сгенерированный исполняемый двоичный файл. В нашем случае это означает выполнение либо hw, либо HelloWorld. Это показано в следующем выводе:
$ go build hw.go
$ ./hw
Hello World!
Теперь, когда мы выяснили, как компилировать Go-код, перейдем к использованию Go в качестве языка скриптов.
Использование Go в качестве языка скриптов
Команда go run создает именованный Go-пакет (который в нашем случае является пакетом main, реализованным в единственном файле), временный исполняемый файл, выполняет его и удаляет после завершения — для нас это похоже на использование языка скриптов. В нашем случае мы можем сделать следующее:
$ go run hw.go
Hello World!
Если вы хотите протестировать свой код, то лучше использовать команду go run. Однако если вы хотите создать и распространить исполняемый двоичный файл, то лучше использовать команду go build.
Важные правила форматирования и кодирования
Вы должны знать, что Go подразумевает строгие правила форматирования и кодирования, которые помогают разработчикам избежать ошибок начинающих. Изучив эти несколько правил и характерных особенностей Go и поняв‚ какое значение они имеют для вашего кода, вы сможете сосредоточиться на фактической функциональности вашего кода. Кроме того, компилятор Go помогает вам следовать этим правилам, выдавая выразительные сообщения об ошибках и предупреждения. Наконец, Go предлагает стандартный инструментарий (gofmt), который умеет форматировать ваш код за вас, так что вам не придется думать об этом.
Ниже приведен список важных правил Go, которые помогут вам при чтении этой главы.
• Go-код поставляется в пакетах, и вы можете свободно использовать функционал существующих пакетов. Однако если вы собираетесь импортировать пакет, то вам следует использовать некоторые из этих функций. Из этого правила есть несколько исключений, но в основном они связаны с инициализацией подключений и сейчас не важны.
• Вы либо используете переменную, либо вообще ее не объявляете. Это правило поможет избежать ошибок, таких как неправильное написание существующей переменной или имени функции.
• В Go есть только один способ форматирования фигурных скобок.
• Блоки кода в Go заключаются в фигурные скобки, даже если содержат лишь один оператор или вообще их не содержат.
• Go-функции могут возвращать несколько значений.
• Вы не можете автоматически конвертировать различные типы данных, даже если они из одного семейства. Например, вы не можете неявно преобразовать целое число в число с плавающей запятой.
В Go гораздо больше правил, но эти — самые важные. Вы увидите их в действии не только в текущей главе, но и в других. Пока мы рассмотрим единственный способ форматирования фигурных скобок в Go, так как это правило применяется повсеместно.
Взгляните на следующую Go-программу curly.go:
package main
import (
"fmt"
)
func main()
{
fmt.Println("Go has strict rules for curly braces!")
}
Код выглядит просто замечательно, но при запуске вас ждет разочарование, так как он не будет компилироваться. В итоге вы получите следующее сообщение о синтаксической ошибке:
$ go run curly.go
# command-line-arguments
./curly.go:7:6: missing function body
./curly.go:8:1: syntax error: unexpected semicolon or newline before {
Официальное объяснение этого сообщения об ошибке заключается в том, что Go требует использования точек с запятой в качестве ограничителей оператора во многих контекстах и компилятор автоматически вставляет требуемые точки с запятой, когда считает, что они необходимы. Следовательно, из-за открывающей фигурной скобки ({), помещенной в отдельную строку, компилятор Go будет вынужден вставить точку с запятой в конце предыдущей строки (func main()), что является основной причиной сообщения об ошибке. Правильный способ написания кода, который был приведен выше, выглядит так:
package main
import (
"fmt"
)
func main() {
fmt.Println("Go has strict rules for curly braces!")
}
Теперь‚ когда мы познакомились с этим глобальным правилом, перейдем к некоторым важным особенностям Go.
Важные особенности Go
В этом большом разделе мы обсудим важные и незаменимые функции Go, включая переменные, управление потоком программы, итерации, получение пользовательского ввода и параллелизм в Go. Мы начнем с обсуждения переменных, их объявления и использования.
Определение и использование переменных
Предположим, нам требуется с помощью Go выполнить некоторые базовые математические вычисления. В этом случае придется определить переменные, чтобы сохранить как входные данные‚ так и результаты.
Чтобы сделать процесс объявления переменных более естественным и удобным‚ Go предоставляет несколько способов объявления новых переменных. Вы можете объявить новую переменную, используя ключевое слово var, за которым следуют имя переменной и желаемый тип данных (мы подробно рассмотрим типы данных в главе 2). При желании вы можете дополнить объявление знаком = и начальным значением для вашей переменной. Если задано начальное значение, то вы можете опустить тип данных, и компилятор подберет его за вас.
Это подводит нас к очень важному правилу Go: если переменной не задано начальное значение, то компилятор Go автоматически инициализирует ее нулевым значением ее типа данных.
Существует также нотация :=, которую можно использовать вместо объявления var. Команда := определяет новую переменную, делая вывод о данных из следующего за ней значения. Официальное название для := звучит так: короткое присваивание, и этот оператор очень часто используется в Go, особенно для получения возвращаемых значений из функций и для циклов for с ключевым словом range.
Оператор короткого присваивания может применяться вместо объявления var с неявным типом. В Go вы будете редко встречать var. Это ключевое слово в основном служит для объявления глобальных или локальных переменных без начального значения. Причина первого заключается в том, что каждый оператор, существующий вне кода функции, должен начинаться с ключевого слова, такого как func или var.
Это означает, что оператор короткого присваивания не получится использовать вне функциональной среды, поскольку там он недоступен. Наконец, вам может понадобиться var, когда вы хотите четко указать тип данных. Например, когда вам нужен int8 или int32 вместо int.
Поэтому, даже если вы и можете объявлять локальные переменные через var или :=, только const (когда значение переменной не будет меняться) и var работают для глобальных переменных, которые являются переменными, определенными вне функции и не заключенными в фигурные скобки. К глобальным переменным можно получить доступ из любого места пакета, не прибегая к необходимости явно передавать их в функцию, и они могут меняться, если только не были определены как константы с использованием ключевого слова const.
Вывод переменных
Программы, как правило, отображают информацию, а это значит, что им часто нужно вывести данные или отправить их куда-нибудь, чтобы другая программа могла их сохранить или обработать. Для вывода данных на экран Go использует функции пакета fmt. Если вы хотите, чтобы Go взял на себя вывод, то можете воспользоваться fmt.Println(). Однако бывают ситуации, когда нужен полный контроль над тем, как будут выводиться данные. В таких случаях можно использовать функцию fmt.Printf().
Она аналогична функции C printf() и требует использования управляющих последовательностей, которые определяют тип данных переменной, которая будет выведена. Кроме того, fmt.Printf() позволяет форматировать сгенерированный вывод, что особенно удобно для значений с плавающей запятой, поскольку позволяет указать цифры, которые будут отображаться в выводе (%.2f отображает две цифры после десятичной точки). Наконец, символ \n используется для вывода символа новой строки и, следовательно, ее создания, так как fmt.Printf() не вставляет автоматически новую строку (это не относится к fmt.Println(), которая вставляет новую строку).
В следующей программе показано, как объявлять‚ использовать и выводить новые переменные. Введите следующий код в обычный текстовый файл variables.go:
package main
import (
"fmt"
"math"
)
var Global int = 1234
var AnotherGlobal = -5678
func main() {
var j int
i := Global + AnotherGlobal
fmt.Println("Initial j value:", j)
j = Global
// math.Abs() требует параметр float64
// соответственно‚ мы приводим тип
k := math.Abs(float64(AnotherGlobal))
fmt.Printf("Global=%d, i=%d, j=%d k=%.2f.\n", Global, i, j, k)
}
Я предпочитаю выделять глобальные переменные, либо начиная их с заглавной буквы, либо используя для названия только заглавные буквы.
В этой программе мы видим:
• глобальную переменную int с именем Global;
• вторую глобальную переменную с именем AnotherGlobal — Go автоматически выводит ее тип данных из ее значения, которое в данном случае является целым числом;
• локальную переменную с именем j и типом int, который, как вы узнаете в следующей главе, является особым типом данных. Переменная j не имеет начального значения; это значит, Go автоматически присваивает ей нулевое значение ее типа данных, что в данном случае равно 0;
• еще одну локальную переменную с именем i — Go выводит ее тип данных из значения. Поскольку это сумма двух значений int, то и тип тоже int;
• поскольку функция math.Abs() требует параметра float64, мы не можем передать туда AnotherGlobal, так как это переменная типа int. Приведение типа float64() преобразует значение AnotherGlobal в float64. Обратите внимание, что AnotherGlobal сохраняет свой тип int;
• наконец, fmt.Printf() форматирует и выводит результат.
При выполнении variables.go мы получаем такой вывод:
Initial j value: 0
Global=1234, i=-4444, j=1234 k=5678.00.
В этом примере показано еще одно важное правило Go, которое уже упоминалось ранее: Go не допускает неявных преобразований данных, подобно C.
В variables.go мы увидели‚ что при использовании функции math.Abs(), которая ожидает значение float64, значение int не получится использовать вместо float64, даже если это конкретное преобразование простое и безошибочное. Компилятор Go отказывается компилировать подобные операторы. Чтобы все работало правильно‚ придется преобразовать значение int в float64 явно, используя float64().
Для преобразований, которые не являются прямыми (например, string в int), существуют специализированные функции, позволяющие обнаруживать проблемы с преобразованием и возвращающие их в виде переменной error.
Управление ходом выполнения программы
Мы разобрались с Go-переменными, но как мы можем менять поток Go-программы на основе значения переменной или какого-либо другого условия? Go поддерживает структуры управления if/else и switch. Обе эти структуры можно найти в большинстве современных языков программирования, так что если вы уже программировали на другом языке, то должны быть знакомы с if и switch. Оператор if не использует круглые скобки для встраивания проверяемых условий, потому что в Go вообще не используются круглые скобки. Кроме того, if ожидаемо поддерживает операторы else и else if.
Продемонстрируем использование if с помощью очень распространенного паттерна‚ который повсеместно применяется в Go. Он гласит, что если значение переменной error, возвращаемой из функции, равно nil, то с выполнением функции все в порядке. В противном случае где-то возникла ошибка, требующая особого внимания. Этот паттерн обычно реализуется следующим образом:
err := anyFunctionCall()
if err != nil {
// сделать что-нибудь, если возникла ошибка
}
err — это переменная, которая содержит значение error, возвращаемое функцией, а != говорит о том, что значение переменной err не равно nil. Подобный код вы встретите в Go-программах множество раз.
Строки, начинающиеся с //, представляют собой однострочные комментарии. Если вы ставите // в середине строки, то все, что после //, считается комментарием. Это правило не применяется, если // находится внутри строкового значения.
Оператор switch имеет две разные формы. В первой он содержит вычисляемое выражение, тогда как во второй не имеет выражения для вычисления. В этом случае выражения вычисляются в каждом операторе case, что повышает гибкость switch. Основное преимущество switch заключается в том, что при правильном использовании он упрощает сложные и трудночитаемые блоки if-else.
Как if, так и switch показаны в следующем коде, который предназначен для обработки пользовательского ввода, заданного в качестве аргумента командной строки. Пожалуйста, введите его и сохраните как control.go. В учебных целях мы представляем код control.go по частям, чтобы его было удобнее объяснять:
package main
import (
"fmt"
"os"
"strconv"
)
Эта первая часть содержит ожидаемую преамбулу с импортом пакетов. Реализация функции main() начинается следующим образом:
func main() {
if len(os.Args) != 2 {
fmt.Println("Please provide a command line argument")
return
}
argument := os.Args[1]
Эта часть программы гарантирует, что у вас есть единственный аргумент командной строки для обработки, доступ к которому осуществляется по имени os.Args[1]. Позже мы расскажем об этом более подробно, но вы можете обратиться к рис. 1.2, чтобы получить дополнительную информацию о срезе os.Args.
// с выражением после switch
switch argument {
case "0":
fmt.Println("Zero!")
case "1":
fmt.Println("One!")
case "2", "3", "4":
fmt.Println("2 or 3 or 4")
fallthrough
default:
fmt.Println("Value:", argument)
}
Здесь мы видим блок switch с четырьмя ветвлениями. Первые три требуют точного совпадения string, а последнее соответствует всему остальному. Порядок операторов case важен, поскольку выполняется только первое совпадение. Ключевое слово fallthrough сообщает Go, что после выполнения этой ветки необходимо перейти на следующую, которая в данном случае является веткой default:
value, err := strconv.Atoi(argument)
if err != nil {
fmt.Println("Cannot convert to int:", argument)
return
}
Поскольку аргументы командной строки инициализируются как строковые значения, нам нужно преобразовать пользовательский ввод в целочисленное значение с помощью отдельного вызова, который в данном случае будет вызовом strconv.Atoi(). Если значение переменной err равно nil, то преобразование прошло успешно и мы можем продолжать. В противном случае на экране выводится сообщение об ошибке, и программа завершает работу.
Следующий код показывает вторую форму switch, где условие вычисляется в каждой ветви case:
// без выражения после switch
switch {
case value == 0:
fmt.Println("Zero!")
case value > 0:
fmt.Println("Positive integer")
case value < 0:
fmt.Println("Negative integer")
default:
fmt.Println("This should not happen:", value)
}
}
Мы получаем больше гибкости, но должны приложить больше усилий‚ чтобы вникнуть в код. В этом случае ветка default не должна выполниться главным образом потому, что любое допустимое целочисленное значение будет перехвачено тремя другими. Тем не менее ветка default присутствует, что является хорошей практикой, поскольку она может перехватывать неожиданные значения.
При выполнении control.go мы получаем такой вывод:
$ go run control.go 10
Value: 10
Positive integer
$ go run control.go 0
Zero!
Zero!
Каждый из двух блоков switch в control.go создает одну строку вывода.
Итерации с помощью циклов for и range
Данный подраздел посвящен итерациям в Go. Этот язык поддерживает циклы for, а также ключевое слово range для перебора всех элементов массивов, срезов и (как мы увидим в главе 3) карт. Примером простоты Go служит тот факт, что язык обеспечивает поддержку только ключевого слова for, вместо того чтобы включать прямую поддержку циклов while. Однако в зависимости от записи цикл for может функционировать как while или бесконечный цикл. Более того, в сочетании с ключевым словом range циклы for могут реализовать функциональность forEach из JavaScript.
Цикл for нужно заключать в фигурные скобки, даже если он содержит один оператор или вообще не содержит операторы.
Вы также можете создавать циклы for с переменными и условиями. Цикл for можно завершить с помощью ключевого слова break или пропустить текущую итерацию, применив ключевое слово continue. При использовании с range циклы for позволяют просматривать все элементы среза или массива, не зная размер структуры данных. Как вы увидите в главе 3, for и range аналогичным образом позволяют выполнять итерации по элементам карты.
В следующей программе показано использование for как отдельно‚ так и с ключевым словом range. Введите ее и сохраните как forLoops.go, чтобы выполнить в дальнейшем:
package main
import "fmt"
func main() {
// традиционный цикл for
for i := 0; i < 10; i++ {
fmt.Print(i*i, " ")
}
fmt.Println()
}
В этом коде показан традиционный цикл for, который использует локальную переменную i. Код выведет на экран квадраты 0, 1, 2, 3, 4, 5, 6, 7, 8 и 9. Квадрат 10 не выводится, поскольку не удовлетворяет условию 10 < 10.
Следующий код является идиоматическим Go:
i := 0
for ok := true; ok; ok = (i != 10) {
fmt.Print(i*i, " ")
i++
}
fmt.Println()
Можно использовать и его, но зачастую его трудно читать, особенно новичкам. Следующий код показывает, как цикл for может имитировать цикл while, который напрямую не поддерживается:
// цикл for, используемый как цикл while
i := 0
for {
if i == 10 {
break
}
fmt.Print(i*i, " ")
i++
}
fmt.Println()
Ключевое слово break в условии if досрочно завершает цикл и действует как условие выхода из цикла.
Наконец, с помощью range мы выполняем итерацию по всем элементам среза aSlice, что возвращает два упорядоченных значения: индекс текущего элемента в срезе и его значение. Если вы хотите проигнорировать любое из этих возвращаемых значений, что в нашем случае не нужно‚ то можете использовать _ вместо значения, которое хотите проигнорировать. Если вам нужен только индекс, то можете полностью исключить второе значение из range, не используя _.
// это срез, но range работает и с массивами
aSlice := []int{-1, 2, 1, -1, 2, -2}
for i, v := range aSlice {
fmt.Println("index:", i, "value: ", v)
}
Если вы запустите forLoops.go, то получите следующий вывод:
$ go run forLoops.go
0 1 4 9 16 25 36 49 64 81
0 1 4 9 16 25 36 49 64 81
0 1 4 9 16 25 36 49 64 81
index: 0 value: -1
index: 1 value: 2
index: 2 value: 1
index: 3 value: -1
index: 4 value: 2
index: 5 value: -2
В этом выводе показано, что первые три цикла for эквивалентны и, следовательно, выдают один и тот же результат. Последние шесть строк показывают индекс и значение каждого элемента, найденного в aSlice.
Теперь, когда мы познакомились с циклами for, посмотрим, как получить пользовательский ввод.
Получение пользовательского ввода
Получение пользовательского ввода — важная часть любой программы. В этом подразделе представлены два способа получения пользовательского ввода, которые заключаются в чтении из стандартного ввода и использовании аргументов командной строки программы.
Чтение стандартного ввода
Функция fmt.Scanln() помогает прочитать пользовательский ввод, когда программа уже запущена, и сохранить его в переменной string, которая передается как указатель в fmt.Scanln(). Пакет fmt содержит дополнительные функции для чтения пользовательского ввода из консоли (os.Stdin), из файлов или списков аргументов.
В следующем коде показано чтение из стандартного ввода — введите его и сохраните как input.go:
package main
import (
"fmt"
)
func main() {
// получить пользовательский ввод
fmt.Printf("Please give me your name: ")
var name string
fmt.Scanln(&name)
fmt.Println("Your name is", name)
}
В ожидании ввода данных полезно сообщить пользователю, какую информацию он должен предоставить, что и является целью вызова fmt.Printf(). Мы не используем здесь fmt.Println(), поскольку fmt.Println() автоматически добавляет символ новой строки в конце, а здесь это не нужно.
Выполнение input.go дает следующий вывод и взаимодействие с пользователем:
$ go run input.go
Please give me your name: Mihalis
Your name is Mihalis
Работа с аргументами командной строки
Использование пользовательского ввода при необходимости может показаться хорошей идеей, но обычно реальное программное обеспечение так не работает. Как правило, пользовательский ввод предоставляется исполняемому файлу в виде аргументов командной строки. По умолчанию аргументы командной строки в Go хранятся в срезе os.Args. Go также предлагает для анализа аргументов командной строки пакет flag, но есть лучшие и более эффективные альтернативы.
На рис. 1.2 показано‚ как работают аргументы командной строки в Go. Этот принцип повторяет работу в языке программирования C. Важно понимать, что срез os.Args должным образом инициализируется Go и программа может к нему обращаться. os.Args содержит значения string.
Рис. 1.2. Как работает срез os.Args
Первый аргумент командной строки в срезе os.Args — это всегда имя исполняемого файла. Если вы используете go run, то получите временное имя и путь. В противном случае это будет путь к исполняемому файлу, указанный пользователем. Остальные аргументы — это то, что следует за именем исполняемого файла‚ а именно различные аргументы командной строки‚ автоматически разделенные пробелами, если только они не заключены в двойные или одинарные кавычки.
Использование os.Args показано в следующем коде. Его задача состоит в поиске минимального и максимального числовых значений входных данных, при этом недопустимые входные данные, такие как символы и строки‚ игнорируются. Введите код и сохраните как cla.go (или под любым другим именем‚ которое вам по душе):
package main
import (
"fmt"
"os"
"strconv"
)
Файл cla.go ожидаемо начинается со своей преамбулы. Пакет fmt используется для вывода данных, а пакет os нужен потому, что os.Args является его частью. Наконец, пакет strconv содержит функции для преобразования строк в числовые значения. Далее мы хотим убедиться, что у нас есть хотя бы один аргумент командной строки:
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Need one or more arguments!")
return
}
Помните, что первый элемент в os.Args — это всегда путь к исполняемому файлу. Поэтому os.Args никогда не бывает полностью пустым. Далее программа проверяет наличие ошибок тем же способом, который мы встречали в предыдущих примерах. Больше информации об ошибках и их обработке вы узнаете в главе 2.
var min, max float64
for i := 1; i < len(arguments); i++ {
n, err := strconv.ParseFloat(arguments[i], 64)
if err != nil {
continue
}
В этом случае мы используем переменную error, возвращаемую strconv.parseFloat(), чтобы убедиться, что вызов strconv.parseFloat() прошел успешно и мы получили допустимое числовое значение для дальнейшей обработки. В противном случае нужно перейти к следующему аргументу командной строки. Цикл for используется для перебора всех доступных аргументов командной строки, кроме первого, который использует значение индекса 0. Это еще один распространенный метод работы со всеми аргументами командной строки.
Следующий код используется для правильной инициализации значений min и max после того‚ как был обработан первый аргумент командной строки:
if i == 1 {
min = n
max = n
continue
}
Мы используем i == 1 в качестве проверки того, является ли итерация первой. В данном случае это именно так, поэтому мы обрабатываем первый аргумент командной строки. Следующий код проверяет, является ли текущее значение нашим новым минимумом или максимумом — именно здесь реализуется логика программы:
if n < min {
min = n
}
if n > max {
max = n
}
}
fmt.Println("Min:", min)
fmt.Println("Max:", max)
}
Последняя часть программы предназначена для вывода результатов, которые представляют собой минимальное и максимальное числовые значения из всех допустимых аргументов командной строки. Результат, который вы получите после запуска cla.go, зависит от входных данных:
$ go run cla.go a b 2 -1
Min: -1
Max: 2
В этом случае a и b — недопустимые данные, и единственными допустимыми входными данными являются -1 и 2, которые служат минимальным и максимальным значениями соответственно.
$ go run cla.go a 0 b -1.2 10.32
Min: -1.2
Max: 10.32
В этом случае a и b являются недопустимыми входными данными и поэтому игнорируются.
$ go run cla.go
Need one or more arguments!
В последнем случае, поскольку cla.go не имеет входных данных для обработки, программа выводит информационное сообщение. Если вы запустите программу без допустимых входных значений, например go run cla.go a b c, то значения min и max будут равны нулю.
В следующем подразделе показан метод различения разных типов данных с использованием переменных error.
Использование переменных ошибок для различения типов входных данных
Теперь позвольте продемонстрировать вам метод, который использует переменные error для различения видов пользовательского ввода. Чтобы метод сработал, необходимо перейти от конкретных случаев к более общим. Если мы говорим о числовых значениях, то необходимо сначала проверить, является ли строка допустимым целым числом, после чего уже проверять, является ли та же самая строка значением с плавающей запятой. Именно в такой последовательности‚ поскольку каждое допустимое целое число также является допустимым значением с плавающей запятой.
Это показано в следующем фрагменте кода:
var total, nInts, nFloats int
invalid := make([]string, 0)
for _, k := range arguments[1:] {
// Это целое число?
_, err := strconv.Atoi(k)
if err == nil {
total++
nInts++
continue
}
Сначала мы создаем три переменные для подсчета общего количества проверенных допустимых значений, найденных целочисленных значений и найденных значений с плавающей запятой соответственно. Переменная invalid, представляющая собой срез, используется для сохранения всех нечисловых значений.
Повторюсь, нам нужно перебрать все аргументы командной строки, кроме первого с индексом 0, поскольку это путь к исполняемому файлу. Мы игнорируем путь к исполняемому файлу, используя arguments[1:] вместо просто arguments, — выбор непрерывной части среза мы обсудим в следующей главе.
Вызов strconv.Atoi() определяет, обрабатываем ли мы допустимое значение int. Если это так, то мы увеличиваем счетчики total и nInts:
// число с плавающей запятой
_, err = strconv.ParseFloat(k, 64)
if err == nil {
total++
nFloats++
continue
}
Аналогично, если проверяемая строка представляет допустимое значение с плавающей запятой, то вызов strconv.parseFloat() будет успешным, а программа обновит соответствующие счетчики. Наконец, если значение вообще не является числовым, оно добавляется к срезу invalid с помощью вызова append():
// значит‚ недопустимое значение
invalid = append(invalid, k)
}
Это обычная методика сохранения неожиданных входных данных в приложениях. Вышеприведенный код можно найти в process.go в репозитории книги на GitHub. Здесь же представлен дополнительный код, который предупреждает вас, если ваш недопустимый ввод превышает допустимый. При выполнении process.go мы получаем такой вывод:
$ go run process.go 1 2 3
#read: 3 #ints: 3 #floats: 0
В этом случае мы обрабатываем 1, 2 и 3. Все они являются допустимыми целочисленными значениями.
$ go run process.go 1 2.1 a
#read: 2 #ints: 1 #floats: 1
В этом случае у нас есть допустимое целое число 1, значение с плавающей запятой 2.1 и недопустимое значение a.
$ go run process.go a 1 b
#read: 1 #ints: 1 #floats: 0
Too much invalid input: 2
a
b
Если недопустимых входных данных больше, чем допустимых, то process.go выводит дополнительное сообщение об ошибке.
В следующем подразделе мы обсудим модель параллелизма Go.
Знакомство с моделью параллелизма в Go
Этот подраздел представляет собой краткое введение в модель параллелизма в Go. Она реализована с использованием горутин и каналов. Горутина — это наименьшая исполняемая сущность в Go. Чтобы создать новую горутину, вы должны использовать ключевое слово go, за которым следует предопределенная или анонимная функция — оба метода эквивалентны, если речь идет о Go.
Обратите внимание, что вы можете выполнять функции или анонимные функции только как горутины.
Канал в Go — это механизм, который, помимо прочего, позволяет горутинам взаимодействовать и обмениваться данными. Если вы программист-любитель или впервые слышите о горутинах и каналах, то не паникуйте. Горутины и каналы, а также конвейеры и совместное использование данных горутинами мы рассмотрим гораздо более подробно в главе 7.
Несмотря на то что создавать горутины легко, при параллельном программировании мы сталкиваемся с другими сложностями, включая синхронизацию и обмен данными между горутинами (это механизм Go, позволяющий избежать побочных эффектов при запуске горутин). Поскольку main() также выполняется как горутина, нам не нужно‚ чтобы она завершалась раньше других горутин программы, поскольку при выходе из main() завершится вся программа вместе с любыми еще не завершенными горутинами. Горутины не имеют общих переменных, но могут совместно использовать память. Хорошо то, что существуют различные методы‚ заставляющие main() ожидать, пока горутины будут обмениваться данными по каналам или, что реже встречается в Go, используя общую память.
Введите следующую Go-программу, которая синхронизирует горутины с помощью вызовов time.Sleep() (это неправильный способ синхронизации горутин‚ а правильный мы обсудим в главе 7), в ваш любимый редактор и сохраните как goRoutines.go:
package main
import (
"fmt"
"time"
)
func myPrint(start, finish int) {
for i := start; i <= finish; i++ {
fmt.Print(i, " ")
}
fmt.Println()
time.Sleep(100 * time.Microsecond)
}
func main() {
for i := 0; i < 5; i++ {
go myPrint(i, 5)
}
time.Sleep(time.Second)
}
Здесь в наивно реализованном примере мы создаем четыре горутины и выводим на экран определенные значения с помощью функции myPrint() — для создания горутин используется ключевое слово go. При выполнении goRoutines.go мы получаем такой вывод:
$ go run goRoutines.go
2 3 4 5
0 4 1 2 3 1 2 3 4 4 5
5
3 4 5
5
Однако если вы запустите программу несколько раз, то, скорее всего, будете каждый раз получать разные выходные данные:
1 2 3 4 5
4 2 5 3 4 5
3 0 1 2 3 4 5
4 5
Это происходит потому, что горутины инициализируются и начинают выполняться в случайном порядке. Планировщик Go отвечает за выполнение горутин точно так же, как планировщик ОС — за выполнение потоков ОС. В главе 7 мы обсудим это более подробно. Кроме того‚ мы представим и решение данной проблемы случайности с помощью переменной sync.WaitGroup. Однако имейте в виду, что параллелизм в Go присутствует повсюду и это является основной причиной добавления данного подраздела. Поэтому, поскольку в некоторых сообщениях об ошибках компиляции говорится о горутинах, не следует думать, что эти горутины были созданы именно вами.
В следующем разделе приводится практический пример, который заключается в разработке на Go версии утилиты which(1), находящей программный файл в пользовательском значении PATH.
Разработка утилиты which(1) на Go
Go может работать с вашей операционной системой с помощью набора пакетов. Хорошим способом изучения нового языка программирования неизменно является попытка реализовать упрощенные версии традиционных утилит UNIX. В этом разделе вы увидите реализованную на Go версию утилиты which(1)‚ которая поможет вам разобраться в том, как Go взаимодействует с базовой ОС и получает переменные среды.
Представленный код, реализующий функционал which(1), можно разделить на три логические части. Первая посвящена получению входного аргумента, представляющего собой имя исполняемого файла, который утилита будет искать. Вторая отвечает за чтение переменной среды PATH, ее разделение и перебор каталогов переменной PATH. Третья посвящена поиску в этих каталогах нужного двоичного файла и выяснению того, возможно ли его найти в принципе‚ а также является ли он обычным файлом или исполняемым. Если нужный исполняемый файл найден, то программа прекращает работу с помощью оператора return. В противном случае она завершается после окончания цикла for и прекращения работы функции main().
Теперь взглянем на сам код, начиная с логической преамбулы, которая обычно включает имя пакета, оператор import и другие определения с глобальной областью действия:
package main
import (
"fmt"
"os"
"path/filepath"
)
Пакет fmt используется для вывода на экран, пакет os — для взаимодействия с базовой операционной системой, а пакет path/filepath — для работы с содержимым переменной PATH, которая считывается как строка с длиной, зависящей от количества содержащихся в ней каталогов.
Вторая логическая часть утилиты выглядит так:
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Please provide an argument!")
return
}
file := arguments[1]
path := os.Getenv("PATH")
pathSplit := filepath.SplitList(path)
for _, directory := range pathSplit {
Сначала мы считываем аргументы командной строки программы (os.Args) и сохраняем первый аргумент в переменной file. Затем получаем содержимое переменной среды PATH и разделяем его с помощью функции filepath.SplitList(), которая предлагает переносимый способ разделения списка путей. Наконец, перебираем все каталоги переменной PATH, используя цикл for вместе с range‚ поскольку filepath.SplitList() возвращает срез.
Оставшаяся часть утилиты содержит следующий код:
fullPath := filepath.Join(directory, file)
// Существует ли путь?
fileInfo, err := os.Stat(fullPath)
if err == nil {
mode := fileInfo.Mode()
// Это обычный файл?
if mode.IsRegular() {
// Является ли он исполняемым?
if mode&0111 != 0 {
fmt.Println(fullPath)
return
}
}
}
}
}
Мы создаем полный путь и изучаем его с помощью функции filepath.Join(), которая используется для объединения различных частей пути с помощью разделителя, характерного для конкретной ОС‚ что позволяет filepath.Join() работать во всех поддерживаемых операционных системах. В этой части мы также получаем некоторую информацию о файле более низкого уровня. Нужно помнить, что в UNIX все представляет собой файл, а это значит, необходимо убедиться в том, что мы имеем дело с обычным файлом, который также является исполняемым.
В этой главе мы приводим весь код представленных исходных файлов. Однако начиная с главы 2 ситуация изменится. Этим мы убиваем сразу двух зайцев: вы видите только действительно важный код, а мы экономим место в книге.
Выполнение which.go генерирует следующий результат:
$ go run which.go which
/usr/bin/which
$ go run which.go doesNotExist
Последней команде не удалось найти исполняемый файл DoesNotExist. В соответствии с философией UNIX и тем, как работают конвейеры UNIX, утилиты ничего не выводят на экран, если им нечего сказать. Однако код выхода, равный 0, означает успех, тогда как ненулевой код выхода обычно означает неудачу.
Хотя вывод сообщений об ошибках на экран довольно полезен, бывают случаи, когда требуется сохранить все сообщения об ошибках и иметь возможность выполнять по ним поиск по мере необходимости. В таком случае вам нужно использовать один или несколько файлов журнала.
Вывод информации в лог
Все системы UNIX имеют собственные файлы журналов (логов) для протоколирования информации, поступающей от запущенных серверов и программ. Обычно большинство файлов системного журнала системы UNIX можно найти в каталоге /var/log. Однако файлы журналов многих популярных сервисов, таких как Apache и Nginx, расположены в другом месте в зависимости от их конфигурации.
Ведение журнала и внесение информации в файлы журналов — это практический способ асинхронного анализа данных и информации из вашего программного обеспечения либо локально, либо на центральном сервере журналов, а также с помощью серверного программного обеспечения, такого как Elasticsearch, Beats и Grafana Loki.
Вообще говоря, использование файла журнала для записи некоторой информации раньше считалось лучшей практикой, нежели вывод той же информации на экран‚ по двум причинам: во-первых, вывод не теряется, поскольку хранится в файле, и, во-вторых, вы можете осуществлять поиск и обрабатывать файлы журнала с помощью инструментов UNIX, таких как grep(1), awk(1) и sed(1), что не получится сделать, если сообщения выводятся в окне терминала.
Однако сейчас это уже не так.
Поскольку мы обычно запускаем наши сервисы через systemd, программы должны выводить лог в stdout, чтобы systemd имел возможность поместить данные в журнал. Страница https://12factor.net/logs содержит дополнительную информацию о журналах приложений. Кроме того, в облачных приложениях рекомендуется просто выводить лог в stderr и позволить контейнерной системе перенаправить поток stderr в нужное место.
Сервис журналирования UNIX поддерживает два свойства, названные уровнем журналирования (logging level) и средством журналирования (logging facility). Уровень журналирования — это значение, которое определяет серьезность записи в журнале. Существуют различные уровни журналирования, включающие в себя debug, info, notice, warning, err, crit, alert и emerg (в обратном порядке важности). Пакет log стандартной библиотеки Go не поддерживает работу с уровнями журналирования. Средство журналирования напоминает категорию‚ присваиваемую записи в журнале, и может принимать значения auth, authpriv, cron, daemon, kern, lpr, mail, mark, news, syslog, user, UUCP, local0, local1, local2, local3, local4, local5, local6 или local7 и определяется внутри /etc/syslog.conf, /etc/rsyslog.conf либо другого подходящего файла в зависимости от серверного процесса, используемого для ведения системного журнала на вашем компьютере UNIX. Это означает, что если средство журналирования определено некорректно, то запись не будет обработана. Таким образом, сообщения журнала, которые вы в него отправляете, могут быть проигнорированы и, следовательно, утрачены.
Пакет log отправляет сообщения журнала в стандартную ошибку. Частью пакета log является log/syslog, который позволяет отправлять сообщения журнала на сервер syslog вашего компьютера. Хотя по умолчанию log пишет в стандартную ошибку, использование log. SetOutput() меняет это поведение. В список функций журналирования входят log.Printf(), log.Print(), log.Println(), log.Fatalf(), log.Fatalln(), log.Panic(), log.Panicln() и log.Panicf().
Ведение журнала предназначено для кода приложения, а не библиотеки. Если вы разрабатываете библиотеки, то не включайте в них журналирование.
Чтобы осуществить запись в системные журналы, вам необходимо вызвать функцию syslog.New() с соответствующими параметрами. Запись в основной файл системного журнала осуществляется путем простого вызова syslog.New() с параметром syslog.LOG_SYSLOG. После этого нужно сообщить вашей Go-программе, что вся информация о ведении журнала поступает в новое средство ведения журнала — это реализуется с помощью вызова функции log.SetOutput(). Процесс показан в следующем коде — введите его в текстовом редакторе и сохраните как systemLog.go:
package main
import (
"log"
"log/syslog"
)
func main() {
sysLog, err := syslog.New(syslog.LOG_SYSLOG, "systemLog.go")
if err != nil {
log.Println(err)
return
} else {
log.SetOutput(sysLog)
log.Print("Everything is fine!")
}
}
После вызова log.SetOutput() вся информация журнала поступает в переменную журналирования syslog, которая перенаправляет ее в syslog.LOG_SYSLOG. Пользовательский текст для записей журнала, поступающих из этой программы, указывается в качестве второго параметра при вызове syslog.New().
Обычно требуется сохранить данные журнала в пользовательских файлах, поскольку они группируют соответствующую информацию, что облегчает обработку и проверку.
При выполнении systemLog.go данные не выводятся. Однако если вы посмотрите на системные журналы, например, компьютера с macOS Big Sur‚ то внутри /var/log/system.log обнаружите записи, подобные следующим:
Dec 5 16:20:10 iMac systemLog.go[35397]: 2020/12/05 16:20:10
Everything is fine!
Dec 5 16:43:18 iMac systemLog.go[35641]: 2020/12/05 16:43:18
Everything is fine!
Число в скобках — это идентификатор процесса, который внес запись в журнал, — в нашем случае это 35397 и 35641.
Аналогично, если вы запустите journalctl -xe на компьютере с Linux, то можете увидеть записи, подобные этим:
Dec 05 16:33:43 thinkpad systemLog.go[12682]: 2020/12/05 16:33:43
Everything is fine!
Dec 05 16:46:01 thinkpad systemLog.go[12917]: 2020/12/05 16:46:01
Everything is fine!
Выходные данные в вашей операционной системе могут слегка различаться, но общая идея сохранится.
Плохие вещи происходят постоянно, даже с хорошими людьми и хорошим ПО. Так что следующий подраздел мы посвятим способам‚ с помощью которых Go справляется с плохими ситуациями в ваших программах.
Функции log.Fatal() и log.Panic()
log.Fatal() используется, когда произошло что-то неправильное и вам просто требуется выйти из программы как можно скорее после сообщения об этой плохой ситуации. Вызов log.Fatal() завершает Go-программу в точке, где log.Fatal() была вызвана после вывода сообщения об ошибке. В большинстве случаев оно может содержать Not enough arguments, Cannot access file или что-то подобное. Кроме того, функция возвращает ненулевой код выхода, который в UNIX указывает на ошибку.
Бывают ситуации, когда программа вот-вот аварийно завершится и вы хотите получить как можно больше информации о сбое — log.Panic() подразумевает нечто действительно неожиданное и неизвестное, например невозможность найти файл, который был ранее доступен‚ или нехватку места на диске. Аналогично функции log.Fatal(), log.Panic() выводит пользовательское сообщение и немедленно завершает работу Go-программы.
Имейте в виду‚ что, с одной стороны, log.Panic() эквивалентна вызову log.Print(), за которым следует вызов panic() — встроенной функции, которая останавливает выполнение текущей функции и начинает паниковать. После этого происходит возврат в вызывающую функцию. С другой стороны, log.Fatal() вызывает log.Print(), а затем os.Exit(1), что является способом немедленного завершения текущей программы.
Как log.Fatal()‚ так и log.Panic() показаны в файле logs.go, который содержит следующий Go-код:
package main
import (
"log"
"os"
)
func main() {
if len(os.Args) != 1 {
log.Fatal("Fatal: Hello World!")
}
log.Panic("Panic: Hello World!")
}
Если запустить logs.go без каких-либо аргументов командной строки, то он вызовет log.Panic(). В противном случае — log.Fatal(). Это показано в следующем выводе из системы Arch Linux:
$ go run logs.go
2020/12/03 18:39:26 Panic: Hello World!
panic: Panic: Hello World!
goroutine 1 [running]:
log.Panic(0xc00009ef68, 0x1, 0x1)
/usr/lib/go/src/log/log.go:351 +0xae
main.main()
/home/mtsouk/Desktop/mGo3rd/code/ch01/logs.go:12 +0x6b
exit status 2
$ go run logs.go 1
2020/12/03 18:39:30 Fatal: Hello World!
exit status 1
Таким образом, вывод log.Panic() включает в себя дополнительную низкоуровневую информацию, которая, как мы надеемся, поможет вам разрешить сложные ситуации, возникшие в вашем Go-коде.
Запись в пользовательский файл журнала
В большинстве случаев, и особенно в приложениях и сервисах, развернутых в рабочей среде, вам часто нужно просто записать данные протокола в выбранный файл журнала. Для этого может быть множество причин, включая запись отладочных данных без вмешательства в файлы системного журнала или хранение ваших собственных данных журнала отдельно от системных журналов в целях передачи его или сохранения в базе данных либо программном обеспечении, таком как Elasticsearch. В этом подразделе рассказывается, как выполнять запись в пользовательский файл журнала, который обычно относится к конкретному приложению.
Запись в файлы и файловый ввод/вывод вы изучите в главе 6, однако сохранять информацию в файлах очень удобно при устранении ошибок и отладке Go-кода, поэтому данной темы мы коснемся и здесь.
Путь к используемому файлу журнала жестко закодирован в коде с использованием глобальной переменной LOGFILE. Для целей этой главы и для предотвращения переполнения вашей файловой системы в случае, если что-то пойдет не так, этот файл журнала находится в каталоге /tmp, который не является обычным местом для хранения данных, поскольку обычно /tmp очищается после каждой перезагрузки.
Кроме того, на данный момент это избавит вас от необходимости запускать CustomLog.go с правами root и от размещения ненужных файлов в ваших драгоценных системных каталогах.
Введите следующий код и сохраните его как CustomLog.go:
package main
import (
"fmt"
"log"
"os"
"path"
)
func main() {
LOGFILE := path.Join(os.TempDir(), "mGo.log")
f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// вызов os.OpenFile() создает файл журнала для записи,
// если он еще не существует, или же открывает его для записи
// путем добавления новых данных в конце (ос.O_APPEND)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
Ключевое слово defer сообщает Go‚ что нужно выполнить оператор непосредственно перед возвратом текущей функции. Это означает, что f.Close() будет выполнена непосредственно перед возвратом main(). Мы более подробно рассмотрим defer в главе 5.
iLog := log.New(f, "iLog ", log.LstdFlags)
iLog.Println("Hello there!")
iLog.Println("Mastering Go 3rd edition!")
}
Последние три оператора создают новый файл журнала на основе открытого (f) и записывают в него два сообщения с помощью Println().
Если вы когда-нибудь решите использовать код CustomLog.go в реальном приложении, то вам следует изменить путь, хранящийся в файле LOGFILE, на что-то более осмысленное.
При выполнении CustomLog.go данные не выводятся. Однако что действительно важно, так это то, что при этом записывается в пользовательский журнал:
$ cat /tmp/mGo.log
iLog 2020/12/05 17:31:07 Hello there!
iLog 2020/12/05 17:31:07 Mastering Go 3rd edition!
Вывод номеров строк в записях журнала
В этом подразделе вы узнаете, как вывести имя файла, а также номер строки‚ в которой находится оператор, оставивший запись в журнале.
Эта функциональность реализована с использованием log.Lshortfile в параметрах log.New() или setFlags(). Флаг log.Lshortfile добавляет имя файла, а также номер строки с Go-оператором, который внес запись журнала‚ в саму эту запись. Если вы используете log.Llongfile вместо log.Lshortfile, то получите полный путь к исходному файлу Go. Обычно в этом нет необходимости, особенно если у вас действительно длинный путь.
Введите следующий код и сохраните его как customLogLineNumber.go:
package main
import (
"fmt"
"log"
"os"
"path"
)
func main() {
LOGFILE := path.Join(os.TempDir(), "mGo.log")
f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
LstdFlags := log.Ldate | log.Lshortfile
iLog := log.New(f, "LNum ", LstdFlags)
iLog.Println("Mastering Go, 3rd edition!")
iLog.SetFlags(log.Lshortfile | log.LstdFlags)
iLog.Println("Another log entry!")
}
Вам разрешено изменять формат записей журнала во время выполнения программы. Иными словами‚ при необходимости вы можете вывести в запись журнала больше аналитической информации. Это реализуется с помощью нескольких вызовов iLog.setFlags().
При выполнении customLogLineNumber.go не генерирует выходных данных, но вносит следующие записи в путь к файлу, указанный в глобальной переменной LOGFILE:
$ cat /tmp/mGo.log
LNum 2020/12/05 customLogLineNumber.go:24: Mastering Go, 3rd edition!
LNum 2020/12/05 17:33:23 customLogLineNumber.go:27: Another log entry!
Скорее всего, на своем компьютере вы получите другой результат, что является вполне ожидаемым поведением.
Обзор Go-дженериков
В этом разделе обсуждаются Go-дженерики, которые являются функцией Go. В настоящее время тема дженериков в Go обсуждается сообществом Go. Так или иначе, полезно знать, как они работают, их философию и о чем конкретно ведутся споры в случае дженериков.
Go-дженерики — одно из наиболее востребованных дополнений к языку программирования Go.
Основная идея дженериков в Go, как и в любом другом поддерживающем их языке программирования, заключается в том, что при выполнении одной и той же задачи не приходится писать специальный код для поддержки нескольких типов данных.
В настоящее время Go поддерживает несколько типов данных в таких функциях, как fmt.Println(), путем использования пустого интерфейса и рефлексии (как интерфейсы, так и рефлексия обсуждаются в главе 4).
Однако требовать от каждого программиста написания большого количества кода и реализации множества функций и методов для поддержки нескольких пользовательских типов данных — не самое оптимальное решение. Здесь и вступают в дело дженерики, предоставляя альтернативу использованию интерфейсов и рефлексии для поддержки нескольких типов данных. В следующем коде показано, как и где могут быть полезны дженерики:
package main
import (
"fmt"
)
func Print[T any](s []T) {
for _, v := range s {
fmt.Print(v, " ")
}
fmt.Println()
}
func main() {
Ints := []int{1, 2, 3}
Strings := []string{"One", "Two", "Three"}
Print(Ints)
Print(Strings)
}
Здесь мы видим функцию Print(), использующую дженерики через переменную дженериков, которая задается с помощью [T any] после имени функции и перед параметрами функции. Благодаря использованию [T any] функция Print() может принимать любой срез любого типа данных и работать с ним. Однако Print() не работает ни с чем, кроме срезов. Это вполне нормально: если ваше приложение поддерживает срезы разных типов, то данная функция все равно избавляет вас от необходимости реализовывать несколько функций для поддержки каждого отдельного среза. Это и есть основная идея, лежащая в основе дженериков.
В главе 4 вы узнаете о пустом интерфейсе и о том, как его можно использовать для приема данных любого типа. Однако пустой интерфейс потребует дополнительного кода для работы с определенными типами данных.
Мы заканчиваем этот раздел изложением некоторых полезных фактов о дженериках.
• Вам не нужно постоянно использовать дженерики в своих программах.
• Вы можете продолжать работать с Go, как и раньше, даже если используете дженерики.
• Вы можете полностью заменить код дженериков кодом без использования дженериков. Вопрос в том, готовы ли вы писать дополнительный код, который необходим для этого?
• Я считаю, что дженерики следует использовать тогда, когда они помогают создавать более простой код и дизайн. Лучше иметь повторяющийся простой код, чем оптимальные абстракции, которые замедляют работу приложений.
• Бывают случаи, когда вам необходимо ограничить типы данных, поддерживаемые функцией, которая использует дженерики. Это неплохо, поскольку не все типы данных обладают одними и теми же возможностями. Вообще говоря, дженерики могут пригодиться при обработке типов данных, которые имеют какие-либо общие характеристики.
Вам потребуется время, чтобы освоить дженерики и использовать их в полной мере. Не торопитесь. Более подробно мы рассмотрим их в главе 13.
Разработка базового приложения телефонной книги
В этом разделе мы разработаем базовое приложение для телефонной книги в Go‚ что позволит нам применить полученные навыки на практике. Несмотря на свои ограничения, это приложение представляет собой утилиту командной строки, которая выполняет поиск среза структур, статически определенного (жестко закодированного) в Go-коде. Утилита поддерживает две команды: search и list. Первая выполняет поиск по заданной фамилии и возвращает полную запись, если фамилия найдена, а вторая перечисляет все доступные записи.
Наша реализация имеет ряд недостатков, в том числе такие:
• если вы хотите добавить или удалить какие-либо данные, то вам необходимо изменить исходный код;
• вы не можете представить данные в отсортированной форме, что не так страшно, когда у вас три записи, но может не работать с более чем 40 записями;
• вы не можете экспортировать свои данные или загрузить их извне;
• вы не можете распространять приложение телефонной книги в виде двоичного файла, поскольку в нем используются жестко закодированные данные.
В последующих главах мы расширим функциональность приложения телефонной книги, чтобы оно стало полностью функциональным, универсальным и эффективным.
Код phoneBook.go можно кратко описать следующим образом.
• Существует новый определяемый пользователем тип данных для хранения записей телефонной книги, который представляет собой структуру Go с тремя полями: Name, Surname и Tel. Структуры группируют набор значений в единый тип данных, позволяя передавать и получать этот набор значений как единый объект.
• Существует глобальная переменная, которая содержит данные телефонной книги и представляет собой срез структур data.
• Имеются две функции, которые помогут вам реализовать функциональность команд search и list.
• Содержимое глобальной переменной data определяется в функции main() с помощью нескольких вызовов append(). Вы можете изменять, добавлять или удалять содержимое среза data по мере необходимости.
• Наконец, программа может выполнять лишь одну задачу одновременно. Это означает, что для выполнения нескольких запросов вам придется запускать программу несколько раз.
Теперь рассмотрим phoneBook.go более подробно, начав с преамбулы:
package main
import (
"fmt"
"os"
)
Далее у нас есть раздел, в котором мы объявляем Go-структуру Entry, а также глобальную переменную data:
type Entry struct {
Name string
Surname string
Tel string
}
var data = []Entry{}
После этого определяем и реализуем две функции‚ обеспечивающие функциональность телефонной книги:
func search(key string) *Entry {
for i, v := range data {
if v.Surname == key {
return &data[i]
}
}
return nil
}
func list() {
for _, v := range data {
fmt.Println(v)
}
}
Функция search() выполняет линейный поиск по срезу data. Этот поиск выполняется медленно, но пока справляется с задачей, учитывая, что телефонная книга не содержит большого количества записей. Функция list() просто выводит содержимое среза data, используя цикл for совместно с range. Поскольку не требуется отображать индекс элемента, который мы выводим, то мы игнорируем его, используя символ _, и просто выводим структуру, содержащую фактические данные.
Наконец, в коде имеется реализация функции main(). Первая ее часть выглядит так:
func main() {
arguments := os.Args
if len(arguments) == 1 {
exe := path.Base(arguments[0])
fmt.Printf("Usage: %s search|list <arguments>\n", exe)
return
}
Переменная exe содержит путь к исполняемому файлу — это аккуратный и профессиональный способ напечатать имя исполняемого двоичного файла в инструкциях программы.
data = append(data, Entry{"Mihalis", "Tsoukalos", "2109416471"})
data = append(data, Entry{"Mary", "Doe", "2109416871"})
data = append(data, Entry{"John", "Black", "2109416123"})
В этой части мы проверяем, были ли нам предоставлены какие-либо командные аргументы. Если нет (len(аргументы) == 1), то программа выводит сообщение и завершает работу путем вызова return. В противном случае‚ прежде чем продолжить‚ она помещает нужные данные в срез data.
Оставшаяся часть реализации функции main() выглядит следующим образом:
// различаем команды
switch arguments[1] {
// команда поиска
case "search":
if len(arguments) != 3 {
fmt.Println("Usage: search Surname")
return
}
result := search(arguments[2])
if result == nil {
fmt.Println("Entry not found:", arguments[2])
return
}
fmt.Println(*result)
// команда списка
case "list":
list()
// ответ на все остальное
default:
fmt.Println("Not a valid option")
}
}
В этом коде используется блок case, который очень удобен, когда требуется написать читаемый код и избежать использования нескольких вложенных блоков if. Блок case различает две поддерживаемые команды, проверяя значение arguments[1]. Если команда не распознана, то вместо нее выполняется ветка default. В случае команды search также рассматривается arguments[2].
Работа с phoneBook.go выглядит следующим образом:
$ go build phoneBook.go
$ ./phoneBook list
{Mihalis Tsoukalos 2109416471}
{Mary Doe 2109416871}
{John Black 2109416123}
$ ./phoneBook search Tsoukalos
{Mihalis Tsoukalos 2109416471}
$ ./phoneBook search Tsouk
Entry not found: Tsouk
$ ./phoneBook
Usage: ./phoneBook search|list <arguments>
Первая команда перечисляет содержимое телефонной книги, тогда как вторая выполняет поиск по заданной фамилии (Tsoukalos). Третья команда ищет что-то, чего нет в телефонной книге, а последняя компилирует phoneBook.go и запускает сгенерированный исполняемый файл без каких-либо аргументов‚ что влечет за собой вывод инструкций.
Несмотря на недостатки, приложение phoneBook.go имеет понятный дизайн, который можно легко расширять, и работает ожидаемым образом, что служит отличной отправной точкой. Мы будем совершенствовать это приложение в следующих главах по мере того, как будем изучать более расширенные концепции.
Упражнения
• Наша версия which(1) останавливается после нахождения первого вхождения нужного исполняемого файла. Внесите необходимые изменения в which.go, чтобы найти все возможные вхождения нужного исполняемого файла.
• Текущая версия which.go обрабатывает только первый аргумент командной строки. Внесите необходимые изменения в which.go, чтобы принять переменную PATH и выполнить поиск нескольких исполняемых двоичных файлов.
• Ознакомьтесь с документацией пакета fmt по адресу https://golang.org/pkg/fmt/.
Резюме
Если вы столкнулись с Go впервые, то благодаря информации из этой главы получите представление о преимуществах языка, внешнем виде Go-кода и некоторых важных характеристиках Go, таких как переменные, итерации, управление потоком и модель параллелизма Go. Если вы уже знакомы с этим языком, то глава поможет освежить ваши знания о том, чем он отличается от других и для каких видов программного обеспечения его рекомендуется использовать. Наконец, мы создали базовое приложение для телефонной книги, используя техники, которые успели изучить.
В следующей главе мы более подробно рассмотрим основные типы данных Go.
Дополнительные ресурсы
• Официальный сайт Go: https://golang.org/.
• The Go Playground: https://play.golang.org/.
• Пакет log: https://golang.org/pkg/log/.
• Elasticsearch Beats: https://www.elastic.co/beats/.
• Grafana Loki: https://grafana.com/oss/loki/.
• Microsoft Visual Studio: https://visualstudio.microsoft.com/.
• Стандартная библиотека Go: https://golang.org/pkg/.
2. Основные типы данных Go
Данные хранятся и используются в переменных, и все переменные Go должны иметь тип данных, который определяется явно или неявно. Знание встроенных типов данных Go позволяет вам понять, как манипулировать простыми значениями данных и создавать более сложные структуры данных, когда простых типов недостаточно или они неэффективны в рамках данной задачи.
В текущей главе рассказывается об основных типах данных Go и структурах данных, которые позволяют группировать данные одного типа. Однако начнем с чего-то более практичного: представьте, что нам нужно прочитать данные в качестве аргументов командной строки утилиты. Как можно гарантировать, что прочитанное окажется именно тем, что мы ожидали? Как справляться с ошибочными ситуациями? Как насчет чтения из командной строки не только чисел и строк, но и дат и времени? Придется ли писать собственный синтаксический анализатор для работы с датами и временем?
В этой главе мы ответим на эти и многие другие вопросы, реализовав следующие три утилиты:
• утилиту командной строки, которая анализирует даты и время;
• утилиту, которая генерирует случайные числа и случайные строки;
• новую версию приложения телефонной книги, содержащую случайно сгенерированные данные.
В этой главе:
• тип данных error;
• числовые типы данных;
• нечисловые типы данных;
• Go-константы;
• группировка схожих данных;
• указатели;
• генерация случайных чисел;
• обновление приложения телефонной книги.
Начнем эту главу с типа данных error, поскольку ошибки в Go играют ключевую роль.
Тип данных error
Для представления условий ошибки и сообщений Go содержит специальный тип данных error. На практике это означает, что этот язык обрабатывает ошибки как значения. Чтобы успешно программировать на Go, вы должны иметь представление об ошибках, которые могут возникнуть по мере использования функций и методов, а также соответствующим образом их обрабатывать.
Как вы уже знаете из предыдущей главы, Go следует такому соглашению о значениях error: если значение переменной error равно nil, то ошибки не было. В качестве примера мы рассмотрим strconv.Atoi(), который используется для преобразования значения string в int (Atoi расшифровывается как ASCII to Int). Как и указано в сигнатуре, strconv.Atoi() возвращает (int, error). Значение error, равное nil, означает, что преобразование прошло успешно и что значение int можно использовать. Если значение error отлично от nil, это означает, что преобразование было неудачным и значение в string не является допустимым значением int.
