Golang для профи: работа с сетью, многопоточность, структуры данных и машинное обучение с Go
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Golang для профи: работа с сетью, многопоточность, структуры данных и машинное обучение с Go

 

Михалис Цукалос
Golang для профи: работа с сетью, многопоточность, структуры данных и машинное обучение с Go
2021

Научный редактор Р. Пресняков

Переводчики Е. Сандицкая (Полонская), О. Сивченко

Технический редактор Н. Гринчик

Литературные редакторы Н. Гринчик, А. Дубейко

Художники Н. Гринчик, В. Мостипан, Г. Синякина (Маклакова)

Корректоры Н. Гринчик, А. Лавровская, Е. Павлович

Верстка Г. Блинов


 

Михалис Цукалос

Golang для профи: работа с сетью, многопоточность, структуры данных и машинное обучение с Go. — СПб.: Питер, 2021.

 

ISBN 978-5-4461-1617-1

© ООО Издательство "Питер", 2021

 

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

 

Об авторе

Михалис Цукалос (Mihalis Tsoukalos) — администратор UNIX, программист, администратор баз данных и математик. Любит писать технические книги и статьи, узнавать что-то новое. Помимо этой книги, Михалис написал Go Systems Programming, а также более 250 технических статей для многих журналов, включая Sys Admin, MacTech, Linux User and Developer, Usenix ;login:, Linux Format и Linux Journal. Сфера научных интересов Михалиса — базы данных, визуализация, статистика и машинное обучение.

Вы можете связаться с автором через его сайт https://www.mtsoukalos.eu/ или по адресу @mactsouk.

Михалис также увлекается фотографией.

Я благодарю сотрудников Packt Publishing за помощь в написании этой книги, в том числе моего научного редактора Мэта Райера (Mat Ryer), а также Кишора Рита (Kishor Rit) за ответы на все мои вопросы и поддержку в период написания книги.

Эту книгу я посвящаю памяти моих любимых родителей Иоанниса и Аргетты.

О научном редакторе

Мэт Райер (Mat Ryer) пишет компьютерные программы с шести лет: сначала на BASIC для ZX Spectrum, а затем, вместе со своим отцом, — на AmigaBASIC и AMOS для Commodore Amiga. Много времени он потратил на копирование вручную кода из журнала Amiga Format, изменение значений переменных или ссылок операторов GOTO, чтобы посмотреть, что из этого выйдет. Тот же дух исследования и одержимость программированием привели 18-летнего Мэтта к работе в местной организации в Мансфилде (Великобритания), где он начал создавать веб-сайты и другие онлайн-сервисы.

После нескольких лет работы с различными технологиями в разных областях не только в Лондоне, но и по всему миру Мэт обратил внимание на новый язык системного программирования под названием Go, впервые использованный в Google. Поскольку Go решал очень актуальные и остросовременные технические проблемы, Мэт начал использовать этот язык для решения задач, когда Go находился еще на стадии бета-тестирования, и с тех пор продолжает программировать на нем. Мэт работал над разными проектами с открытым исходным кодом, создал несколько пакетов Go, в том числе Testify, Moq, Silk и Is, а также инструментарий для разработчиков в MacOS — BitBar.

С 2018 года Мэт — соучредитель компании Machine Box, но он по-прежнему принимает участие в конференциях, пишет о Go в своем блоге и является активным участником сообщества Go.

Предисловие

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

В книге много свежих интересных тем, включая совершенно новую главу об использовании Go для машинного обучения. Вы также найдете здесь описания и примеры кода для пакетов Viper и Cobra Go, gRPC, вы узнаете, как работать с образами Docker, файлами YAML, пакетами go/scanner и go/token, научитесь генерировать из Go код WebAssembly. В общей сложности второе издание книги «Golang для профи» расширилось более чем на 130 страниц.

Для кого предназначена эта книга

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

Часть информации из второго издания можно найти в другой книге автора — Go Systems Programming. Основное различие между этими двумя книгами заключается в том, что Go Systems Programming посвящена разработке системных инструментов с использованием возможностей языка Go, а «Golang для профи» — разъяснению возможностей и внутренних особенностей самого Go, что позволит вам стать лучшим разработчиком на Go. Обе книги могут служить справочниками после того, как вы их прочитаете один-два раза.

О чем эта книга

Глава 1 «Go и операционная система» начинается с истории возникновения языка Go и его преимуществ, затем приводится описание утилиты godoc и объяснение, как компилировать и выполнять Go-программы. Далее речь пойдет о выводе данных и получении данных от пользователя, об аргументах командной строки программы и использовании журнальных файлов. Последний раздел первой главы посвящен обработке ошибок, которая в Go играет ключевую роль.

Глава 2 «Go изнутри» посвящена сборщику мусора Go и принципам его работы. Затем поговорим о небезопасном коде и о пакете unsafe, а также о том, как вызывать из Go-программы код на C, а из C-программы — код на Go.

Вы узнаете, как использовать ключевое слово defer, а также познакомитесь с утилитами strace(1) и dtrace(1). В оставшихся разделах этой главы вы изучите, как получить информацию о вашей среде Go, как использовать ассемблер и как генерировать из Go код WebAssembly.

Глава 3 «Работа с основными типами данных Go» посвящена типам данных, предоставляемым в Go: массивам, срезам и хеш-таблицам, а также указателям, константам, циклам, функциям для работы с датами и временем. Вы точно не захотите пропустить эту главу!

Глава 4 «Использование составных типов данных» начинается с изучения структур Go и ключевого слова struct, после чего речь пойдет о кортежах, строках, рунах, байтовых срезах и строковых литералах. Из оставшейся части главы вы узнаете о регулярных выражениях и сопоставлении с образцом, об операторе switch, пакетах strings и math/big, о разработке на Go хранилища типа «ключ — значение» и о работе с файлами форматов XML и JSON.

Глава 5 «Как улучшить код Go с помощью структур данных» посвящена разработке пользовательских структур данных в тех случаях, когда стандартные структуры Go не соответствуют конкретной задаче. Здесь же рассмотрено построение и применение бинарных деревьев, связных списков, пользовательских хеш-таблиц, стеков и очередей, а также их преимущества. В этой главе продемонстрировано использование структур из стандартного пакета Go container, а также показано, как можно использовать Go для проверки головоломок судоку и генерации случайных чисел.

Глава 6 «Неочевидные знания о пакетах и функциях Go» посвящена пакетам и функциям, в том числе использованию функции init(), стандартного Go-пакета syscall, а также пакетов text/template и html/template. Кроме того, вы узнаете, как применять расширенные пакеты go/scanner, go/parser и go/token. Эта глава определенно улучшит ваши навыки разработки на Go!

В главе 7 «Рефлексия и интерфейсы на все случаи жизни» обсуждаются три передовые концепции Go: рефлексия, интерфейсы и методы типов. Кроме того, в этой главе описываются объектно-ориентированные возможности Go и способы отладки Go-программ с помощью Delve.

Глава 8 «Как объяснить UNIX-системе, что она должна делать» посвящена системному программированию на Go. Здесь рассматриваются такие темы, как пакет flag для работы с аргументами командной строки, обработка сигналов UNIX, файловый ввод и вывод, пакеты bytes, io.Reader и io.Writer, а также обсуждается использование пакетов Viper и Cobra Go. Напомню: если вы действительно увлекаетесь системным программированием на Go, то я настоятельно рекомендую вам после прочтения этой книги приобрести и прочесть Go Systems Programming!

В главе 9 «Конкурентность в Go: горутины, каналы и конвейеры» обсуждаются горутины, каналы и конвейеры — то, что позволяет реализовать конкурентность на Go.

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

Глава 10 «Конкурентность в Go: расширенные возможности» является продолжением предыдущей главы. Прочитав ее, вы станете повелителем горутин и каналов! Вы глубже изучите планировщик Go, научитесь использовать мощное ключевое слово select, узнаете о различных типах каналов Go, а также о разделяемой памяти, мьютексах, типах sync.Mutex и sync.RWMutex. В последней части главы говорится о пакете context, пулах обработчиков и о том, как распознать состояние гонки (race conditions).

В главе 11 «Тестирование, оптимизация и профилирование кода» обсуждаются тестирование, оптимизация и профилирование кода, а также кросс-компиляция под разные платформы, создание документации, тестирование производительности Go-кода, создание тестовых функций и поиск неиспользуемого Go-кода.

Глава 12 «Основы сетевого программирования на Go» посвящена пакету net/http и тому, как разрабатывать веб-клиенты и веб-серверы на Go. Далее рассмотрены структуры http.Response, http.Request и http.Transport, а также тип http.NewServeMux. Вы даже узнаете, как разработать на Go целый веб-сайт! Кроме того, в этой главе вы научитесь читать конфигурацию сетевых интерфейсов и выполнять на Go DNS-поиск, а также использовать с Go gRPC.

В главе 13 «Сетевое программирование: создание серверов и клиентов» рассказывается о работе с HTTPS-трафиком и создании на Go серверов и клиентов UDP и TCP с использованием функций из пакета net. Здесь же рассмотрены такие темы, как построение клиентов и серверов RPC, разработка на Go многопоточного TCP-сервера и чтение «сырых» сетевых пакетов.

В главе 14 «Машинное обучение на Go» рассказывается о реализации на Go алгоритмов машинного обучения, включая классификацию, кластеризацию, обнаружение аномалий, выбросы, нейронные сети и TensorFlow, а также работу Go с Apache Kafka.

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

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

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

Книга включает в себя материалы о Go и WebAssembly, использовании Docker с Go, создании профессиональных утилит работы с командной строкой с помощью пакетов Viper и Cobra, обработке JSON и YAML, выполнении операций с матрицами, работе с головоломками судоку, пакетами go/scanner и go/token, а также с git(1) и GitHub, пакетом atomic, об использовании в Go gRPC и HTTPS.

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

 

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

Как получить максимальную пользу от этой книги

Для работы с книгой вам потребуется UNIX-компьютер и установленная на нем относительно новая версия Go. Это может быть любой компьютер с операционной системой Mac OS X, macOS или Linux. Большая часть представленного здесь кода также будет работать и на компьютерах под управлением Microsoft Windows.

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

Загрузите файлы с примерами кода

Примеры кода для этой книги размещены на GitHub по адресу https://github.com/PacktPublishing/Mastering-Go-Second-Edition. В случае если код обновится, он будет также размещен в уже существующем репозитории GitHub.

Чтобы скачать файлы с кодом, нужно выполнить следующие действия.

1. Перейдите по указанной ссылке на сайт github.com.

2. Нажмите кнопку Clone or Download.

3. Щелкните кнопкой мыши на ссылке Download ZIP.

4. Скачайте архив с файлами примеров.

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

WinRAR или 7-Zip для Windows;

• Zipeg, iZip или UnRarX для Mac;

• 7-Zip или PeaZip для Linux.

По адресу https://github.com/PacktPublishing/ вам доступны и другие примеры кода из нашего богатого каталога книг и видео. Все это к вашим услугам!

Где скачать цветные иллюстрации

Мы предоставляем вам PDF-файл с цветными скриншотами и диаграммами, использованными в этой книге. Вы можете его скачать по этому адресу: https://static.packt-cdn.com/downloads/9781838559335.pdf.

Условные обозначения

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

Кодвтексте: размещенные в тексте книги кодовые слова, имена таблиц из базы данных, папок и файлов; расширения файлов, пути, фиктивные URL-адреса, вводимые пользователем данные и учетные записи Twitter. Например: «Первый способ похож на использование команды man(1), но только для функций и пакетов Go».

Блоки кода представлены следующим образом:

package main

import (

    "fmt"

)

func main() {

    fmt.Println("This is a sample Go program!")

}

Когда нужно обратить ваше внимание на определенную часть блока кода, соответствующие строки или элементы выделяются жирным шрифтом:

import (

    "fmt"

)

func main() {

    fmt.Println("This is a sample Go program!")

}

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

$ date

Sat Oct 21 20:09:20 EEST 2017

$ go version

go version go1.12.7 darwin/amd64

Курсивом обозначаются новые термины, жирным шрифтом — важные слова. То, что вы видите на экране (например, слова в меню или диалоговых окнах), отображается в тексте так: «Выберите на панели Administration раздел System info».

 

Так выглядят советы и рекомендации.

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

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

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

1. Go и операционная система

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

В этой главе рассмотрены следующие темы:

история и будущее языка программирования Go;

• преимущества Go;

• компиляция Go-кода;

• выполнение Go-кода;

• загрузка и использование внешних Go-пакетов;

• стандартный ввод, вывод и сообщения об ошибках в UNIX;

• вывод данных на экран;

• получение данных от пользователя;

• вывод данных о стандартных ошибках;

• работа с лог-файлами;

• использование Docker для компиляции и запуска исходных файлов Go;

обработка ошибок в Go.

История Go

Go — это современный универсальный язык программирования с открытым исходным кодом, выпуск которого официально состоялся в конце 2009 года. Go планировался как внутренний проект Google — это означает, что сначала он был запущен в качестве эксперимента и с тех пор вдохновлялся многими другими языками программирования, в том числе C, Pascal, Alef и Oberon. Создателями Go являются профессиональные программисты Роберт Гризмер (Robert Griesemer), Кен Томсон (Ken Thomson) и Роб Пайк (Rob Pike). Они разработали Go как язык для профессионалов, позволяющий создавать надежное, устойчивое и эффективное программное обеспечение. Помимо синтаксиса и стандартных функций, в состав Go входит довольно богатая стандартная библиотека.

На момент публикации этой книги последней стабильной версией Go была версия 1.14. Однако даже если номер вашей версии выше, книга все равно будет актуальной.

Если вы устанавливаете Go впервые, начните с посещения веб-сайта https://golang.org/dl/. Однако с большой вероятностью в вашем дистрибутиве UNIX уже есть готовый к установке пакет для языка программирования Go, поэтому вы можете получить Go с помощью обычного менеджера пакетов.

Куда движется Go?

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

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

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

Преимущества Go

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

Среди наиболее значимых преимуществ и возможностей Go можно отметить следующие.

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

• Цель Go — счастливые разработчики. Именно счастливые разработчики пишут самый лучший код!

• Компилятор Go выводит информативные предупреждения и сообщения об ошибках, которые помогут вам решить конкретную проблему. Проще говоря, компилятор Go будет вам помогать, а не портить жизнь, выводя бессмысленные сообщения!

• Код Go является переносимым, особенно между UNIX-машинами.

• Go поддерживает процедурное, параллельное и распределенное программирование.

• Go поддерживает сборку мусора, поэтому вам не придется заниматься выделением и освобождением памяти.

• У Go нет препроцессора; вместо этого выполняется высокоскоростная компиляция. Вследствие этого Go можно использовать как язык сценариев.

• Go позволяет создавать веб-приложения и предоставляет простой веб-сервер для их тестирования.

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

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

• Для разработки, отладки и тестирования Go-приложений вам не понадобится графический интерфейс пользователя (Graphical User Interface, GUI), так как Go можно использовать из командной строки — именно так, как, мне кажется, предпочитают многие пользователи UNIX.

• Go поддерживает Unicode, а следовательно, вам не понадобится дополнительная обработка для вывода символов на разных языках.

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

Идеален ли Go?

Идеального языка программирования не существует, и Go не исключение. Есть языки программирования, которые эффективнее в некоторых других областях программирования, или же мы их просто больше любим. Лично я не люблю Java, и хотя раньше мне нравился C++, сейчас он мне не нравится. C++ стал слишком сложным как язык программирования, а код на Java, на мой взгляд, не очень красиво смотрится.

Вот некоторые из недостатков Go:

у Go нет встроенной поддержки объектно-ориентированного программирования. Это может стать проблемой для тех программистов, которые привыкли писать объектно-ориентированный код. Однако вы можете имитировать наследование в Go, используя композицию;

• некоторые считают, что Go никогда не заменит C;

• C все еще остается более быстрым, чем любой другой язык системного программирования, главным образом потому, что UNIX написана на C.

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

Что такое препроцессор

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

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

Препроцессор используется в таких языках программирования, как C, C++, Ada, PL/SQL. Печально известный препроцессор C обрабатывает строки, которые начинаются с символа # и называются директивами или прагмами. Таким образом, директивы и прагмы не являются частью языка программирования C!

Утилита godoc

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

Утилита godoc может выполняться как обычное приложение командной строки, которое выводит данные на терминал, или же как приложение командной строки, которое запускает веб-сервер. В последнем случае для просмотра документации Go вам понадобится браузер.

 

Если ввести в командной строке просто godoc, без каких-либо параметров, то получим список параметров командной строки, поддерживаемых godoc.

Первый способ аналогичен использованию команды man(1), только для функций и пакетов Go. Например, чтобы получить информацию о функции Printf() из пакета fmt, необходимо ввести команду:

$ go doc fmt.Printf

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

$ go doc fmt

Второй способ требует выполнения godoc с параметром -http:

$ godoc -http=:8001

Число в предыдущей команде, в данном случае равное 8001, — это номер порта, который будет прослушивать HTTP-сервер. Вы можете указать любой доступный номер порта, если у вас есть необходимые привилегии. Однако обратите внимание, что номера портов от 0 до 1023 зарезервированы и могут использоваться только пользователем root, поэтому лучше избегать использования одного из этих портов и выбирать какой-нибудь другой, если только он еще не используется другим процессом.

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

$ godoc -http :8001

Если после этого ввести в браузере URL-адрес http://localhost:8001/pkg/, то вы получите список доступных пакетов Go и сможете просмотреть их документацию.

Компиляция Go-кода

Из этого раздела вы узнаете, как скомпилировать код на Go. Хорошая новость: вы можете скомпилировать код Go из командной строки без графического приложения. Более того, для Go не имеет значения имя исходного файла с текстом программы, если именем пакета является main и в нем есть только одна функция main(). Дело в том, что именно с функции main() начинается выполнение программы. Из-за этого в файлах одного проекта не может быть нескольких функций main().

Нашей первой скомпилированной Go-программой будет программа с именем aSourceFile.go, которая содержит следующий код Go:

package main

import (

    "fmt"

)

 

func main() {

    fmt.Println("This is a sample Go program!")

}

Обратите внимание, что сообщество Go предпочитает называть исходный файл Go source_file.go, а не aSourceFile.go. В любом случае, что бы вы ни выбрали, будьте последовательны.

Чтобы скомпилировать aSourceFile.go и создать статически скомпонованный исполняемый файл, нужно выполнить следующую команду:

$ go build aSourceFile.go

В результате будет создан новый исполняемый файл с именем aSourceFile, который теперь нужно выполнить:

$ file aSourceFile

aSourceFile: Mach-O 64-bit executable x86_64

$ ls -l aSourceFile

-rwxr-xr-x 1 mtsouk staff 2007576 Jan 10 21:10 aSourceFile

$ ./aSourceFile

This is a sample Go program!

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

Выполнение Go-кода

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

 

Этот способ позволяет использовать Go как язык сценариев, подобно Python, Ruby или Perl.

Итак, чтобы запустить aSourceFile.go, не создавая исполняемый файл, необходимо выполнить следующую команду:

$ go run aSourceFile.go

This is a sample Go program!

Как видим, результат выполнения этой команды точно такой же, как и раньше.

 

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

В этой книге для выполнения примеров кода в основном будет использоваться gorun; в первую очередь потому, что так проще, чем сначала запускать gobuild, а затем — исполняемый файл. Кроме того, gorun после завершения программы не оставляет файлов на жестком диске.

Два правила Go

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

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

Правило пакетов Go: не нужен — не подключай

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

Рассмотрим следующую простую программу, которая сохраняется как packa­geNotUsed.go:

package main

 

import (

    "fmt"

    "os"

)

 

func main() {

    fmt.Println("Hello there!")

}

 

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

Если попытаться выполнить packageNotUsed.go, то программа не будет выполнена, а мы получим от Go следующее сообщение об ошибке:

$ go run packageNotUsed.go

# command-line-arguments

./packageNotUsed.go:5:2: imported and not used: "os"

Если удалить пакет os из списка import программы, то packageNotUsed.go отлично скомпилируется — попробуйте сами.

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

package main

 

import (

    "fmt"

    _ "os"

)

 

func main() {

    fmt.Println("Hello there!")

}

Как видим, если в списке import поставить перед именем пакета символ подчеркивания, то мы не получим сообщение об ошибке в процессе компиляции, даже если этот пакет не используется в программе:

$ go run packageNotUsedUnderscore.go

Hello there!

 

Причина, по которой Go позволяет обойти это правило, станет более понятной в главе 6.

Правильный вариант размещения фигурных скобок — всего один

Рассмотрим следующую 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 for "main"

./curly.go:8:1: syntax error: unexpected semicolon or newline before {

Официально смысл этого сообщения об ошибке разъясняется так: во многих контекстах Go требует использования точки с запятой как признака завершения оператора и поэтому компилятор автоматически вставляет точки с запятой там, где считает их необходимыми. Поэтому при размещении открывающей фигурной скобки ({) в отдельной строке компилятор Go поставит точку с запятой в конце предыдущей строки (funcmain()) — это и есть причина сообщения об ошибке.

Как скачивать Go-пакеты

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

 

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

Вы узнаете намного больше о пакетах и модулях Go из главы 6.

Рассмотрим следующую простую Go-программу, которая сохраняется как getPackage.go:

package main

 

import (

    "fmt"

    "github.com/mactsouk/go/simpleGitHub"

)

 

func main() {

    fmt.Println(simpleGitHub.AddTwo(5, 6))

}

В одной из команд import указан интернет-адрес — это значит, что в программе используется внешний пакет. В данном случае внешний пакет называется simpleGitHub и находится по адресу github.com/mactsouk/go/simpleGitHub.

Если вы попытаетесь сразу выполнить getPackage.go, то будете разочарованы:

$ go run getPackage.go

getPackage.go:5:2: cannot find package

"github.com/mactsouk/go/simpleGitHub" in any of:

    /usr/local/Cellar/go/1.9.1/libexec/src/github.com/mactsouk/go/simpleGitHub

        (from $GOROOT)

    /Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub (from $GOPATH)

Как видно, необходимо установить недостающий пакет на ваш компьютер. Чтобы скачать этот пакет, нужно выполнить следующую команду:

$ go get -v github.com/mactsouk/go/simpleGitHub

github.com/mactsouk/go (download)

github.com/mactsouk/go/simpleGitHub

Загруженные файлы вы найдете в следующем каталоге:

$ ls -l ~/go/src/github.com/mactsouk/go/simpleGitHub/

total 8

-rw-r--r--  1 mtsouk  staff  66 Oct 17 21:47 simpleGitHub.go

Однако команда goget также компилирует пакет. Соответствующие файлы размещаются здесь:

$ ls -l ~/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a

-rw-r--r--  1 mtsouk  staff  1050 Oct 17 21:47

/Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/simpleGitHub.a

Теперь без проблем выполняйте getPackage.go:

$ go run getPackage.go

11

Временные файлы загруженного Go-пакета можно удалить:

$ go clean -i -v -x github.com/mactsouk/go/simpleGitHub

cd /Users/mtsouk/go/src/github.com/mactsouk/go/simpleGitHub

rm -f simpleGitHub.test simpleGitHub.test.exe

rm -f /Users/mtsouk/go/pkg/darwin_amd64/github.com/mactsouk/go/

simpleGitHub.a

Аналогичным образом с помощью UNIX-команды rm(1) можно удалить весь загруженный и размещенный на локальном компьютере Go-пакет, чтобы удалить его исходный Go-код после использования goclean:

$ go clean -i -v -x github.com/mactsouk/go/simpleGitHub

$ rm -rf ~/go/src/github.com/mactsouk/go/simpleGitHub

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

Стандартные потоки UNIX: stdin, stdout и stderr

В любой ОС UNIX есть три файла, которые постоянно открыты для своих процессов. Помните, что для UNIX все является файлом, даже если это принтер или мышь.

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

Таким образом, по умолчанию любая UNIX-система поддерживает три специальных стандартных имени файла: /dev/stdin, /dev/stdout и /dev/stderr, к которым также можно получить доступ, используя файловые дескрипторы 0, 1 и 2 соответственно. Эти три файловых дескриптора называются стандартными потоками ввода, вывода и ошибок соответственно. Кроме того, на компьютере с MacOS файловый дескриптор 0 может быть доступен как /dev/fd/0, а на компьютере с Debian Linux — как /dev/fd/0 или /dev/pts/0.

Для доступа к стандартному потоку ввода Go использует os.Stdin, для доступа к стандартному потоку вывода — os.Stdout и для доступа к стандартному потоку ошибок — os.Stderr. Несмотря на то что для доступа к этим устройствам все равно можно использовать /dev/stdin, /dev/stdout и /dev/stderr или дескрипторы соответствующих файлов, все же лучше, безопаснее и легче придерживаться варианта с os.Stdin, os.Stdout и os.Stderr, предлагаемого Go.

Вывод результатов

Подобно C в UNIX, Go предлагает различные способы вывода результатов на экран. Все функции вывода, использованные в этом разделе, требуют подключения стандартного Go-пакета fmt. Функции будут продемонстрированы в программе printing.go, которую мы разделим на две части.

Самый простой способ вывести что-нибудь в Go — использовать функцию fmt.Println() или fmt.Printf(). У функции fmt.Printf() много общего с C-функцией printf(3). Вместо fmt.Print() также можно использовать fmt.Println(). Основное различие между функциями fmt.Print() и fmt.Println() заключается в том, что последняя при каждом вызове автоматически добавляет в конец вывода символ новой строки.

В то же время наибольшим различием между fmt.Println() и fmt.Printf() явля­ется то, что в fmt.Printf() нужно указывать спецификатор формата для всего, что вы хотите вывести на экран, точно так же, как в C-функции printf(3). С одной стороны, это означает, что вы лучше контролируете то, что делаете, но с другой — приходится писать больше кода. В Go эти спецификаторы формата называются глаголами. Подробнее о глаголах читайте на странице https://golang.org/pkg/fmt/.

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

Первая часть printing.go содержит следующий Go-код:

package main

 

import (

    "fmt"

)

 

func main() {

    v1 := "123"

    v2 := 123

    v3 := "Have a nice day\n"

    v4 := "abc"

В этой части мы видим импорт пакета fmt и определение четырех Go-переменных. Символ \n, использованный в v3, является символом разрыва строки. Однако если вы просто хотите вставить разрыв строки в выводимые данные, то вместо чего-то вроде fmt.Print("\n") можете просто вызвать fmt.Println() без аргументов.

Вторая часть printing.go выглядит так:

    fmt.Print(v1, v2, v3, v4)

    fmt.Println()

    fmt.Println(v1, v2, v3, v4)

    fmt.Print(v1, " ", v2, " ", v3, " ", v4, "\n")

    fmt.Printf("%s%d %s %s\n", v1, v2, v3, v4)

}

В этой части выводятся четыре переменные с использованием функций fmt.Println(), fmt.Print() и fmt.Printf(), чтобы лучше понять, чем эти функции различаются между собой.

Если выполнить print.go, то получим следующий вывод:

$ go run printing.go

123123Have a nice day

abc

123 123 Have a nice day

abc

123 123 Have a nice day

abc

123123 Have a nice day

abc

Как видим, функция fmt.Println(), в отличие от fmt.Print(), также добавляет пробел между выводимыми параметрами.

В результате вызов наподобие fmt.Println(v1,v2) эквивалентен fmt.Print(v1,"",v2,"\n").

Кроме fmt.Println(), fmt.Print() и fmt.Printf(), которые являются простейшими функциями для вывода данных на экран, существует также семейство S-функций, в которое входят функции fmt.Sprintln(), fmt.Sprint() и fmt.Sprintf(). Они используются для построения строк по заданному формату.

Наконец, существует семейство F-функций, в которое входят функции fmt.Fprintln(), fmt.Fprint() и fmt.Fprintf(). Они используются для записи в файлы с помощью io.Writer.

 

Подробнее об интерфейсах io.Writer и io.Reader вы прочитаете в главе 8.

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

Использование стандартного потока вывода

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

Рассмотрим соответствующий метод в программе stdOUT.go, которую разделим на три части. Первая часть программы выглядит так:

package main

 

import (

    "io"

    "os"

)

Итак, в stdOUT.go вместо пакета fmt будет использоваться пакет io. Пакет os применяется для чтения программой аргументов командной строки и для доступа к os.Stdout.

Вторая часть stdOUT.go содержит следующий Go-код:

func main() {

    myString := ""

    arguments := os.Args

    if len(arguments) == 1 {

        myString = "Please give me one argument!"

    } else {

        myString = arguments[1]

    }

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

Третья часть программы выглядит следующим образом:

    io.WriteString(os.Stdout, myString)

    io.WriteString(os.Stdout, "\n")

}

В данном случае функция io.WriteString() работает так же, как fmt.Print(), однако принимает только два параметра. Первый из них — это файл, в который будут записываться результаты, в данном случае os.Stdout, а второй — строковая переменная.

 

Строго говоря, первый параметр функции io.WriteString() должен иметь тип io.Writer, что требует в качестве второго параметра срез байтов. Однако в данном случае строковая переменная отлично справляется со своей задачей. Подробнее о срезах вы узнаете в главе 3.

Запуск stdOUT.go приведет к следующему результату:

$ go run stdOUT.go

Please give me one argument!

$ go run stdOUT.go 123 12

123

Как видим, функция io.WriteString() отправляет содержимое своего второго параметра на экран, если первым параметром является os.Stdout.

Получение данных от пользователя

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

Что такое := и =

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

 

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

Оператор := работает следующим образом:

m := 123

Результатом выполнения этого оператора станет создание целочисленной переменной с именем m и значением 123.

Но если вы попытаетесь использовать := для уже объявленной переменной, то компиляция завершится неудачно и выдаст следующее предельно ясное сообщение об ошибке:

$ go run test.go

# command-line-arguments

./test.go:5:4: no new variables on left side of :=

Возможно, у вас возникнет вопрос: что произойдет, если мы ожидаем от функции два значения или более и хотим использовать существующую переменную для одного из них? Какой оператор применять: := или =? Ответ прост: оператор :=, как в следующем примере кода:

i, k := 3, 4

j, k := 1, 2

Поскольку переменная j впервые встречается во втором выражении, то мы использовали :=, даже если в первом выражении значение k уже определено.

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

Чтение стандартного потока ввода

Чтение данных из стандартного потока ввода рассмотрим на примере программы stdIN.go, которую разделим на две части. Первая часть выглядит так:

package main

 

import (

    "bufio"

    "fmt"

    "os"

)

В данном коде нам впервые в этой книге встретился пакет bufio.

 

Подробнее о пакете bufio вы узнаете в главе 8.

Обычно для ввода и вывода файлов используется пакет bufio, но в этой книге мы будем использовать в основном пакет os, так как в нем содержится много удобных функций; одно из самых востребованных свойств этого пакета — то, что он предоставляет доступ к аргументам командной строки Go-программы (os.Args).

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

Вторая часть stdIN.go содержит следующий Go-код:

func main() {

    var f *os.File

    f = os.Stdin

    defer f.Close()

 

    scanner := bufio.NewScanner(f)

    for scanner.Scan() {

        fmt.Println(">", scanner.Text())

    }

}

Во-первых, здесь вызывается функция bufio.NewScanner(), которой в качестве параметра передается стандартный поток ввода (os.Stdin). Этот вызов возвращает переменную bufio.Scanner, которая используется функцией Scan() для построчного чтения из os.Stdin. Каждая прочитанная строка выводится на экран, после чего считывается следующая строка. Обратите внимание, что каждая строка, которую выводит программа, начинается с символа >.

Запуск stdIN.go даст нам следующий вывод:

$ go run stdIN.go

This is number 21

> This is number 21

This is Mihalis

> This is Mihalis

Hello Go!

> Hello Go!

Press Control + D on a new line to end this program!

> Press Control + D on a new line to end this program!

Как принято в UNIX, вы можете прервать чтение программой данных из стандартного потока ввода, нажав Ctrl+D.

 

Go-программы stdIN.go и stdOUT.go очень просты, однако не стоит их недооценивать: они пригодятся, когда мы будем обсуждать UNIX-каналы в главе 8.

Работа с аргументами командной строки

Способ, описанный в этом разделе, рассмотрим на примере Go-кода cla.go, который разделим на три части. Эта программа находит минимальный и максимальный из своих аргументов командной строки.

Первая часть программы выглядит так:

package main

 

import (

    "fmt"

    "os"

    "strconv"

)

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

Вторая часть программы выглядит так:

func main() {

    if len(os.Args) == 1 {

        fmt.Println("Please give one or more floats.")

        os.Exit(1)

    }

 

    arguments := os.Args

    min, _ := strconv.ParseFloat(arguments[1], 64)

    max, _ := strconv.ParseFloat(arguments[1], 64)

Здесь cla.go проверяет, есть ли у нас аргументы командной строки, контролируя длину os.Args. Дело в том, что для работы программы необходим как минимум один аргумент командной строки. Обратите внимание, что os.Args — это срез Go, содержащий значения типа string. Первый элемент среза — это имя исполняемой программы. Поэтому, чтобы инициализировать переменные min и max, нужно использовать второй элемент среза os.Args, имеющий тип string, индекс которого равен 1.

Важный момент: если вы ожидаете одно или несколько значений с плавающей точкой, это вовсе не значит, что пользователь предоставит вам именно корректные значения с плавающей точкой. Он может этого и не сделать — случайно или преднамеренно. Однако, поскольку мы пока еще не обсуждали обработку ошибок в Go, cla.go предполагает, что все аргументы командной строки имеют правильный формат и, следовательно, будут приемлемыми. Поэтому cla.go игнорирует значение типа error, возвращаемое функцией strconv.ParseFloat(), используя следующую инструкцию:

n, _ := strconv.ParseFloat(arguments[i], 64)

Предыдущий оператор говорит Go, что мы хотим получить только первое значение, возвращаемое strconv.ParseFloat(), и что нас не интересует второе значение, которое в данном случае является переменной типа error, — мы присваиваем это значение символу подчеркивания. Символ подчеркивания, который называют пустым идентификатором, является средством сброса значения в Go. Если Go-функция возвращает несколько значений, пустой идентификатор можно использовать несколько раз.

 

Игнорирование всех или некоторых значений, возвращаемых Go-функцией, особенно значений типа error, — очень опасный метод, который не следует использовать в продуктивном коде!

Третья часть содержит следующий Go-код:

    for i := 2; i < len(arguments); i++ {

        n, _ := strconv.ParseFloat(arguments[i], 64)

    

        if n < min {

            min = n

        }

        if n > max {

            max = n

        }

    }

 

    fmt.Println("Min:", min)

    fmt.Println("Max:", max)

}

Здесь используется цикл for. Он позволяет перебрать все элементы среза os.Args, который перед этим присвоен переменной arguments.

Выполнение cla.go приведет к следующему выводу:

$ go run cla.go -10 0 1

Min: -10

Max: 1

$ go run cla.go -10

Min: -10

Max: -10

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

$ go run cla.go a b c 10

Min: 0

Max: 10

Вывод ошибок

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

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

Первая часть программы выглядит так:

package main

 

import (

    "io"

    "os"

)

 

func main() {

    myString := ""

    arguments := os.Args

    if len(arguments) == 1 {

        myString = "Please give me one argument!"

    } else {

        myString = arguments[1]

    }

До сих пор stdERR.go практически не отличается от stdOUT.go.

Вторая часть stdERR.go выглядит так:

    io.WriteString(os.Stdout, "This is Standard output\n")

    io.WriteString(os.Stderr, myString)

    io.WriteString(os.Stderr, "\n")

}

Функция io.WriteString() вызывается два раза для записи в стандартный поток ошибок (os.Stderr) и еще один раз — для записи в стандартный поток вывода (os.Stdout).

Выполнение stdERR.go даст следующий результат:

$ go run stdERR.go

This is Standard output

Please give me one argument!

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

При применении bash(1) можно перенаправить стандартный поток ошибок в файл:

$ go run stdERR.go 2>/tmp/stdError

This is Standard output

$ cat /tmp/stdError

Please give me one argument!

 

Число, стоящее после имени UNIX-программы или после системного вызова, соответствует номеру раздела руководства, к которому относится страница этой программы или вызова. Большинство имен встречается только на одной странице справочника, следовательно, указывать номер раздела не обязательно. Однако есть имена, которые встречаются в нескольких разделах, поскольку имеют несколько значений, такие как crontab(1) и crontab(5). Поэтому если вы попытаетесь получить справочную страницу имени с несколькими значениями, не указав номер раздела, то получите запись с наименьшим номером раздела.

Подобным образом можно отменить вывод ошибок, перенаправив их на устройство /dev/null, что соответствует указанию UNIX полностью игнорировать этот вывод:

$ go run stdERR.go 2>/dev/null

This is Standard output

В двух примерах мы перенаправили файловый дескриптор стандартного потока ошибок в определенный файл и в /dev/null соответственно. Если вы хотите сохранить данные стандартного потока вывода и стандартного потока ошибок в одном файле, то можете перенаправить файловый дескриптор стандартного потока ошибок (2) в файловый дескриптор стандартного потока вывода (1). В следующей команде показан довольно распространенный в UNIX-системах способ:

$ go run stdERR.go >/tmp/output 2>&1

$ cat /tmp/output

This is Standard output

Please give me one argument!

Наконец, можно перенаправить и стандартный поток вывода, и стандартный поток ошибок на /dev/null:

$ go run stdERR.go >/dev/null 2>&1

Запись в журнальные файлы

Пакет log позволяет отправлять журнальные сообщения в системную службу журналирования UNIX-машины, а Go-пакет syslog, который является частью пакета log, позволяет определить уровень журналирования и средство журналирования, которое будет использовать ваша Go-программа.

Обычно большинство системных журнальных файлов в UNIX размещаются в каталоге /var/log. Однако журнальные файлы многих популярных сервисов, таких как Apache и Nginx, находятся в других местах, в зависимости от их конфигурации.

Вообще, использование журнальных файлов для записи некоторой информации считается более эффективной практикой, чем вывод тех же данных на экран, по двум причинам: во-первых, потому, что выходные данные, сохраняемые в файле, не теряются, а во-вторых, потому, что вы можете вести поиск и обрабатывать файлы журналов с помощью инструментов UNIX, таких как grep(1), awk(1) и sed(1); если же сообщения выводятся в окне терминала, это невозможно.

Пакет log предлагает множество функций для перенаправления вывода в системную службу журналирования UNIX-машины. В число этих функций входят log.Printf(), log.Print(), log.Println(), log.Fatalf(), log.Fatalln(), log.Panic(), log.Panicln() и log.Panicf().

 

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

Уровни журналирования

Уровень журналирования — это значение, которое определяет степень серьезности журнального сообщения. Существуют следующие уровни журналирования (в порядке возрастания серьезности): debug, info, notice, warning, err, crit, alert и emerg.

Средства журналирования

Средство журналирования подобно категории, используемой при регистрации информации. Средство журналирования принимает значения 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-машине.

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

Серверы журналирования

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

На машинах с macOS процесс ведения журнала называется syslogd(8). На большинстве машин с Linux, напротив, используется rsyslogd(8) — улучшенная и более надежная версия syslogd(8), которая была первой системной утилитой UNIX для регистрации журнальных сообщений.

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

Файл конфигурации rsyslogd(8) обычно называется rsyslog.conf и находится в /etc. Содержимое файла конфигурации rsyslog.conf, без учета строк с комментариями и строк, начинающихся с $, может выглядеть следующим образом:

$ grep -v '^#' /etc/rsyslog.conf | grep -v '^$' | grep -v '^\$'

auth,authpriv.*               /var/log/auth.log

*.*;auth,authpriv.none        -/var/log/syslog

daemon.*                      -/var/log/daemon.log

kern.*                        -/var/log/kern.log

lpr.*                         -/var/log/lpr.log

mail.*                        -/var/log/mail.log

user.*                        -/var/log/user.log

mail.info                     -/var/log/mail.info

mail.warn                     -/var/log/mail.warn

mail.err                      /var/log/mail.err

news.crit                     /var/log/news/news.crit

news.err                      /var/log/news/news.err

news.notice                   -/var/log/news/news.notice

*.=debug;\

    auth,authpriv.none;\

    news.none;mail.none       -/var/log/debug

*.=info;*.=notice;*.=warn;\

    auth,authpriv.none;\

    cron,daemon.none;\

    mail,news.none            -/var/log/messages

*.emerg                  :omusrmsg:*

daemon.*;mail.*;\

    news.err;\

    *.=debug;*.=info;\

    *.=notice;*.=warn        |/dev/xconsole

local7.* /var/log/cisco.log

Таким образом, для того чтобы передать журнальную информацию в /var/log/cisco.log, нужно использовать средство журналирования local7. Символ звездочки после имени средства журналирования дает серверу журналирования указание перехватывать все уровни журналирования, которые поступают в средство журналирования local7, и записывать их в /var/log/cisco.log.

Сервер syslogd(8) имеет довольно похожий файл конфигурации, обычно это /etc/syslog.conf. В macOS High Sierra файл /etc/syslog.conf практически пуст и заменен на /etc/asl.conf. Тем не менее, логика конфигурации у /etc/syslog.conf, /etc/rsyslog.conf и /etc/asl.conf одинакова.

Пример Go-программы, которая записывает информацию в журнальные файлы

Рассмотрим использование пакетов log и log/syslog для записи в файлы системного журнала на примере Go-кода, представленного в файле logFiles.go.

 

Обратите внимание: пакет log/syslog не реализован в версии Go для Microsoft Windows.

Первая часть logFiles.go выглядит так:

package main

 

import (

    "fmt"

    "log"

    "log/syslog"

    "os"

    "path/filepath"

)

 

func main() {

    programName := filepath.Base(os.Args[0])

    sysLog, err := syslog.New(syslog.LOG_INFO|syslog.LOG_LOCAL7, programName)

Первым параметром функции syslog.New() является приоритет, который представляет собой соединение средства журналирования и уровня журналирования. Так, приоритет LOG_NOTICE|LOG_MAIL, который выбран в качестве примера, будет отправлять сообщения уровня журналирования NOTICE в средство журналирования MAIL.

В результате этот код устанавливает такой режим журналирования по умолчанию: средство журналирования local7 и уровень журналирования info. Второй параметр функции syslog.New() — это имя процесса, который будет отображаться в журналах в качестве отправителя сообщения. Вообще, рекомендуется использовать настоящее имя исполняемого файла, чтобы впоследствии можно было легко найти нужную информацию в файлах журналов.

Вторая часть программы содержит следующий Go-код:

    if err != nil {

        log.Fatal(err)

    } else {

        log.SetOutput(sysLog)

    }

    log.Println("LOG_INFO + LOG_LOCAL7: Logging in Go!")

После вызова функции syslog.New() нужно проверить переменную типа error, которая возвращается этой функцией, чтобы убедиться, что все в порядке. Если это так, тогда значение переменной типа error равно nil и можно вызвать функцию log.SetOutput(), которая задает приемник вывода для записи в журнал по умолчанию, — в данном случае это созданный нами ранее регистратор журнала (sysLog). Затем можно использовать функцию log.Println() для отправки информации на сервер журнала.

Третья часть logFiles.go содержит следующий код:

    sysLog, err = syslog.New(syslog.LOG_MAIL, "Some program!")

    if err != nil {

        log.Fatal(err)

    } else {

        log.SetOutput(sysLog)

    }

 

    log.Println("LOG_MAIL: Logging in Go!")

    fmt.Println("Will you see this?")

}

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

При выполнении logFiles.go на экран компьютера с Debian Linux будут выведены следующие данные:

$ go run logFiles.go

Broadcast message from systemd-journald@mail (Tue 2017-10-17 20:06:08 EEST):

logFiles[23688]: Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL:

Logging in Go!

Message from syslogd@mail at Oct 17 20:06:08 ...

Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL: Logging in Go!

Will you see this?

Выполнение того же Go-кода на компьютере с macOS High Sierra приведет к следующим результатам:

$ go run logFiles.go

Will you see this?

Имейте в виду, что на большинстве UNIX-машин информация журналов хранится в нескольких файлах. Это также относится и к машине с Debian Linux, использованной в этом разделе. В результате logFiles.go отправляет выходные данные в несколько файлов журнала, в чем можно убедиться с помощью следу­ющих команд оболочки:

$ grep LOG_MAIL /var/log/mail.log

Oct 17 20:06:08 mail Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL:

Logging in Go!

$ grep LOG_LOCAL7 /var/log/cisco.log

Oct 17 20:06:08 mail logFiles[23688]: 2017/10/17 20:06:08 LOG_INFO +

LOG_LOCAL7: Logging in Go!

$ grep LOG_ /var/log/syslog

Oct 17 20:06:08 mail logFiles[23688]: 2017/10/17 20:06:08 LOG_INFO +

LOG_LOCAL7: Logging in Go!

Oct 17 20:06:08 mail Some program![23688]: 2017/10/17 20:06:08 LOG_MAIL:

Logging in Go!

Как видно из результатов, сообщение оператора log.Println("LOG_INFO+LOG_LOCAL7:LogginginGo!") записано в два файла: /var/log/cisco.log и /var/log/syslog, тогда как сообщение оператора log.Println("LOG_MAIL:LogginginGo!") попало в /var/log/syslog и /var/log/mail.log.

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

Функция log.Fatal()

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

Использование log.Fatal() будет проиллюстрировано в программе logFatal.go, которая содержит следующий Go-код:

package main

 

import (

    "fmt"

    "log"

    "log/syslog"

)

 

func main() {

    sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!")

    if err != nil {

        log.Fatal(err)

    } else {

        log.SetOutput(sysLog)

    }

    log.Fatal(sysLog)

    fmt.Println("Will you see this?")

}

Выполнение logFatal.go приведет к следующему выводу:

$ go run logFatal.go

exit status 1

Легко понять, что при использовании log.Fatal() Go-программа завершается в том месте, где была вызвана функция log.Fatal(). Именно поэтому вы не увидели вывод оператора fmt.Println("Willyouseethis?").

Однако в соответствии с параметрами вызова syslog.New() при этом в файл журнала /var/log/mail.log, связанный с почтой, добавлена запись:

$ grep "Some program" /var/log/mail.log

Jan 10 21:29:34 iMac Some program![7123]: 2019/01/10 21:29:34 &{17 Some program! iMac.local {0 0} 0xc00000c220}

Функция log.Panic()

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

В такие сложные моменты одним из решений станет использование log.Panic() — разновидности функций журналирования, работа которой будет продемонстрирована в этом разделе на примере Go-кода из файла logPanic.go.

Go-код logPanic.go выглядит следующим образом:

package main

 

import (

    "fmt"

    "log"

    "log/syslog"

)

 

func main() {

    sysLog, err := syslog.New(syslog.LOG_ALERT|syslog.LOG_MAIL, "Some program!")

    if err != nil {

        log.Fatal(err)

    } else {

        log.SetOutput(sysLog)

    }

 

    log.Panic(sysLog)

    fmt.Println("Will you see this?")

}

Выполнение logPanic.go в macOS Mojave приведет к следующим результатам:

$ go run logPanic.go

panic: &{17 Some program! iMac.local

{0 0} 0xc0000b21e0}

goroutine 1 [running]:

log.Panic(0xc00004ef68, 0x1, 0x1)

    /usr/local/Cellar/go/1.11.4/libexec/src/log/log.go:326 +0xc0

main.main()

    /Users/mtsouk/Desktop/mGo2nd/Mastering-Go-Second-Edition/ch01/logPanic.go:17 +0xd6

exit status 2

Выполнение той же программы в Debian Linux с версией Go 1.3.3 приведет к следующему:

$ go run logPanic.go

panic: &{17 Some program! mail

{0 0} 0xc2080400e0}

goroutine 16 [running]:

runtime.panic(0x4ec360, 0xc208000320)

    /usr/lib/go/src/pkg/runtime/panic.c:279 +0xf5

log.Panic(0xc208055f20, 0x1, 0x1)

    /usr/lib/go/src/pkg/log/log.go:307 +0xb6

main.main()

    /home/mtsouk/Desktop/masterGo/ch/ch1/code/logPanic.go:17 +0x169

goroutine 17 [runnable]:

runtime.MHeap_Scavenger()

    /usr/lib/go/src/pkg/runtime/mheap.c:507

runtime.goexit()

    /usr/lib/go/src/pkg/runtime/proc.c:1445

goroutine 18 [runnable]:

bgsweep()

    /usr/lib/go/src/pkg/runtime/mgc0.c:1976

runtime.goexit()

    /usr/lib/go/src/pkg/runtime/proc.c:1445

goroutine 19 [runnable]:

runfinq()

    /usr/lib/go/src/pkg/runtime/mgc0.c:2606

runtime.goexit()

    /usr/lib/go/src/pkg/runtime/proc.c:1445

exit status 2

Таким образом, log.Panic() выводит дополнительную низкоуровневую информацию, которая, по идее, должна помочь разрешить сложную ситуацию, возникшую в Go-коде.

Как и log.Fatal(), функция log.Panic() добавит запись в соответствующий файл журнала и немедленно прекратит работу Go-программы.

Запись в специальный журнальный файл

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

Назовем нашу Go-утилиту customLog.go и будем записывать данные в журнальный файл /tmp/mGo.log.

Разделим Go-код для customLog.go на три части. Первая часть выглядит следующим образом:

package main

 

import (

    "fmt"

    "log"

    "os"

)

 

var LOGFILE = "/tmp/mGo.log"

Путь к журнальному файлу в customLog.go задан жестко как значение глобальной переменной с именем LOGFILE. Для наших целей в примере журнальный файл находится в каталоге /tmp, что не является обычным местом для хранения данных, поскольку, как правило, каталог /tmp очищается после каждой перезагрузки системы. Однако на данном этапе это избавит нас от необходимости запускать customLog.go с полномочиями пользователя root и не потребует размещать лишние файлы в системных каталогах. Если позже вы решите использовать код customLog.go в реальном приложении, вам следует изменить путь на более правильный.

Вторая часть customLog.go выглядит следующим образом:

func main() {

    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()

Здесь мы создаем новый журнальный файл, используя функцию os.OpenFile() с необходимыми правами доступа к UNIX-файлам (0644).

Последняя часть customLog.go выглядит следующим образом:

    iLog := log.New(f, "customLogLineNumber ", log.LstdFlags)

 

    iLog.SetFlags(log.LstdFlags)

    iLog.Println("Hello there!")

    iLog.Println("Another log entry!")

}

Если вы посмотрите на страницу документации пакета log, которую можно найти по адресу https://golang.org/pkg/log/, то увидите, что функция SetFlags позволяет устанавливать выходные флаги (варианты) для текущего средства журналирования. По умолчанию функция предлагает значения LstdFlags: Ldate и Ltime, то есть в каждой записи журнала, которая записывается в журнальный файл, будут указаны текущая дата и время.

При выполнении customLog.go не будет генерироваться вывод на экран. Одна­ко если дважды выполнить эту программу, получим следующее содержимое файла /tmp/mGo.log:

$ go run customLog.go

$ cat /tmp/mGo.log

customLog 2019/01/10 18:16:09 Hello there!

CustomLog 2019/01/10 18:16:09 Another log entry!

$ go run customLog.go

$ cat /tmp/mGo.log

customLog 2019/01/10 18:16:09 Hello there!

CustomLog 2019/01/10 18:16:09 Another log entry!

CustomLog 2019/01/10 18:16:17 Hello there!

CustomLog 2019/01/10 18:16:17 Another log entry!

Вывод номеров строк в записях журнала

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

package main

 

import (

    "fmt"

    "log"

    "os"

)

 

var LOGFILE = "/tmp/mGo.log"

 

func main() {

    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()

Как видим, здесь пока нет особых отличий от кода customLog.go.

Остальной Go-код из файла customLogLineNumber.go выглядит так:

    iLog := log.New(f, "customLogLineNumber ", log.LstdFlags)

    iLog.SetFlags(log.LstdFlags | log.Lshortfile)

    iLog.Println("Hello there!")

    iLog.Println("Another log entry!")

}

Всю магию выполняет оператор iLog.SetFlags(log.LstdFlags|log.Lshortfile), который, кроме log.LstdFlags, также активизирует флаг log.Lshortfile. Последний добавляет в строку записи журнала полное имя файла, а также номер строки Go-оператора, который создал эту запись.

Выполнение customLogLineNumber.go не выводит данные на экран. Но после двух запусков файла customLogLineNumber.go содержимое файла журнала /tmp/mGo.log будет примерно таким:

$ go run customLogLineNumber.go

$ cat /tmp/mGo.log

customLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:26: Hello there!

CustomLogLineNumber 2019/01/10 18:25:14 customLogLineNumber.go:27: Another log entry!

CustomLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:26: Hello there!

CustomLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:27: Another log entry!

CustomLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:26: Hello there!

CustomLogLineNumber 2019/01/10 18:25:23 customLogLineNumber.go:27: Another log entry!

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

 

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

Обработка ошибок в Go

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

Скорее всего, при разработке Go-пакетов вам придется создавать и обрабатывать собственные сообщения об ошибках.

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

 

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

Тип данных error

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

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

Пример Go-кода для иллюстрации этого процесса представлен в файле newError.go. Разделим его на две части. Первая часть программы выглядит так:

package main

 

import (

    "errors"

    "fmt"

)

 

func returnError(a, b int) error {

    if a == b {

        err := errors.New("Error in returnError() function!")

        return err

    } else {

        return nil

    }

}

Здесь происходит много интересного. Прежде всего, впервые в этой книге встречается определение Go-функции, отличной от main(). Имя этой новой функции — returnError(). Кроме того, здесь показано, как работает функция errors.New(), которая принимает в качестве параметра значение типа string. Наконец, если функция должна вернуть переменную типа error, но ошибка не произошла, то возвращается nil.

 

Подробнее о типах Go-функций вы узнаете в главе 6.

Вторая часть newError.go выглядит так:

func main() {

    err := returnError(1, 2)

    if err == nil {

        fmt.Println("returnError() ended normally!")

    } else {

        fmt.Println(err)

    }

 

    err = returnError(10, 10)

    if err == nil {

        fmt.Println("returnError() ended normally!")

    } else {

        fmt.Println(err)

    }

 

    if err.Error() == "Error in returnError() function!" {

        fmt.Println("!!")

    }

}

Как видно из кода, большая часть времени уходит на проверки того, равна ли переменная типа error со значением nil, после чего можно действовать соответственно. Здесь также продемонстрировано использование функции errors.Error(), которая позволяет преобразовать переменную типа error в тип string. Эта функция позволяет сравнивать переменную типа error с переменной типа string.

 

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

Выполнение newError.go приведет к следующим результатам:

$ go run newError.go

returnError() ended normally!

Error in returnError() function!

!!

Если вы попытаетесь сравнить переменную типа error с переменной типа string без предварительного преобразования переменной типа error в string, компилятор Go выдаст следующее сообщение об ошибке:

# command-line-arguments

./newError.go:33:9: invalid operation: err == "Error in returnError()

function!" (mismatched types error and string)

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

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

if err != nil {

    fmt.Println(err)

    os.Exit(10)

}

 

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

Представленный пример кода выводит сгенерированное сообщение об ошибке на экран и выходит из программы посредством функции os.Exit(). Обратите внимание: чтобы выйти из программы, можно также поставить ключевое слово return внутри функции main(). Вообще, вызов os.Exit() из функции, если только это не main(), считается плохой практикой. Функции, отличные от main(), как правило, перед выходом возвращают сообщение об ошибке, которое обрабатывается вызывающей функцией.

Если вы хотите отправить сообщение об ошибке не на экран, а в службу журналирования, нужно изменить предыдущий Go-код следующим образом:

if err != nil {

    log.Println(err)

    os.Exit(10)

}

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

if err != nil {

    panic(err)

    os.Exit(10)

}

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

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

А теперь пора познакомиться с Go-программой, которая не только обрабатывает сообщения об ошибках, генерируемые стандартными Go-функциями, но и создает собственное сообщение об ошибке. Эта программа называется errors.go. Разделим ее на пять частей. Как вы увидите, утилита errors.go пытается улучшить функциональность программы cla.go, о которой уже говорилось в данной главе. Теперь мы добавим проверку того, соответствуют ли ее аргументы командной строки формату чисел с плавающей точкой.

Первая часть программы выглядит так:

package main

 

import (

    "errors"

    "fmt"

    "os"

    "strconv"

)

Эта часть error.go содержит ожидаемые операторы import.

Вторая часть error.go содержит следующий Go-код:

func main() {

    if len(os.Args) == 1 {

        fmt.Println("Please give one or more floats.")

        os.Exit(1)

    }

 

    arguments := os.Args

    var err error = errors.New("An error")

    k := 1

    var n float64

Здесь создается новая переменная типа error с именем err, чтобы инициализировать ее собственным значением.

Третья часть программы выглядит так:

    for err != nil {

        if k >= len(arguments) {

            fmt.Println("None of the arguments is a float!")

            return

        }

        n, err = strconv.ParseFloat(arguments[k], 64)

        k++

    }

 

    min, max := n, n

Это самая сложная часть программы: если первый аргумент командной строки не является корректным числом с плавающей точкой, нужно проверить следующий аргумент и продолжать проверку до тех пор, пока не встретится аргумент командной строки подходящего формата. Если ни один из аргументов командной строки не соответствует формату числа с плавающей точкой, error.go прекращает работу и выводит на экран сообщение об ошибке. Вся проверка выполняется путем исследования значения типа error, которое возвращается функцией strconv.ParseFloat(). Весь этот код предназначен лишь для точной инициализации переменных min и max.

Четвертая часть программы содержит следующий Go-код:

    for i := 2; i < len(arguments); i++ {

        n, err := strconv.ParseFloat(arguments[i], 64)

        if err == nil {

            if n < min {

                min = n

            }

            if n > max {

                max = n

            }

        }

    }

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

Наконец, последняя часть кода программы выводит на экран текущие значения переменных min и max:

    fmt.Println("Min:", min)

    fmt.Println("Max:", max)

}

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

Если выполнить error.go, то получим следующий вывод:

$ go run errors.go a b c

None of the arguments is a float!

$ go run errors.go b c 1 2 3 c -1 100 -200 a

Min: -200

Max: 100

Использование Docker

В последнем разделе этой главы вы узнаете, как можно использовать образ Docker для компиляции и выполнения Go-кода внутри образа Docker.

Возможно, вы знаете, что в Docker все начинается с образа Docker; вы можете создать собственный образ Docker с нуля или начать с уже существующего. В этом разделе мы загрузим базовый образ Docker с Docker Hub и продолжим сборку Go-программы HelloWorld! внутри этого образа.

Содержимое Dockerfile будет использоваться следующим образом:

FROM golang:alpine

 

RUN mkdir /files

COPY hw.go /files

WORKDIR /files

RUN go build -o /files/hw hw.go

ENTRYPOINT ["/files/hw"]

Здесь в первой строке определяется образ Docker, который будет использоваться. Остальные три команды создают в образе Docker новый каталог, копируют в образ Docker файл (hw.go) из текущего пользовательского каталога и соответственно изменяют текущий рабочий каталог образа Docker. Последние две команды создают двоичный исполняемый файл из исходного Go-файла и задают путь к этому двоичному файлу, который будет выполняться при запуске образа Docker.

Итак, как использовать этот Dockerfile? Если файл с именем hw.go существует и находится в текущем рабочем каталоге, можно создать новый образ Docker следующим образом:

$ docker build -t go_hw:v1 .

Sending build context to Docker daemon 2.237MB

Step 1/6 : FROM golang:alpine

alpine: Pulling from library/golang

cd784148e348: Pull complete

7e273b0dfc44: Pull complete

952c3806fd1a: Pull complete

ee1f873f86f9: Pull complete

7172cd197d12: Pull complete

Digest:

sha256:198cb8c94b9ee6941ce6d58f29aadb855f64600918ce602cdeacb018ad77d647

Status: Downloaded newer image for golang:alpine

---> f56365ec0638

Step 2/6 : RUN mkdir /files

---> Running in 18fa7784d82c

Removing intermediate container 18fa7784d82c

---> 9360e95d7cb4

Step 3/6 : COPY hw.go /files

---> 680517bc4aa3

Step 4/6 : WORKDIR /files

---> Running in f3f678fcc38d

Removing intermediate container f3f678fcc38d

---> 640117aea82f

Step 5/6 : RUN go build -o /files/hw hw.go

---> Running in 271cae1fa7f9

Removing intermediate container 271cae1fa7f9

---> dc7852b6aeeb

Step 6/6 : ENTRYPOINT ["/files/hw"]

---> Running in cdadf286f025

Removing intermediate container cdadf286f025

---> 9bec016712c4

Successfully built 9bec016712c4

Successfully tagged go_hw:v1

Имя нового образа Docker — go_hw:v1.

Если на вашем компьютере уже есть образ Docker golang:alpine, то результат этой команды будет таким:

$ docker build -t go_hw:v1 .

Sending build context to Docker daemon 2.237MB

Step 1/6 : FROM golang:alpine

---> f56365ec0638

Step 2/6 : RUN mkdir /files

---> Running in 982e6883bb13

Removing intermediate container 982e6883bb13

---> 0632577d852c

Step 3/6 : COPY hw.go /files

---> 68a0feb2e7dc

Step 4/6 : WORKDIR /files

---> Running in d7d4d0c846c2

Removing intermediate container d7d4d0c846c2

---> 6597a7cb3882

Step 5/6 : RUN go build -o /files/hw hw.go

---> Running in 324400d532e0

Removing intermediate container 324400d532e0

---> 5496dd3d09d1

Step 6/6 : ENTRYPOINT ["/files/hw"]

---> Running in bbd24840d6d4

    Removing intermediate container bbd24840d6d4

---> 5a0d2473aa96

    Successfully built 5a0d2473aa96

    Successfully tagged go_hw:v1

Чтобы убедиться, что образ Docker go_hw:v1 действительно существует на вашем компьютере, нужно сделать следующее:

$ docker images

REPOSITORY   TAG       IMAGE ID            CREATED                SIZE

go_hw        v1        9bec016712c4        About a minute ago     312MB

golang       alpine    f56365ec0638        11 days ago            310MB

В файле hw.go содержится следующий код:

package main

 

import (

    "fmt"

)

 

func main() {

    fmt.Println("Hello World!")

}

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

$ docker run go_hw:v1

Hello World!

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

При желании можно передать (push) образ Docker в реестр Docker, размещенный в Интернете, чтобы впоследствии иметь возможность извлекать его оттуда (pull).

Таким местом может быть Docker Hub — при условии, что у вас есть учетная запись Docker Hub, создать которую легко, и это бесплатно. Итак, для того чтобы отправить созданный нами образ в Docker Hub, нужно создать в Docker Hub учетную запись, затем выполнить на вашей UNIX-машине следующие команды:

$ docker login

Authenticating with existing credentials...

Login Succeeded

$ docker tag go_hw:v1 "mactsouk/go_hw:v1"

$ docker push "mactsouk/go_hw:v1"

The push refers to repository [docker.io/mactsouk/go_hw]

bdb6946938e3: Pushed

99e21c42e35d: Pushed

0257968d27b2: Pushed

e121936484eb: Pushed

61b145086eb8: Pushed

789935042c6f: Pushed

b14874cfef59: Pushed

7bff100f35cb: Pushed

v1: digest:

sha256:c179d5d48a51b74b0883e582d53bf861c6884743eb51d9b77855949b5d91dd

e1 size: 1988

Первая команда необходима для входа в Docker Hub, ее достаточно выполнить один раз. Команда dockertag нужна для указания имени, под которым локальный образ будет храниться в Docker Hub. Эту команду нужно выполнить перед командой pushdocker. Последняя команда отправляет желаемый образ Docker в Docker Hub и генерирует расширенное сообщение о результатах. Если вы сделаете свой образ Docker общедоступным, то любой желающий сможет получить его из Docker Hub и использовать.

Есть несколько способов удалить один или несколько образов Docker с локальной UNIX-машины. Один из них — воспользоваться IMAGEID образа Docker:

$ docker rmi 5a0d2473aa96 f56365ec0638

Untagged: go_hw:v1

Deleted:

sha256:5a0d2473aa96bcdafbef92751a0e1c1bf146848966c8c971f462eb1eb242d2a6

Deleted:

sha256:5496dd3d09d13c63bf7a9ac52b90bb812690cdfd33cfc3340509f9bfe6215c48

Deleted:

sha256:598c4e474b123eccb84f41620d2568665b88a8f176a21342030917576b9d82a8

Deleted:

sha256:6597a7cb3882b73855d12111787bd956a9ec3abb11d9915d32f2bba4d0e92ec6

Deleted:

sha256:68a0feb2e7dc5a139eaa7ca04e54c20e34b7d06df30bcd4934ad6511361f2cb8

Deleted:

sha256:c04452ea9f45d85a999bdc54b55ca75b6b196320c021d777ec1f766d115aa514

Deleted:

sha256:0632577d852c4f9b66c0eff2481ba06c49437e447761d655073eb034fa0ac333

Deleted:

sha256:52efd0fa2950c8f3c3e2e44fbc4eb076c92c0f85fff46a07e060f5974c1007a9

Untagged: golang:alpine

Untagged:

golang@sha256:198cb8c94b9ee6941ce6d58f29aadb855f64600918ce602cdeacb018ad77d647

Deleted:

sha256:f56365ec0638b16b752af4bf17e6098f2fda027f8a71886d6849342266cc3ab7

Deleted:

sha256:d6a4b196ed79e7ff124b547431f77e92dce9650037e76da294b3b3aded709bdd

Deleted:

sha256:f509ec77b9b2390c745afd76cd8dd86977c86e9ff377d5663b42b664357c3522

Deleted:

sha256:1ee98fa99e925362ef980e651c5a685ad04cef41dd80df9be59f158cf9e52951

Deleted:

sha256:78c8e55f8cb4c661582af874153f88c2587a034ee32d21cb57ac1fef51c6109e

Deleted:

sha256:7bff100f35cb359a368537bb07829b055fe8e0b1cb01085a3a628ae9c187c7b8

 

Работа с Docker — огромная и действительно важная тема, к которой мы вернемся еще не раз.

Упражнения и ссылки

• Посетите сайт Go: https://golang.org/.

• Посетите сайт Docker: https://www.docker.com/.

• Посетите сайт Docker Hub: https://hub.docker.com/.

• Посетите Go 2 Draft Designs: https://blog.golang.org/go2draft.

• Посетите сайт документации по Go: https://golang.org/doc/.

• Почитайте документацию пакета log: https://golang.org/pkg/log/.

• Почитайте документацию пакета log/syslog: https://golang.org/pkg/log/syslog/.

• Почитайте документацию пакета os: https://golang.org/pkg/os/.

• Посетите https://golang.org/cmd/gofmt/ — страницу документации инструмента gofmt, который используется для форматирования Go-кода.

• Напишите Go-программу, которая вычисляет сумму всех аргументов командной строки, которые являются действительными числами.

• Напишите Go-программу, вычисляющую среднее значение всех чисел с плавающей запятой, переданных программе в качестве аргументов командной строки.

• Напишите Go-программу, которая считывает целые числа до тех пор, пока не встретит во входных данных слово END.

• Можете ли вы изменить программу customLog.go так, чтобы данные журнала записывались одновременно в два файла журнала? Возможно, для этого вам придется обратиться к главе 8.

• Если вы работаете на компьютере Mac, почитайте описание редактора TextMate по адресу http://macromates.com/, а также редактора BBEdit по адресу https://www.barebones.com/products/bbedit/.

• Посетите страницу документации пакета fmt: https://golang.org/pkg/fmt/, чтобы больше узнать о «глаголах» форматирования и доступных функциях.

Приглашаю вас посетить страницу https://blog.Golang.Org/why-generics, чтобы больше узнать о Go и Generics.

Резюме

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

Следующая глава посвящена деталям внутренней реализации Go, в том числе сборке мусора, компилятору Go, вызову C-кода из Go, ключевому слову defer, Go-ассемблеру и WebAssembly, а также функциям panic и recover.

1 Если godoc на вашем компьютере не установлена, просто выполните такую команду:

$ go get golang.org/x/tools/cmd/godoc. — Здесь и далее примеч. науч. ред.

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

Если godoc на вашем компьютере не установлена, просто выполните такую команду:

2. Go изнутри

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

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

Кроме того, вы узнаете, как вызывать код на Go из программ на C, научитесь использовать функции panic() и recovery() и ключевое слово defer.

В этой главе рассмотрим следующие темы:

компилятор Go;

• как работает сборка мусора в Go;

• как проверить работу сборщика мусора;

• вызов кода на C из программы на Go;

• вызов кода на Go из программы на C;

• функции panic() и recovery();

• пакет unsafe;

• удобное, но коварное ключевое слово defer;

• Linux-утилита strace(1);

• утилита dtrace(1) из систем FreeBSD, включая macOS Mojave;

• где найти информацию о вашей среде Go;

• построение деревьев узлов (node trees) с помощью Go;

• генерация кода WebAssembly из Go;

Go-ассемблер.

Компилятор Go

Компилятор Go запускается с помощью инструмента go. Этот инструмент делает гораздо больше, чем просто создает исполняемые файлы.

 

Используемый в этом разделе файл unsafe.go не содержит никакого особенного кода — представленные здесь команды будут работать с любым корректным исходным файлом Go.

Для того чтобы скомпилировать исходный файл Go, нужно воспользоваться командой2gotoolcompile. В результате вы получите объектный файл — файл с расширением .o. Пример компиляции показан ниже, в виде следующих команд, выполненных на компьютере с macOS Mojave:

$ go tool compile unsafe.go

$ ls -l unsafe.o

-rw-r--r--  1 mtsouk  staff  6926 Jan 22 21:39 unsafe.o

$ file unsafe.o

unsafe.o: current ar archive

Объектный файл — это файл, в котором содержится объектный код, то есть машинный код в переносимом формате, который в большинстве случаев не может быть непосредственно выполнен. Главным преимуществом переносимого формата является то, что на этапе компоновки ему требуется минимум памяти.

Если при выполнении компиляции gotool использовать флаг командной строки -pack, то вместо объектного файла получим архивный файл:

$ go tool compile -pack unsafe.go

$ ls -l unsafe.a

-rw-r--r--  1 mtsouk  staff  6926 Jan 22 21:40 unsafe.a

$ file unsafe.a

unsafe.a: current ar archive

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

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

$ ar t unsafe.a

__.PKGDEF

_go_.o

Обратите внимание еще на один полезный флаг командной строки для команды gotoolcompile: -race. Он позволяет распознавать состояние гонки. Подробнее о том, что такое состояние гонки и почему его следует избегать, вы прочитаете в главе 10.

В конце этой главы вы найдете дополнительные примеры применения команды gotoolcompile, когда речь пойдет о языке ассемблера и деревьях узлов (node trees). Сейчас для проверки попробуйте выполнить следующую команду:

$ go tool compile -S unsafe.go

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

Сборка мусора

Сборка мусора (Garbage Collection, GC) — это процесс освобождения места в памяти, которое больше не используется. Другими словами, сборщик мусора определяет объекты, находящиеся вне области видимости, на которые нельзя больше ссылаться (недостижимые объекты), и освобождает занимаемую ими память. Этот процесс выполняется конкурентно, не до и не после, а во время работы Go-программы. Документация реализации сборщика мусора в Go гласит следующее:

«GC выполняется конкурентно (concurrent), одновременно с потоками мутатора (mutator), в точном соответствии с типом (этот принцип также известен как чувствительность к типу), допускается параллельное выполнение нескольких потоков GC. Это конкурентная пометка и очистка (mark-sweep), при которой используется барьер записи (write barrier). При этом процессе ничего не генерируется и не сжимается. Освобождение памяти выполняется на основе размера, выделенного для каждой программы P, чтобы в общем случае минимизировать фрагментацию и избежать блокировок».

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

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

Первая часть кода gColl.go выглядит следующим образом:

package main

 

import (

    "fmt"

    "runtime"

    "time"

)

 

func printStats(mem runtime.MemStats) {

    runtime.ReadMemStats(&mem)

    fmt.Println("mem.Alloc:", mem.Alloc)

    fmt.Println("mem.TotalAlloc:", mem.TotalAlloc)

    fmt.Println("mem.HeapAlloc:", mem.HeapAlloc)

    fmt.Println("mem.NumGC:", mem.NumGC)

    fmt.Println("-----")

}

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

Вторая часть программы выглядит так:

func main() {

    var mem runtime.MemStats

    printStats(mem)

 

    for i := 0; i < 10; i++ {

        s := make([]byte, 50000000)

        if s == nil {

            fmt.Println("Operation failed!")

        }

    }

    printStats(mem)

Цикл for создает много больших срезов Go, чтобы под них выделялись большие объемы памяти и запускался сборщик мусора.

Последняя часть gColl.go содержит следующий код Go, который вызывает дополнительное выделение памяти для срезов Go:

    for i := 0; i < 10; i++ {

        s := make([]byte, 100000000)

        if s == nil {

            fmt.Println("Operation failed!")

        }

        time.Sleep(5 * time.Second)

    }

    printStats(mem)

}

Выполнение gColl.go на macOS Mojave дает следующий результат:

$ go run gColl.go

mem.Alloc: 66024

mem.TotalAlloc: 66024

mem.HeapAlloc: 66024

mem.NumGC: 0

-----

mem.Alloc: 50078496

mem.TotalAlloc: 500117056

mem.HeapAlloc: 50078496

mem.NumGC: 10

-----

mem.Alloc: 76712

mem.TotalAlloc: 1500199904

mem.HeapAlloc: 76712

mem.NumGC: 20

-----

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

В следующей команде показан прием, позволяющий получить еще более по­дробный отчет о том, как работает сборщик мусора Go:

$ GODEBUG=gctrace=1 go run gColl.go

Если перед любой командой gorun поставить GODEBUG=gctrace=1, то Go выводит аналитические данные о работе сборщика мусора. Данные представлены в такой форме:

gc 4 @0.025s 0%: 0.002+0.065+0.018 ms clock,

    0.021+0.040/0.057/0.003+0.14 ms cpu, 47->47->0 MB, 48 MB goal, 8 P

gc 17 @30.103s 0%: 0.004+0.080+0.019 ms clock,

    0.033+0/0.076/0.071+0.15 ms cpu, 95->95->0 MB, 96 MB goal, 8 P

Здесь приводится подробная информация о размерах кучи в процессе сборки мусора. Возьмем для примера тройку значений 47->47->0MB. Первое число — размер кучи перед запуском сборщика мусора; второе значение — размер кучи, когда сборщик завершает работу; последнее значение — актуальный размер кучи.

Трехцветный алгоритм

В основе работы сборщика мусора Go лежит трехцветный алгоритм.

 

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

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

...