# 1. Первое знакомство с Go Go был задуман в сентябре 2007 года Робертом Грисемером, Робом Пайком и Кеном Томпсоном из Google и анонсирован в ноябре 2009 года. Сам язык и сопутствующие ему инструменты разрабатывались с учётом требований к эффективности как при компиляции исходного кода, так и при выполнении программ. Go имеет поверхностное сходство с языком программирования C, и является таким же инструментом для профессиональных программистов, позволяя достичь максимальной эффективности с минимальными затратами. Но это гораздо больше, чем просто современная версия языка программирования C. Он заимствует и приспосабливает для своих нужд хорошие идеи из многих других языков, избегая возможностей, которые могут привести к созданию сложного и ненадёжного кода. Его реализация конкурентности является простой в использовании и эффективной, а подход к абстракции данных и объектно-ориентированному программированию непривычный, но очень гибкий. В Go также есть автоматическое управление памятью и *сборщик мусора*. Go особенно хорошо подходит для разработки инфраструктуры и инструментария для работы других программистов. Однако, будучи в действительности языком общего назначения, он применяется в самых разнообразных областях, становясь все более популярным в качестве замены интерпретируемых языков и обеспечивая компромисс между выразительностью и безопасностью. Программы на Go обычно выполняются быстрее, чем программы, написанные на интерпретируемых языках, и не завершаются аварийно с неожиданными ошибками. Go — это проект с открытым исходным кодом, так что исходный код его библиотек и инструментов, включая компилятор, находится в открытом доступе. Свой вклад в проект вносит активное мировое сообщество. Go работает на большом количестве Unix-подобных систем, таких как Linux, FreeBSD, OpenBSD, MacOS, а также на Microsoft Windows и Plan 9. При этом программы, написанные для одной из этих операционных систем, легко переносимы на другие. Этот курс призван помочь вам начать работать с Go, причём с самого начала эффективно использовать все возможности языка и его стандартной библиотеки для написания понятных, идиоматичных и эффективных программ. ## 1.1. Происхождение Go Подобно биологическим видам, успешные языки порождают потомство, которое наследует наилучшие особенности своих предков. Скрещивание при этом иногда приводит к удивительным результатам. Аналогом мутаций служит появление радикально новых идей. Как и в случае с живыми существами, глядя на такое влияние предков, можно многое сказать о том, почему язык получился именно таким, какой он есть, и для каких условий работы он приспособлен более всего. На рисунке ниже показано, какие языки повлияли на дизайн языка программирования Go. ![Происхождение Go](https://i.imgur.com/ULYoHh5.png) Go часто описывают как "C-подобный язык" или "язык С XXI века". От языка С Go унаследовал синтаксис выражений, конструкции управления потоком, базовые типы данных, передачу параметров в функции по значению, понятие указателей и, что важнее всего, направленность С на получение при компиляции эффективного машинного кода и естественное взаимодействие с абстракциями современных операционных систем. Однако в генеалогическом древе Go есть и другие предки. Одно из сильнейших влияний на Go оказали языки программирования Никлауса Вирта, начиная с Pascal. Modula-2 привнесла концепцию пакетов; Oberon использует один файл для определения модуля и его реализации; Oberon-2 повлиял на синтаксис пакетов, импорта и объявлений, а Object Oberon предоставил синтаксис для объявлений методов. Ещё одна линия предков Go, которая выделяет его среди прочих современных языков программирования, представляет собой последовательность малоизвестных исследовательских языков, разработанных в Bell Labs и основанных на концепции *взаимодействующих последовательных процессов* (communicating sequential processes - CSP) из статьи Тони Хоара 1978 года, посвящённой основам конкурентности. В CSP программа представляет собой параллельное объединение процессов, не имеющих общего состояния; процессы взаимодействуют и синхронизируются с помощью каналов. Но CSP Хоара представлял собой формальный язык описания фундаментальных концепций конкурентности, а не язык программирования для написания выполнимых программ. Роб Пайк и другие начали экспериментировать с реализациями CSP в виде фактических языков программирования. Первый из них назывался "Squeak" ("язык для общения с мышью"), который являлся языком для обработки событий мыши и клавиатуры со статически созданными каналами. За ним последовал Newsqueak, в котором С-образные инструкции и синтаксис выражений сочетались с записью типов в стиле Pascal. Это был чисто функциональный язык со сборкой мусора, направленный, как и его предшественник, на управление клавиатурой, мышью и оконными событиями. Каналы в нем стали полноправными участниками языка, динамически создаваемыми и хранимыми в переменных. Операционная система Plan 9 развила эти идеи в языке Alef. Alef попытался сделать Newsqueak жизнеспособным системным языком программирования, но конкурентность без сборки мусора требовала слишком больших усилий. Ряд конструкций в Go демонстрирует влияние генов непрямых предков; например, `iota` происходит из APL, а лексическая область видимости с вложенными функциями — из Scheme (и большинства последующих за ним языков). Уникальные для Go срезы обеспечивают динамические массивы с эффективным произвольным доступом, но при этом разрешают сложные размещения, напоминающие связные списки. Инструкция `defer` также представляет собой новинку Go. ## 1.2. Проект Go Все языки программирования тем или иным образом отражают философию их создателей, что часто приводит к включению в язык программирования их реакции на слабости и недостатки более ранних языков. Go не является исключением. Проект Go родился из разочарования в Google несколькими программными системами, страдающими от "взрыва сложности" (эта проблема отнюдь не уникальна для Google). Как заметил Роб Пайк, "сложность мультипликативна": устранение проблемы путем усложнения одной части системы медленно, но верно добавляет сложность в другие части. Постоянное требование внесения новых функций, настроек и конфигураций очень быстро заставляет отказаться от простоты, несмотря на то, что в долгосрочной перспективе простота является ключом к хорошему программному обеспечению. Простота требует большего количества работы по определению самой сути идеи в начале проекта и большей дисциплины во время его жизненного цикла, чтобы отличать хорошие изменения от плохих. При достаточных усилиях хорошие изменения, в отличие от плохих, могут быть приняты без ущерба для того, что Фред Брукс назвал "концептуальной целостностью" проекта. Плохие же изменения всего лишь разменивают простоту на удобство. Только с помощью простоты дизайна система может в процессе роста оставаться устойчивой, безопасной и последовательной. Проект Go включает в себя сам язык, его инструментарий, стандартные библиотеки и последнее (по списку, но не по значению) — культуру радикальной простоты. Будучи одним из современных высокоуровневых языков, Go обладает преимуществом ретроспективного анализа других языков, и это преимущество использовано в полной мере: в Go имеются сборка мусора, система пакетов, функции первого класса, лексическая область видимости, интерфейс системных вызовов и неизменяемые строки, текст в которых кодируется с использованием кодировки UTF-8. Однако язык программирования Go имеет сравнительно немного возможностей, и вряд ли в него будут добавлены новые. Например, в нем отсутствуют неявные числовые преобразования, нет конструкторов и деструкторов, перегрузки операторов, значений параметров по умолчанию; нет наследования, исключений; отсутствуют макросы, аннотации функций и локальная память потока. Язык программирования Go является зрелым и стабильным и гарантирует обратную совместимость: старые программы на Go можно компилировать и запускать с помощью новых версий компиляторов и стандартных библиотек. Go имеет достаточную систему типов, чтобы избежать большинства ошибок программистов в динамических языках, но эта система типов гораздо проще, чем в других строго типизированных языках программирования. Это приводит к изолированным очагам "нетипизированного программирования" в пределах более широких схем типов. Так что программисты на Go не прибегают к длинным конструкциям, которыми программисты на C++ или Haskell пытаются выразить свойства безопасности своих программ в виде доказательств на основе типов. На практике Go дает программистам преимущества безопасности и производительности времени выполнения относительно строгой системы типов без излишней сложности и накладных расходов. Go поощряет понимание дизайна современных компьютерных систем, в частности — важность локализации. Его встроенные типы данных и большинство библиотечных структур данных созданы для естественной работы без явной инициализации или неявных конструкторов, так что в коде скрывается относительно мало распределений и записей памяти. Составные типы Go (структуры и массивы) хранят свои элементы непосредственно,требуя меньшего количества памяти и её распределений, а также меньшего количества косвенных обращений с помощью указателей по сравнению с языками, использующими косвенные поля. А поскольку современные компьютеры являются параллельными вычислительными машинами, Go обладает возможностями конкурентности, основанными, как упоминалось ранее, на CSP. Стеки переменного размера легких потоков (или горутин) Go изначально достаточно малы, чтобы создание одной горутины было дешевым, а создание миллиона — практичным. Стандартная библиотека Go, часто описываемая фразой "всё включено", предоставляет строительные блоки и API для ввода-вывода, работы с текстом и графикой, криптографические функции, функции для работы с сетью и для создания распределённых приложений. Библиотека поддерживает множество стандартных форматов файлов и протоколов. Библиотеки и инструменты интенсивно используют соглашения по снижению потребностей в настройке, упрощая тем самым логику программ; таким образом, различные программы на Go становятся более похожими одна на другую и тем самым — более простыми в изучении. Проекты собираются с помощью всего лишь одного инструмента go и используют только имена файлов и идентификаторов и иногда — специальные комментарии для определения всех библиотек, выполнимых файлов, теcтов, примеров, документации и прочего в проектах; исходный код Go содержит всю необходимую спецификацию построения проекта. ## 1.3. Примеры программ Эта лекция представляет собой обзор основных компонентов Go. Здесь предоставлено достаточно информации и примеров для того, чтобы вы научились делать полезные вещи с помощью Go как можно быстрее. Приведённые здесь (как и во всём курсе) примеры позволяют решать задачи, которые вам, возможно, придётся или приходилось решать в реальной практике программиста. Мы попытаемся дать вам представление о разнообразии программ, которые можно написать на Go, начиная от простой обработки файлов и до конкурентных интернет-клиентов и веб-серверов. Мы, конечно, не будем объяснять всё на первой лекции, но изучение таких программ на новом языке может быть эффективным способом начать работу. При изучении нового языка программирования существует естественная тенденция писать новый код так, как вы писали бы его на хорошо знакомом вам языке. Попытайтесь противостоять этой тенденции. На курсе мы постараемся показать и объяснить, как писать хорошие идиоматичные программы на Go, так что используйте представленный здесь код как руководство при написании собственного кода. ### 1.3.1. Hello, World Давайте начнём с традиционной программы "hello, world", которая впервые появилась в книге "Язык программирования С" в 1978 году. Поскольку язык С — один из основных прямых предков Go, у нас есть исторические основания для того, чтобы начать с данной программы; кроме того, она иллюстрирует ряд центральных идей. ```go package main import "fmt" func main() { fmt.Println("Hello, мир") } ``` Go — компилируемый язык. Его инструментарий преобразует исходный код программы, а также библиотеки, от которых он зависит в команды на машинном языке компьютера. Доступ к инструментарию Go осуществляется с помощью единой команды `go`, которая имеет множество подкоманд. Простейшей из них является подкоманда `run`, которая компилирует исходный код из одного или нескольких файлов с расширением `.go`, осуществляет связывание с библиотеками, а затем запускает полученный в результате исполняемый файл. (Здесь и далее в лекциях символ `$` используется в качестве приглашения командной строки) ``` $ go run helloworld.go ``` Ничего удивительного, что программа выводит строку ``` Hello, мир ``` Go изначально поддерживает Unicode, так что он может обработать текст с любым набором символов. Если программа предназначена более чем для разового эксперимента, вероятно, вы захотите скомпилировать её и сохранить результат для дальнейшего использования. Это делается с помощью команды `go build`: ``` $ go build helloworld.go ``` Она создаёт бинарный исполняемый файл `helloworld`, который можно запустить в любой момент времени без какой-либо обработки: ``` $ ./helloworld Hello, мир ```` Давайте теперь поговорим о самой программе. Код на Go организован в виде пакетов, которые подобны библиотекам или модулям других языков. Пакет состоит из одного или нескольких `.go` файлов с исходным кодом в одном каталоге, которые определяют, какие действия выполняет данный пакет. Каждый файл с исходным кодом начинается с объявления `package` (в данном случае — `package main`), которое указывает, какому пакету принадлежит данный файл. Затем следует список других пакетов, которые этот файл импортирует, а после него — объявления программы, хранящейся в этом файле. Стандартная библиотека Go имеет более сотни пакетов для распространённых задач, таких как ввод и вывод, сортировка, работа с текстом и т.д. Например, пакет `fmt` содержит функции для форматированного вывода и сканирования ввода. Функция `Println` является одной из основных функций в пакете `fmt`; она выводит одно или несколько значений, разделённых пробелами и с добавлением символа новой строки в конце, так что выводимые значения располагаются в одной строке. Пакет `main` — особый. Он определяет отдельную программу, т.е. исполняемый файл, а не библиотеку. В пакете `main` *функция* `main` также является особой — именно с нее начинается программа. Программа делает то, что делается в функции `main`. Конечно, для выполнения действий функция `main` обычно вызывает другие функции из других пакетов, как, например `fmt.Println`. Мы должны указать компилятору, какие пакеты необходимы для данного исходного файла; эту задачу решают объявления `import`, следующие за объявлением `package`. Программа "hello, world" использует только одну функцию из одного стороннего пакета, но большинство программ импортируют несколько пакетов. Необходимо импортировать только те пакеты, которые вам нужны. Программа не будет компилироваться как при наличии отсутствующего импорта пакетов, так и при наличии излишнего. Это строгое требование предотвращает накопление ссылок на неиспользуемые пакеты по мере развития программы. Объявления `import` должны следовать за объявлением `package`. После этого в программе располагаются объявления функций, переменных, констант и типов (вводимые с помощью ключевых слов `func`, `var`, `const` и `type`); по большей части порядок объявлений не имеет значения. Приведённая программа максимально короткая, так как объявляет только одну функцию, которая, в свою очередь, также вызывает только одну функцию. Для экономии места мы не всегда будем указывать объявления `package` и `import` в примерах, но они должны быть в файлах исходного кода, чтобы он успешно компилировался. Интегрированная среда разработки JetBrains GoLand автоматически добавляет импорты для стандартных пакетов при необходимости. Объявление функции состоит из ключевого слова `func`, имени функции, списка параметров (пустого списка в случае функции `main`), списка результатов (также пустого в данном случае) и тела функции — инструкций, которые определяют выполняемые функцией действия — в фигурных скобках. Более подробно функции рассмотрим на пятой лекции. Go не требует точек с запятой в конце инструкции или объявления, за исключением случаев, когда две или более инструкций находятся в одной строке. По сути, символы новой строки после определённых лексем преобразуются в точки с запятой, так что местоположение символов новой строки имеет значение для корректного синтаксического анализа кода на Go. Например, открывающая фигурная скобка `{` функции должна находится в той же строке, что и конец объявления `func`, а не на отдельной строке, а в выражении `x + y` символ новой строки разрешен после, но не до оператора `+`. Go занимает жёсткую позицию относительно форматирования кода. Инструмент `gofmt` приводит код к стандартному формату, а подкоманда `fmt` инструмента `go` применяет `gofmt` ко всем файлам в указанном пакете или к файлам в текущем каталоге по умолчанию. Все файлы исходного кода на Go в курсе пропущены через `gofmt`, и вам нужно выработать привычку поступать так же со своим кодом. Формальное объявление стандартного формата устраняет множество бессмысленных споров о мелочах и, что более важно, разрешает целый ряд автоматизированных преобразований исходного кода, которые были бы неосуществимы при разрешённом произвольном форматировании. Многие текстовые редакторы и, в частности, среда разработки JetBrains GoLand могут настраиваться так, что при каждом сохранении файла будет запускаться инструмент `gofmt`, так что ваш исходный код всегда будет правильно отформатирован. Еще один инструмент, `goimports`, дополнительно управляет вставкой и удалением объявлений импорта при необходимости. Он не является частью стандартного дистрибутива, но вы можете получить его с помощью следующей команды: ``` $ go get golang.org/x/tools/cmd/goimports ``` Для большинства пользователей обычным средством загрузки и построения пакетов, запуска тестов, показа документации и так далее является инструмент `go`, который мы подробно рассмотрим в 10-й лекции. ### 1.3.2. Аргументы командной строки Большинство программ обрабатывают некоторые входные данные для генерации некоторых выходных данных; это довольно точное определение вычислений. Но как получить входные данные для работы программы? Некоторые программы генерируют собственные данные, но чаще ввод поступает из внешнего источника: файла, подключения к сети, вывода из другой программы, ввода пользователя с помощью клавиатуры, из аргументов командной строки или другого подобного источника. В нескольких следующих примерах мы рассмотрим некоторые из перечисленных альтернатив, начиная с аргументов командной строки. Пакет `os` предоставляет функции и различные значения для работы с операционной системой платформо-независимым образом. Аргументы командной строки доступны в программе в виде переменной с именем `Args`, которая является частью пакета `os` (таким образом, её имя в любом месте программы за пределами пакета `os` выглядит как `os.Args`). Переменная `os.Args` представляет собой *срез* (slice) строк. Срезы являются фундаментальным понятием в Go, и вскоре мы поговорим о них гораздо подробнее. Пока же думайте о срезе как о последовательности (с динамическим размером) элементов одного типа, в которой к отдельным элементам можно обращаться как `s[i]`, а к непрерывной последовательности — как `s[m:n]`. Количество этих элементов определяется как `len(s)`. Как и в большинстве языков программирования, индексация в Go использует *полуоткрытые* интервалы, которые включают первый индекс, но исключают последний, потому что это упрощает логику. Например, срез `s[m:n]`, где `0 ≤ m ≤ n ≤ len(s)`, содержит `n-m` элементов. Первый элемент `os.Args`, `os.Args[0]` содержит имя самой команды; остальные элементы представляют собой аргументы, которые были переданы программе, когда началось её выполнение. Выражение вида `s[m:n]` даёт срез, который указывает на элементы от `m` до `n-1`, так что элементы, которые нам потребуются в следующем примере, находятся в срезе `os.Args[1:len(os.Args)]`. Если опущено значение `m` или `n`, используются значения по умолчанию — `0` или `len(s)` соответственно, так что мы можем сократить запись нужного нам среза до `os.Args[1:]`. Далее представлена реализация команды Unix `echo`, которая выводит в одну строку аргументы, переданные в командой строке. Она импортирует два пакета, которые указаны в круглых скобках, а не в виде отдельных объявлений импорта. Можно использовать любую запись, но обычно используется список. Порядок импорта значения не имеет; инструмент `gofmt` сортирует имена пакетов в алфавитном порядке. (Если имеется несколько версий примера, мы будем часто их нумеровать, чтобы вы точно знали, о какой из них мы говорим.) ```go // Echo1 выводит аргументы командной строки package main import ( "fmt" "os" ) func main() { var s, sep string for i := 1; i < len(os.Args); i++ { s += sep + os.Args[i] sep = " " } fmt.Println(s) } ``` Комментарии начинаются с символа `//`. Весь текст от `//` до конца строки является комментарием, предназначенным для программиста, и игнорируется компилятором. По соглашению мы описываем каждый пакет в комментарии, непосредственно предшествующем его объявлению; для пакета `main` этот комментарий состоит из одного или нескольких полных предложений, которые описывают программу в целом. Объявление `var` объявляет две переменные — `s` и `sep` типа `string`. Как часть объявления, переменная может быть инициализирована. Если переменная не инициализирована явно, она неявно инициализируется *нулевым значением* соответствующего типа (которое равно `0` для числовых типов и пустой строке `""` для строк). Таким образом, в этом примере объявление неявно инициализирует строки `s` и `sep`. Более подробно о переменных и объявлениях мы поговорим в лекции 2, "Структура программы". Для чисел Go предоставляет обычные арифметические и логические операторы. Однако, при применении к строкам оператор `+` выполняет *конкатенацию* их значений, так что выражение ```go sep + os.Args[i] ``` представляет собой конкатенацию строк `sep` и `os.Args[i]`. Использованная в программе инструкция ```go s += sep + os.Args[i] ``` представляет собой инструкцию присваивания, которая выполняет конкатенацию старого значения `s` с `sep` и `os.Args[i]` и присваивает новое значение переменной s; она эквивалентна выражению ```go s = s + sep + os.Args[i] ``` Оператор `+=` является *присвивающим оператором*. Каждый арифметический и логический оператор наподобие `+` или `*` имеет соответствующий присваивающий оператор. Программа `echo` могла бы вывести все выходные данные в цикле по одному фрагменту за раз, но наша версия вместо этого строит строку, добавляя новый текст в конце каждого фрагмента. Изначально строка `s` пуста, т.е. имеет значение `""`, и каждая итерация цикла добавляет к ней текст; после первой итерации перед очередным фрагментом вставляется пробел, так что после завершения цикла между всеми аргументами имеются пробелы. Этот процесс имеет квадратичное время работы, так что он может оказаться дорогостоящим, если количество аргументов будет большим, но для `echo` это маловероятно. Далее мы приведём несколько усовершенствованных версий `echo`. Индексная переменная цикла `i` объявлена в первой части цикла `for`. Символы `:=` являются частью *краткого объявления переменной*, инструкции, которая объявляет одну или несколько переменных и назначает им соответствующие типы на основе значения инициализатора. В следующих лекциях мы расскажем об этом подробнее. Оператор инкремента `i++` добавляет к `i` единицу. Эта запись эквивалентна записи `i += 1`, которая, в свою очередь, эквивалентна записи `i = i + 1`. Имеется и соответствующий оператор декремента `i--`, который вычитает 1. Это инструкции, а не выражения, как в большинстве языков семейства С, поэтому запись `j = i++` является некорректной; кроме того, эти операторы могут быть только постфиксными. Цикл `for` является единственной инструкцией цикла в Go. Он имеет ряд разновидностей, одна из которых показана здесь: ```go for инициализация; условие; последействие { // нуль или несколько инструкций } ``` Вокруг трех компонентов цикла `for` скобки не используются. Фигурные же скобки обязательны, причем открывающая фигурная скобка обязана находиться в той же строке, что и инструкция *последействие*. Необязательная инструкция *инициализации* выполняется до начала работы цикла. Если она имеется в наличии, она обязана быть *простой инструкцией*, т.е. кратким объявлением переменной, инструкцией инкремента или присваивания, или вызовом функции. *Условие* представляет собой логическое выражение, которое вычисляется в начале каждой итерации цикла. Если его вычисление дает результат `true`, выполняются инструкции тела цикла. Инструкция *последействие* выполняется после тела цикла, после чего вновь вычисляется условие. Цикл завершается, когда вычисление условия дает значение `false`. Любая из перечисленных частей может быть опущена. При отсутствии как инициализации, так и последействия можно опустить точки с запятыми: ```go // традиционный цикл "while" for условие { // ... } ``` Если условие опущено полностью в любой из разновидностей цикла, например, в ```go // Традиционный бесконечный цикл for { // ... } ``` мы получаем бесконечный цикл, который должен завершиться некоторым иным путем, например с помощью инструкции `break` или `return`. Еще одна разновидность цикла for выполняет итерации для *диапазона* значений для типа данных наподобие строки или среза. Для иллюстрации приведём вторую версию программы `echo`: ```go // Echo2 выводит аргументы командной строки. package main import ( "fmt" "os" ) func main() { s, sep := "", "" for _, arg := range os.Args[1:] { s += sep + arg sep = " " } fmt.Println(s) } ``` В каждой итерации цикла `range` производит пару значений: индекс и значение элемента с этим индексом. В данном примере мы не нуждаемся в индексе, но синтаксис цикла по диапазону требует, чтобы, имея дело с элементом, мы работали и с индексом. Одно из решений заключается в том, чтобы присваивать значение индекса временной переменной с очевидным именем наподобие `temp` и игнорировать его. Однако Go не допускает наличия неиспользуемых локальных переменных, так что этот способ приведет к ошибке компиляции. Решение заключается в применении *пустого идентификатора* (blank identifier) с именем `_` (символ подчеркивания). Пустой идентификатор может использоваться везде, где синтаксис требует имя переменной, но логике программе оно не нужно, например в цикле по диапазону, в котором нам достаточно знать значение элемента, но не его индекс. Большинство программистов Go предпочтет использовать `range` и `_` для записи программы echo (как это сделано выше), поскольку индексирование `os.Args` выполняется при этом неявно, а значит, труднее допустить ошибку при написании. В этой версии программы для объявления и инициализации `s` и `sep` используется краткое объявление переменной, но мы можем объявить эти переменные и отдельно. Имеются разные способы объявления строковых переменных; приведенные далее объявления эквивалентны: ```go s := "" var s string var s = "" var s string = "" ``` Почему мы должны предпочитать один вид объявления другим? Первая разновидность, краткое объявление переменной, является наиболее компактной, однако может использоваться только внутри функции, но не для переменных уровня пакета. Вторая разновидность основана на инициализации по умолчанию (для строки — значением `""`). Третья разновидность используется редко, в основном при объявлении нескольких переменных. Четвертая разновидность содержит явное указание типа переменной, которое является излишним, когда тип совпадает с типом начального значения переменной, но которое является обязательным в других случаях, когда типы переменной и инициализатора разные. На практике обычно следует использовать одну из первых двух разновидностей: с явной инициализацией (чтобы указать важность начального значения) и с неявной инициализацией по умолчанию (чтобы указать, что начальное значение не играет роли). Как отмечалось выше, при каждой итерации цикла строка `s` получает новое содержимое. Оператор `+=` создает новую строку путем конкатенации старой строки, символа пробела и очередного аргумента, а затем присваивает новую строку переменной `s`. Старое содержимое строки `s` более не используется, поэтому оно будет в надлежащее время обработано сборщиком мусора. Если объем данных велик, это может быть дорогостоящим решением. Более простым и более эффективным решением было бы использование функции `Join` из пакета `strings`: ```go func main() { fmt.Println(strings.Join(os.Args[1:], " ")) } ``` Наконец, если нам не нужно беспокоиться о формате и нужно увидеть только значения, например, для отладки, мы можем позволить функции `Println` форматировать результаты для нас: ```go fmt.Println(os.Args[1:]) ``` Вывод этой инструкции похож на вывод, полученный в версии с применением `strings.Join`, но с окружающими квадратными скобками. Таким образом может быть выведен любой срез. ### Упражнение 1.1. Измените программу `echo` так, чтобы она выводила также `os.Args[0], имя выполняемой команды. ### Упражнение 1.2. Измените программу `echo` так, чтобы она выводила индекс и значение каждого аргумента по одному аргументу в строке. ### Упражнение 1.3. Поэкспериментируйте с измерением разницы времени выполнения потенциально неэффективных версий и версии с применением `strings.Join`. ### 1.3.3. Поиск повторяющихся строк Программы для копирования файлов, печати, поиска, сортировки, подсчета и другие имеют схожую структуру: цикл по входным данным, некоторые вычисления над каждым элементом и генерация вывода “налету” или по окончании вычислений. Мы покажем три варианта программы под названием `dup`, на создание которой нас натолкнула команда `uniq` из Unix, которая ищет соседние повторяющиеся строки. Использованные структуры и пакеты представляют собой модели, которые могут быть легко адаптированы. Первая версия `dup` выводит каждую строку, которая в стандартном вводе появляется больше одного раза, выводя предварительно количество ее появлений. В этой программе вводятся инструкция `if` , тип данных `map` и пакет `bufio`. ```go // Dup1 выводит текст каждой строки, которая появляется в // стандартном вводе более одного раза, а также количество // ее появлений. package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) input := bufio.NewScanner(os.Stdin) for input.Scan() { counts[input.Text()]++ } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } } ``` Как и в цикле `for`, вокруг условия инструкции `if` нет скобок, но для тела инструкции фигурные скобки обязательны. Может иметься необязательная часть `else`, которая выполняется при ложности условия. *Отображение* (map) содержит набор пар "ключ-значение" и обеспечивает константное время выполнения операций cохранения, извлечения или проверки наличия элемента в множестве. Ключ может быть любого типа, лишь бы значения этого типа можно было сравнить с помощью оператора `==`; распространенным примером ключа являются строки. Значение может быть любого типа. В нашем примере ключи представляют собой строки, а значения представлены типом `int`. Встроенная функция `make` создает новое пустое отображение; она имеет и другие применения. Всякий раз, когда `dup` считывает строку ввода, эта строка используется как ключ в отображении, и соответствующее значение увеличивается. Инструкция `counts[input.Text()]++` эквивалентна следующим двум инструкциям: ```go line := input.Text() counts[line] = counts[line] + 1 ``` Если в отображении еще нет нужного нам ключа, это не проблема. Когда новая строка встречается впервые, выражение `counts[line]` в правой части возвращает нулевое значение, которое для типа `int` равно 0. Для вывода результатов мы вновь используем цикл по диапазону, на этот раз — по отображению `counts`. Как и ранее, каждая итерация дает две величины — ключ и значение элемента отображения для этого ключа. Порядок обхода отображения не определён, на практике этот порядок случаен и варьируется от одного выполнения программы к другому. Это сделано преднамеренно, поскольку предотвращает написание программ, опирающихся на конкретное упорядочение, которое не гарантируется. Далее наступает очередь пакета `bufio`, который помогает сделать ввод и вывод эффективным и удобным. Одной из наиболее полезных его возможностей является тип с именем `Scanner`, который считывает входные данные и разбивает их на строки или слова; зачастую это самый простой способ обработки ввода, который поступает построчно. Программа использует краткое объявление переменной для создания новой переменной `input`, которая ссылается на `bufio.Scanner`: ```go input := bufio.NewScanner(os.Stdin) ``` Сканер считывает стандартный ввод программы. Каждый вызов `input.Scan()` считывает очередную строку и удаляет завершающий символ новой строки; результат можно получить путем вызова `input.Text()`. Функция `Scan` возвращает значение `true`, если строка считана и доступна, и значение `false`, если входные данные исчерпаны. Функция `fmt.Printf`, подобно функции `printf` в языке программирования С и других языках, выполняет форматированный вывод на основе списка выражений. Первым ее аргументом является строка формата, которая указывает, как должны быть отформатированы последующие аргументы. Формат каждого аргумента определяется символом преобразования, буквой, следующей за знаком процента. Например, `%d` форматирует целочисленный операнд в десятичной записи, a `%s` выводит значение строкового операнда. Функция `Printf` имеет больше десятка таких преобразований, которые программисты на Go называют глаголами (verbs). Приведенная далее таблица далека от полной спецификации, но иллюстрирует ряд доступных возможностей: | Символ преобразования | Назначение | |--------------------------|-------------------------------------------------------| |`%d`|Десятичное целое| |`%x`, `%o`, `%b`|Целое в шестнадцатеричном, восьмеричном и двоичном представлениях| |`%f`, `%g`, `%e`|Числа с плавающей точкой: `3.141593`, `3.141592653589793`,`3.141593е+00`| |`%t`|Булево значение: `true` или `false`| |`%c`|Руна (символ Unicode)| |`%s`|Строка| |`%q`|Выводит в кавычках строку типа `"abc"` или символ типа `'с'`| |`%v`|Любое значение в естественном формате| |`%T`|Тип любого значения| |`%%`|Символ процента| Строка формата в `dup1` содержит также символы табуляции `\t` и новой строки `\n`. Строковые литералы могут содержать такие управляющие последовательности для представления символов, которые обычно невидимы на экране и не могут быть введены непосредственно. По умолчанию `Printf` не записывает символ новой строки. По соглашению функции форматирования, имена которых заканчиваются на f , такие как `log.Printf` и `fmt.Errorf`, используют правила форматирования `fmt.Printf`, тогда как функции, имена которых заканчиваются на ln, как `Println`, форматируют свои аргументы так, как будто используется символ преобразования `%v`, а за ним — символ новой строки. Многие программы считывают входные данные либо из стандартного ввода, как приведенная выше, либо из последовательности именованных файлов. Следующая версия `dup` может как выполнять чтение стандартного ввода, так и работать со списком файлов, используя `os.Open`: ```go // Dup2 выводит текст каждой строки, которая появляется во // входных данных более одного раза. Программа читает // стандартный ввод или список именованных файлов, package main import ( "bufio" "fmt" "os" ) func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, "dup2: %v\n", err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf("%d\t%s\n", n, line) } } } func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } } ```