КулЛиб - Классная библиотека! Скачать книги бесплатно
Всего книг - 713023 томов
Объем библиотеки - 1403 Гб.
Всего авторов - 274606
Пользователей - 125091

Новое на форуме

Новое в блогах

Впечатления

Влад и мир про Шенгальц: Черные ножи (Альтернативная история)

Читать не интересно. Стиль написания - тягомотина и небывальщина. Как вы представляете 16 летнего пацана за 180, худого, болезненного, с больным сердцем, недоедающего, работающего по 12 часов в цеху по сборке танков, при этом имеющий силы вставать пораньше и заниматься спортом и тренировкой. Тут и здоровый человек сдохнет. Как всегда автор пишет о чём не имеет представление. Я лично общался с рабочим на заводе Свердлова, производившего

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
Влад и мир про Владимиров: Ирландец 2 (Альтернативная история)

Написано хорошо. Но сама тема не моя. Становление мафиози! Не люблю ворьё. Вор на воре сидит и вором погоняет и о ворах книжки сочиняет! Любой вор всегда себя считает жертвой обстоятельств, мол не сам, а жизнь такая! А жизнь кругом такая, потому, что сам ты такой! С арифметикой у автора тоже всё печально, как и у ГГ. Простая задачка. Есть игроки, сдающие определённую сумму для участия в игре и получающие определённое количество фишек. Если в

  подробнее ...

Рейтинг: 0 ( 0 за, 0 против).
DXBCKT про Дамиров: Курсант: Назад в СССР (Детективная фантастика)

Месяца 3-4 назад прочел (а вернее прослушал в аудиоверсии) данную книгу - а руки (прокомментировать ее) все никак не доходили)) Ну а вот на выходных, появилось время - за сим, я наконец-таки сподобился это сделать))

С одной стороны - казалось бы вполне «знакомая и местами изьезженная» тема (чуть не сказал - пластинка)) С другой же, именно нюансы порой позволяют отличить очередной «шаблон», от действительно интересной вещи...

В начале

  подробнее ...

Рейтинг: +2 ( 2 за, 0 против).
DXBCKT про Стариков: Геополитика: Как это делается (Политика и дипломатия)

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

Не собирался же ее брать изначально поскольку (давным давно до этого) после прочтения одной "явно неудавшейся" книги автора, навсегда зарекся это делать... Но потом до меня все-таки дошло что (это все же) не "очередная злободневная" (читай

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).
DXBCKT про Москаленко: Малой. Книга 3 (Боевая фантастика)

Третья часть делает еще более явный уклон в экзотерику и несмотря на все стсндартные шаблоны Eve-вселенной (базы знаний, нейросети и прочие девайсы) все сводится к очередной "ступени самосознания" и общения "в Астралях")) А уж почти каждодневные "глюки-подключения-беседы" с "проснувшейся планетой" (в виде галлюцинации - в образе симпатичной девчонки) так и вообще...))

В общем герою (лишь формально вникающему в разные железки и нейросети)

  подробнее ...

Рейтинг: +1 ( 1 за, 0 против).

Head First. Изучаем Go [Джей Макгаврен] (pdf) читать онлайн

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


 [Настройки текста]  [Cбросить фильтры]

Head First
Go
Wouldn`t it be dreamy if there
were a book on Go that focused on
the things you need to know? I guess
it`s just a fantasy...

Jay McGavren

Head First
Go
Как бы было хорошо
найти книгу о языке Go,
в которой не будет ничего лишнего...
Наверное, об этом можно
только мечтать...

Джей Макгаврен

2020

ББК 32.973.2-018.1
УДК 004.43
М15

Макгаврен Джей
М15

Head First. Изучаем Go. — СПб.: Питер, 2020. — 544 с.: ил. — (Серия «Head First O’Reilly»).
ISBN 978-5-4461-1395-8
Go упрощает построение простых, надежных и эффективных программ. А эта книга сделает его доступным
для обычных программистов. Основная задача Go — эффективная работа с сетевыми коммуникациями и многопроцессорной обработкой, но код на этом языке пишется и читается не сложнее чем на Python и JavaScript.
Простые примеры позволят познакомиться с языком в действии и сразу приступить к программированию на
Go. Так что вы быстро освоите общепринятые правила и приемы, которые позволят вам называть себя гофером.

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

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

ISBN 978-1491969557 англ.

ISBN 978-5-4461-1395-8

Authorized Russian translation of the English edition of Head First Go
ISBN 9781491969557 © 2019 Jay McGavren
This translation is published and sold by permission of O’Reilly Media, Inc., which
owns or controls all rights to publish and sell the same
© Перевод на русский язык ООО Издательство «Питер», 2020
© Издание на русском языке, оформление ООО Издательство «Питер», 2020
© Серия «Head First O’Reilly», 2020

оглавление

Содержание (сводка)
Введение

25

1

Знакомство с Go. Основы синтаксиса

35

2

Какой код будет выполняться? Условные команды и циклы

65

3

Вызовы функций. Функции

113

4

Запаковка кода. Пакеты

147

5

И далее по списку. Массивы

183

6

Проблема с присоединением. Сегменты

209

7

Значения и метки. Карты

239

8

Совместное хранение. Структуры

265

9

Ты – мой тип! Определяемые типы

299

10

Все при себе. Инкапсуляция и встраивание

323

11

Что можно сделать? Интерфейсы

355

12

Снова на ногах. Восстановление после сбоев

383

13

Совместное выполнение. Горутины и каналы

413

14

Контроль качества кода. Автоматизация тестирования

435

15

Запросы и ответы. Веб-приложения

459

16

Пример для подражания. Шаблон HTML

479

A

Функция os.OpenFile: Открытие файлов

515

Б

Еще шесть тем. Напоследок

529

Содержание (настоящее)
Введение
Ваш мозг и Go. Вы сидите за книгой и пытаетесь что-нибудь выучить, но ваш мозг считает, что вся эта писанина не нужна. Ваш мозг
говорит: «Выгляни в окно! На свете есть более важные вещи, например сноуборд». Как заставить мозг изучить программирование на Go?
Для кого написана эта книга?

26

Мы знаем, о чем вы думаете

27

И мы знаем, о чем думает ваш мозг

27

Метапознание: наука о мышлении



29



30

Примите к сведению

32

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

33

Вот что сделали МЫ



5

оглавление

Знакомство с Go

1

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

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

)

Результат.

"Hello, Go!"

Hello, Go!
1 + 2
4 < 6

3
true
'Җ'

1174

6

На старт... внимание... Go!

36

Интерактивная среда Go Playground

37

Что это все означает?

38

Структура типичного файла Go

38

А если что-то пойдет не так?

39

Вызов функций

41

Функция Println

41

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

42

Возвращаемые значения функций

43

Шаблон программы Go

45

Строки

45

Руны

46

Логические значения

46

Числа

47

Математические операции и сравнения

47

Типы

48

Объявление переменных

50

Нулевые значения

51

Короткие объявления переменных

53

Правила выбора имен

55

Преобразования

56

Установка Go на вашем компьютере

59

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

60

Инструменты Go

61

Быстрый запуск кода командой «go run»

61

Ваш инструментарий Go

62

оглавление

2

Ключевое
слово «if».

Какой код будет выполняться?
Условные команды и циклы
В каждой программе есть части, которые должны выполняться
только в определенных ситуациях.«Этот код должен выполняться, если
произошла ошибка. А если нет — должен выполняться этот код». Почти в каждой
программе присутствует код, который должен выполняться только в том случае,
если некоторое условие истинно. Почти в каждом языке программирования существуют условные команды, которые позволяют определить, нужно ли выполнять
те или иные сегменты кода. Язык Go не исключение. Возможно, какие-то части кода
должны выполняться многократно. Как и многие языки, Go поддерживает циклы
для многократного выполнения блоков кода. В этой главе вы научитесь применять
как условные команды, так и циклы!

Условие.

Начало условного
блока.

Вызов методов

66

Проверка результата

68

Комментарии

68

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

69

Множественные возвращаемые значения функций или методов

70

Вариант 1. Игнорировать возвращаемое значение ошибки

71

Вариант 2. Обработка ошибки

72

Условные команды

73

Условная выдача фатальной ошибки

76

Избегайте замещения имен
if 1 < 2 {
fmt.Println("It's true!") Преобразование строк в числа
}
Блоки
Конец
условного
блока.

Тело условного блока.

78
80
83

Создание игры

89

Имена пакетов и пути импортирования

90

Генерирование случайных чисел

91

Получение целого числа с клавиатуры

93

Сравнение предположения с загаданным числом

94

Циклы

95

Операторы инициализации и приращения необязательны

97

Циклы и области видимости

97

Использование цикла в игре

100

Пропуск частей цикла командами continue и break

102

Выход из цикла

103

Вывод загаданного числа

104

Поздравляем, игра готова!

106

Ваш инструментарий Go

108

7

оглавление

3

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

высота

ширина

8

Повторяющийся код

114

Форматирование вывода функциями Printf и Sprintf

115

Глаголы форматирования

116

Форматирование значений ширины

117

Форматирование с дробными значениями ширины

118

Использование Printf в программе

119

Объявление функций

120

Объявление параметров функции

121

Использование функций в программе

122

Функции и области видимости переменных

124

Возвращаемые значения функций

125

Использование возвращаемого значения в программе

127

Функции paintNeeded нужна обработка ошибок

129

Значения ошибок

130

Объявление нескольких возвращаемых значений

131

Использование множественных возвращаемых значений
с функцией paintNeeded

132

Всегда обрабатывайте ошибки!

133

В параметрах функций хранятся копии аргументов

136

Указатели

137

Типы указателей

138

Чтение или изменение значения по указателю

139

Использование указателей с функциями

141

Исправление функции «double» с использованием
указателей

142

Ваш инструментарий Go

144

оглавление

4

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

go
bin
hi
pkg
src
greeting
greeting.go

Разные программы, одна функция

148

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

150

Хранение кода пакетов

151

Создание нового пакета

152

Импорт пакета в программу

153

Файлы пакетов имеют одинаковую структуру

154

Соглашения по выбору имен пакетов

157

Уточнение имен

157

Перемещение общего кода в пакет

158

Константы

160

Вложенные каталоги и пути импорта пакетов

162

Установка исполняемых файлов командой «go install»

164

Переменная GOPATH и смена рабочих областей

165

Настройка GOPATH

166

Публикация пакетов

167

Загрузка и установка пакетов командой «go get»

171

Чтение документации пакетов командой «go doc»

173

Документирование пакетов

175

Просмотр документации в браузере

177

Запуск сервера документации HTML командой «godoc»

178

Сервер «godoc» включает ВАШИ пакеты!

179

Ваш инструментарий Go

180

hi
main.go

9

оглавление

5

И далее по списку
Массивы
Многие программы работают со списками. Списки адресов. Списки
телефонных номеров. Списки товаров. В Go существуют два встроенных способа хранения списков. В этой главе мы разберем первый способ: массивы. Вы
научитесь создавать массивы, заполнять их данными и извлекать сохраненные
данные. Далее рассмотрим способы обработки всех элементов в массиве: сначала сложный вариант с циклами for, а затем простой — с циклами for...range.

Количество
элементов
в массиве.

В массивах хранятся наборы значений

184

Нулевые значения в массивах

186

Литералы массивов

187

Функции пакета «fmt» умеют работать с массивами

188

Обращение к элементам массива в цикле

189

Проверка длины массива функцией «len»

190

Безопасный перебор массивов в цикле «for...range»

191

Пустой идентификатор в циклах «for...range»

192

Суммирование чисел в массиве

193

Вычисление среднего значения

195

Чтение текстового файла

197

Чтение текстового файла в массив

200

Чтение текстового файла в программе «average»

202

Наша программа может обрабатывать только три значения!

204

Ваш инструментарий Go

206

Тип элементов
в массиве.

var myArray [4]string

10

Индекс 0.
Индекс 1.
Индекс 2.
Индекс 3.

оглавление

6

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

Сегмент.

Базовый массив.

Сегменты

210

Литералы сегментов

211

Оператор сегмента

214

Базовые массивы

216

При изменении базового массива изменяется сегмент

217

Расширение сегментов функцией «append»

218

Сегменты и нулевые значения

220

Чтение строк из файла с использованием сегментов и «append»

221

Тестирование измененной программы

223

Возвращение сегмента nil в случае ошибки

224

Аргументы командной строки

225

Получение аргументов командной строки из сегмента os.Args

226

Оператор сегмента может использоваться с другими сегментами

227

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

228

Функции с переменным количеством аргументов

229

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

231

Использование функции с переменным количеством параметров
для вычисления среднего значения

232

Передача сегментов функциям с переменным
количеством аргументов

233

Сегменты спасли положение!

235

Ваш инструментарий Go

236

11

оглавление

7

Значения и метки
Карты
Сваливать все в одну кучу удобно до тех пор, пока не потребуется
что-нибудь найти.Вы уже видели, как создавать списки значений в массивах
и сегментах. Вы знаете, как применить одну операцию к каждому значению в массиве или сегменте. Но что, если требуется поработать с конкретным значением?
Чтобы найти его, придется начать с начала массива или сегмента и просмотреть.
Каждое. Существующее. Значение. А если бы существовала разновидность коллекций, в которой каждое значение снабжается специальной меткой? Тогда нужное
значение можно было бы быстро найти по метке! Эта глава посвящена картам,
которые предназначены именно для этого.

Ключи ускоряют
поиск данных!

Carlos Diaz
Mikey Moose
Amber Graham
Brian Martin

Карта

12

Подсчет голосов

240

Чтение имен из файла

241

Подсчет имен: сложный способ с сегментами

243

Карты

246

Карты (продолжение)

247

Литералы карт

248

Нулевые значения с картами

249

Нулевое значение для карты равно nil

249

Как отличить нулевые значения от присвоенных

250

Удаление пар «ключ/значение» функцией «delete»

252

Версия программы с использованием карты

253

Циклы for...range с картами

255

Цикл for...range обрабатывает карты в случайном порядке!

257

Обновление программы подсчета голосов
циклом for...range

258

Программа подсчета голосов готова!

259

Ваш инструментарий Go

261

оглавление

8

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

266

Структуры формируются из значений многих типов

267

Обращение к полям структуры

268

Хранение данных подписчиков в структуре

269

Определения типов и структуры

270

Использование определяемого типа для информации
о подписчиках

272

Использование определяемых типов с функциями

273

Изменение структуры в функции

276

Обращение к полям структур по указателю

278

Передача больших структур с помощью указателей

280

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

282

Экспорт определяемых типов

283

Экспорт полей структур

284

Литералы структур

285

Создание типа структуры Employee

287

Создание типа структуры Address

288

Добавление структуры как поля в другой тип

289

Создание вложенной структуры

289

Анонимные поля структур

292

Встроенные структуры

293

Наши определяемые типы готовы!

294

Ваш инструментарий Go

295

13

оглавление

9

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

300

Определяемые типы с базовыми основными типами

301

Определяемые типы и операторы

303

Преобразования типов с помощью функций

305

Разрешение конфликтов имен с использованием методов

308

Определение методов

309

Параметр получателя (почти) не отличается от других параметров

310

Метод (почти) не отличается от функции

311

Указатели и параметры получателей

313

Преобразование литров и миллилитров в галлоны с помощью методов

317

Преобразование Gallons в Liters и Milliliters с помощью методов

318

Ваш инструментарий Go

319

Сколько куп­
лено топлива
по представле­
нию Стива
10 галлонов
Сколько куплено
на самом деле!
10 литров

14

оглавление

10

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

Проверка данных в set-методах
прекрасно работает... когда пользователи
вызывают их. Но они обращаются к полям
структур напрямую и продолжают вводить
недопустимые данные!

Создание типа структуры Date

324

Пользователи заполняют поле структуры Date
недопустимыми значениями!

325

Set-методы

326

Set-методам необходимы указатели на получателей

327

Добавление остальных set-методов

328

Включение проверки данных в set-методы

330

Полям все равно можно присвоить
недопустимые значения!

332

Перемещение типа Date в другой пакет

333

Отмена экспортирования полей Date

335

Обращения к неэкспортируемым полям
через экспортируемые методы

336

Get-методы

338

Инкапсуляция

339

Встраивание типа Date в тип Event

342

Неэкспортируемые поля не повышаются

343

Экспортируемые методы повышаются так же,
как и поля

344

Инкапсуляция поля Title типа Event

346

Повышенные методы существуют наряду
с методами внешнего типа

347

Пакет calendar готов!

348

Ваш инструментарий Go

350

15

оглавление

11

Что можно сделать?
Интерфейсы
Иногда конкретный тип значения не важен. Вас не интересует,
с чем вы работаете. Вы просто хотите быть уверены в том, что оно может
делать то, что нужно вам. Тогда вы сможете вызывать для значения определенные методы. Неважно, с каким значением вы работаете — Pen или
Pencil; вам просто нужно нечто, содержащее метод Draw. Именно эту задачу

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

Плеер

Магнитофон

16

Два разных типа с одинаковыми методами

356

В параметре метода может передаваться только один тип

357

Интерфейсы

359

Определение типа, поддерживающего интерфейс

360

Конкретные типы и типы интерфейсов

361

Присваивание любого типа, поддерживающего интерфейс

362

Вызывать можно только методы, определенные
как часть интерфейса

363

Исправление функции playList с помощью интерфейса

365

Утверждения типа

368

Ошибки утверждений типа

370

Предотвращение паники при ошибках утверждений типов

371

Тестирование TapePlayer и TapeRecorder с утверждениями типов

372

Интерфейс error

374

Интерфейс Stringer

376

Пустой интерфейс

378

Ваш инструментарий Go

381

оглавление

12

Снова на ногах
Восстановление после сбоев
Любая программа сталкивается с ошибками. Учтите их
при планировании. Иногда обработка ошибки сводится к простому
выводу сообщения и завершению программы. Другие ошибки требуют
дополнительных действий: например, закрытия открытых файлов или сетевых подключений или иного освобождения ресурсов, чтобы после вашей
программы оставался порядок. В этой главе мы покажем, как отложить
завершающие действия, чтобы они выполнились даже в случае ошибки.
Также вы узнаете, как поднять панику в тех (редких) ситуациях, когда это
уместно, и как восстановиться после нее.

Не преоб­
разуется
в float64!

20.25
hello
10.5

Снова о чтении чисел из файла

384

Любая ошибка помешает закрытию файла!

386

Отложенные вызовы функций

387

Восстановление после ошибок

388

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

389

Получение списка файлов в каталоге

392

Рекурсивные вызовы функций

394

Рекурсивный вывод содержимого каталога

396

Обработка ошибок в рекурсивной функции

398

Запуск паники

399

Трассировка стека

400

Отложенные вызовы завершаются перед аварийным завершением

400

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

401

Когда стоит паниковать

402

Функция recover

404

Возвращаемое значение recover

405

Восстановление после паники в scanDirectory

407

Возобновление состояния паники

408

Ваш инструментарий Go

410

bad-data.txt

17

оглавление

13

Совместное выполнение
Горутины и каналы
Одновременная работа над одной задачей не всегда быстрее
всего приводит к цели.Некоторые большие задачи можно разбить на
мелкие. Горутины (goroutines) позволяют программам работать над несколькими задачами одновременно. Они координируют свою работу при помощи
каналов, по которым могут отправлять данные друг другу и синхронизировать
выполнение, чтобы одна горутина не опережала другую. Горутины позволяют
использовать всю мощь многопроцессорных компьютеров, чтобы программы
выполнялись как можно быстрее!
Загрузка веб-страниц

414

Многозадачность

416

Конкурентность на базе горутин

417

Использование горутин

418

Использование горутин с функцией responseSize

420

Порядок выполнения горутин

422

Горутины не могут использоваться с возвращаемыми значениями

423

Отправка и получение значений в каналах

425

Каналы и синхронизация горутин

426

Соблюдение синхронизации в горутине

427

Использование каналов в программе для вывода размера веб-страниц

430

Изменение канала для передачи структуры

432

Ваш инструментарий Go

433

Получающая горутина ожидает,
когда другая горутина отправит
значение.

18

оглавление

14

Контроль качества кода
Автоматизация тестирования
А вы уверены, что ваша программа работает правильно?
Точно уверены? Прежде чем рассылать новую версию пользователям, вы,
скорее всего, опробовали ее новые возможности и убедились, что все работает.
Но опробовали ли вы старые возможности? А вдруг что-нибудь сломалось в
процессе доработки? Все старые возможности? Если от этого вопроса вам стало
слегка не по себе, значит, вашим программам необходимо автоматизированное
тестирование. Автоматизированные тесты гарантируют, что компоненты программы работают правильно даже после того, как вы внесете изменения в код.
Средства тестирования Go и команда go test упрощают написание автотестов.
Автотесты обнаруживают ошибки до того,
как их обнаружат пользователи

436

Функция, для которой нужны автотесты

437

Мы внесли ошибку!

439

Написание тестов

440

Выполнение тестов командой «go test»

441

Тестирование возвращаемых значений

442

Метод «Errorf» и подробные сообщения о непрохождении тестов

444

«Вспомогательные» тестовые функции

445

Прохождение тестов

446

Разработка через тестирование

447

Еще одна ошибка

448

Выполнение определенных наборов тестов

451

Табличные тесты

452

Табличные тесты (продолжение)

453

Решение проблемы с паникой при помощи тестов

454

Ваш инструментарий Go

456

Проходит.

P

Ошибка!

O

For []slice{"apple", "orange", "pear"}, JoinWithCommas
should return "apple, orange, and pear".
For []slice{"apple", "orange"}, JoinWithCommas should
return "apple and orange".
19

оглавление

15

Запросы и ответы
Веб-приложения
Мы живем в XXI веке. Пользователям нужны веб-приложения.
Go поможет и в этом! Стандартная библиотека Go включает пакеты, при помощи
которых можно размещать собственные веб-приложения и делать их доступными
для любого веб-браузера. Последние две главы этой книги будут посвящены построению веб-приложений. Первое, что необходимо веб-приложению — способность отвечать на запросы, отправляемые браузером. В этой главе вы узнаете, как
реализовать эту функцию с использованием пакета net/http.

Пользователь вводит URL-адрес
в браузере.

Написание веб-приложений на языке Go

460

Браузеры, запросы, серверы и ответы

461

Простое веб-приложение

462

Ваш компьютер общается сам с собой

463

Простое веб-приложение: шаг за шагом

464

Пути к ресурсам

466

Разные ответы для разных путей к ресурсам

467

Функции первого класса

469

Передача функций другим функциям

470

Функции как типы

470

Что дальше

474

Ваш инструментарий Go

475

Браузер отправляет
запрос серверу.
example.com:
GET "/hello"

Сервер передает
запрос программе.
GET "/hello"

example.com
Hello, web!

Браузер выводит ответ.

20

Сервер отправляет
ответ браузеру.

Программа Go
Hello, web!

Программа генерирует
соответствующий ответ.

оглавление

16

Пример для подражания
Шаблон HTML
Веб-приложение должно выдавать ответ с разметкой HTML,
а не с простым текстом.Для сообщений электронной почты и постов в
соцсетях достаточно простого текста. Тем не менее ваши страницы должны
быть отформатированы с выделением заголовков и разбиением на абзацы.
На них должны быть формы, в которых пользователь сможет ввести данные
для приложения. Для решения таких задач вам понадобится разметка HTML.
Рано или поздно в разметку HTML потребуется вставлять данные. Для этого в Go
существует пакет html/template — мощный инструмент для вставки данных
в HTML-ответы приложения. Шаблоны играют ключевую роль при построении
более масштабных и качественных веб-приложений, и в последней главе книги
мы научим вас ими пользоваться!
Гостевая книга

480

Функции обработки запроса и проверки ошибок

481

Создание каталога проекта и пробный запуск

482

Создание списка записей в HTML

483

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

484

Пакет «text/template»

485

Использование интерфейса io.Writer с методом шаблона Execute

486

ResponseWriter и os.Stdout поддерживают io.Writer

487

Вставка данных в шаблоны при помощи действий

488

Необязательные действия "if" в шаблонах

489

Повторение частей шаблонов в действиях «range»

490

Вставка полей структуры в шаблон

491

Чтение сегмента записей из файла

492

Структура для хранения записей и количества записей

494

Обновление шаблона для включения записей

495

Ввод данных в формах HTML

498

Включение формы HTML в ответ

499

Запросы на отправку данных формы

500

Путь и метод HTTP для отправки данных формы

501

Получение значений полей формы из запроса

502

Сохранение данных формы

504

Перенаправления HTTP

506

Полный код приложения

508

Ваш инструментарий Go

511

21

оглавление

Функция os.OpenFile
Приложение A. Открытие файлов
Некоторые программы не только читают данные, но и записывают их в файлы.Когда в этой книге мы собирались поработать с файлами, приходилось создавать их в текстовом редакторе, чтобы программа могла
прочитать данные. Но некоторые программы генерируют данные, и когда это
происходит, программа должна иметь возможность записать данные в файл.
Функция os.OpenFile уже использовалась для открытия файла для записи. Но у

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

На этот раз
новый текст
присоединя­
ется в конец
файла.

Как работает os.OpenFile

516

Передача констант флагов функции os.OpenFile

517

Двоичная запись

519

Побитовые операторы

519

Побитовый оператор И

520

Побитовый оператор ИЛИ

521

Побитовый оператор ИЛИ и константы пакета «os»

522

Решение проблемы с параметрами os.OpenFile

523

Разрешения доступа к файлам в стиле Unix

524

Представление разрешений с типом os.FileMode

525

Восьмеричная система счисления

526

Преобразование восьмеричных значений в FileMode

527

Подробный анализ вызова os.OpenFile

528

Aardvarks are...
amazing!

aardvark.txt

22

оглавление

Еще шесть тем
Приложение Б. Напоследок
Позади большой путь, и книга почти подошла к концу.Мы будем скучать, но, пожалуй, было бы неправильно отпускать вас в свободное
плавание без небольшой дополнительной подготовки. Мы приберегли шесть
важных тем для этого приложения.
№ 1. Команды инициализации для «if»

530

№ 2. Команда switch

532

№ 3. Другие базовые типы

533

№ 4. О рунах

533

№ 5. Буферизованные каналы

537

№ 6. Дополнительные ресурсы

540

Команда
инициализации.

Все символы
имеют печатное
представление.

0:
1:
2:
3:
4:
0:
2:
4:
6:
8:

A
B
C
D
E
Б
Г
Д
Ж
И

Условие.

if count := 5; count > 4 {
fmt.Println("count is", count)
}

"d"
"c"
"b"
"a"

Отправка значения при за­
полненном буфере приводит
к блокировке отправляющей
горутины.

Другие отправляемые зна­
чения добавляются в буфер,
пока он не заполнится.

23

об авторе
Посвящается моей неизменно
терпеливой Кристин

Автор Head First Go

Джей Макгаврен

Джей Макгаврен — автор книг Head First Ruby и Head First Go, опубликованных издательством O’Reilly. Преподает программирование
в компании Treehouse.
Вместе с ним в пригороде Финикса проживают его очаровательная жена
и непостоянное количество детей и собак.
Персональный веб-сайт Джея находится по адресу http://jay.mcgavren.com.

Как работать с этой книгой

Введение
Не могу поверить,
что они включили
такое в книгу о Go.

вопрос:
ответим на важный
В этом разделе мы
игу о Go?»
ючили ТАКОЕ в кн
«Так почему они вкл

дальше 4  25

как работать с этой книгой

Для кого написана эта книга?
Если вы ответите «да» на все следующие вопросы:
1

У вас есть доступ к компьютеру с текстовым редактором?

2

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

3

Вы предпочитаете застольные беседы сухим,
скучным академическим лекциям?

тогда эта книга для вас.

Êîìó ýòà êíèãà íå ïîäîéäåò?
Если вы ответите «да» хотя бы на один из следующих вопросов:
1

У вас нет никакого опыта работы с компьютером?
(Быть экспертом не обязательно, но нужно понимать, что
такое файлы и папки, как открыть приложение-терминал
и как пользоваться простым текстовым редактором.)

2

Вы — опытный разработчик, которому нужен справочник?

3

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

...эта книга не для вас.

[Замечание от отдел
а продаж:
вообще-то эта книга
для любого,
у кого есть деньги.]

26 введение

введение

Мы знаем, о чем вы думаете
«Разве серьезные книги по программированию на Go такие?»
«И почему здесь столько рисунков?»
«Можно ли так чему-нибудь научиться?»

Ваш м
озг счи
тает
что Э
,
ТО ва
жно.

È ìû çíàåì, î ÷åì äóìàåò âàø ìîçã
Мозг жаждет новых впечатлений. Он постоянно ищет, анализирует,
ожидает чего-то необычного. Он так устроен, и это помогает нам
выжить.
Как наш мозг поступает со всеми обычными, повседневными вещами?
Он всеми силами пытается оградиться от них, чтобы они не мешали
его настоящей работе — сохранению того, что действительно важно.
Мозг не считает нужным сохранять скучную информацию. Она не
проходит фильтр, отсекающий «очевидно несущественное».
Но как же мозг узнает, что важно? Представьте, что вы выехали на
прогулку и вдруг прямо перед вами появляется тигр. Что происходит
в вашей голове и теле?
Активизируются нейроны. Вспыхивают эмоции. Происходят химические реакции. И тогда ваш мозг понимает...
Конечно, это важно! Не забывать!
А теперь представьте, что вы находитесь дома или в библиотеке — в теплом, уютном месте, где тигры не водятся. Вы
учитесь — готовитесь к экзамену. Или пытаетесь освоить
сложную техническую тему, на которую вам выделили неделю... максимум десять дней.

Замечательно.
Еще 520 сухих
скучных страниц.

ага­
зг пол ожно
о
м
Ваш
ТО м
то Э
ет, ч
ать.
омин
не зап

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

дальше 4  27

как работать с этой книгой

иться.

т уч
Эта книга для тех, кто хоче

том не забыть.
то-то» понять, а по
«ч
о
эт
о
жн
ну
а
ал
ем? Снач
новейшим исслеКак мы что-то узна
аточно. Согласно
ст
до
не
ов
кт
фа
побольше
обучения, для
гии и психологии
Затолкать в голову
ло
ио
об
йр
не
,
ки
ти
нице. Мы
и когнитивис
остой текст на стра
дованиям в област
о большее, чем пр
о-т
чт
я
тс
уе
еб
тр
а
усвоения материал
ть.
ь ваш мозг работа
знаем, как заставит

st»

пы серии «Head Fir

Основные принци

льно повышает
ный текст, и значите
ыч
об
м
че
е,
чш
лу
ий ). Кр ом е то го,
ка запоминается
да нн ым иссл едов ан
Наглядность. Графи
по
%
89
(до
ии
ац
ри яти я ин фо рм
к которым он отноается на рисунках,
эф фе кти вн ос ть во сп
ещ
зм
ра
т
кс
Те
м.
решения задач,
более понятны
роятнос ть успешного
материал становится
ве
и

е
иц
ан
стр
й
или на соседне
сится, а не под ними
е.
лу, повышается вдво
иа
относящихся к матер
и разговорном
ания показали, что пр
ов
ед
сл
ис
ие
вн
да
Не
ьтатов на итогоь изложения.
ий) улучшение резул
кц
Ра зговорный стил
ле
ых
ьн
ал
рм
фо
читать лекцию.
териала (вмес то
ию, вмес то того чтобы
стиле изложения ма
тор
ис
йте
ва
азы
сск
ательная беседа
стигает 40 %. Ра
ваше внимание: заним
т
вом тес тировании до
че
ле
ив
пр
о
Чт
.
зно
слишком серье
Не относитесь к себе
?
ия
за столом или лекц
, в вашей голове
напрягать изви лины
ете
чн
на
не
вы
ка
чи та те ля . По
он должен решать
Ак ти вн ое уч ас ти е
ресован в результате;
нте
заи
ть
бы
ен
лж
го необходимы
ет. Читатель до
знаниями. А дл я это
ничего не произойд
ми
вы
но
ть
ва
де
ла
ов
лушария мозга
ать выводы и
задействованы оба по
задачи, формулиров
ых
тор
ко
и
ни
ше
ре
в
ные вопросы,
упражнения и каверз
.
и разные чувс тва
каж дому: «Я очень
. Ситуация, знакомая
ля
те
та
чи
ия
ан
им
ие на интересное,
хранение) вн
зг обращает вниман
Привлечение (и со
Мо
е».
иц
ан
стр
ой
рв
й темы не обязано
засыпаю на пе
хочу изучить это, но
сложной техническо
ие
ен
уч
Из
е.
но
ан
ьное, неожид
странное, притягател
быстрее.
ое узнается намного
есн
тер
быть скучным. Ин
значительной мере
ность запоминать в
об
ос
сп
ша
на
что
о,
безразлично. Мы
иям. Известн
инаем то, что нам не
Обращение к эмоц
ом
зап
Мы
я.
ни
ва
жи
чь идет о таких
льного сопере
здесь ни при чем: ре
зависит от эмоциона
нты
ме
ти
сан
т,
Не
м.
-то чувствуе
решении задачи,
тво «Да я крут!» при
запоминаем, когда что
вс
чу
и
ес
тер
ин
,
во
збираетесь в теме
ние, любопытст
вы понимаете, что ра
эмоциях, как удивле
гда
ко
и
ил

й
но
считают слож
которую окружающие
ела.
б из технического отд
Бо
ка
ай
лучше, чем всезн

28 введение

введение

Ìåòàïîçíàíèå: íàóêà î ìûøëåíèè
Если вы действительно хотите быстрее и глубже усваивать новые
знания — задумайтесь над тем, как вы думаете. Учитесь учиться.
Мало кто из нас изучает теорию метапознания во время учебы. Нам
положено учиться, но нас редко этому учат.

Как бы теперь
заставить мой мозг
все это запомнить…

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

Как же УБЕДИТЬ мозг, что программирование не менее
важно, чем голодный тигр?
Есть способ медленный и скучный, а есть быстрый и эффективный. Первый основан на тупом повторении. Всем известно, что даже
самую скучную информацию можно запомнить, если повторять ее снова и снова.
При достаточном количестве повторений ваш мозг прикидывает: «Вроде бы
несущественно, но раз одно и то же повторяется столько раз... Ладно, уговорил».
Быстрый способ основан на повышении активности мозга и особенно
на сочетании разных ее видов. Доказано, что все факторы, перечисленные на
предыдущей странице, помогают вашему мозгу работать на вас. Например, исследования показали, что размещение слов внутри рисунков (а не в подписях,
в основном тексте и т. д.) заставляет мозг анализировать связи между текстом
и графикой, а это приводит к активизации большего количества нейронов.
Больше нейронов — выше вероятность того, что информация будет сочтена
важной и достойной запоминания.
Разговорный стиль тоже важен: обычно люди проявляют больше внимания,
когда они участвуют в разговоре, так как им приходится следить за ходом беседы
и высказывать свое мнение. Причем мозг совершенно не интересует, что вы
«разговариваете» с книгой! С другой стороны, если текст сух и формален, то
мозг чувствует то же, что чувствуете вы на скучной лекции в роли пассивного
участника. Его клонит в сон.
Но рисунки и разговорный стиль — это только начало.

дальше 4  29

как использовать эту книгу

Вот что сделали МЫ
Мы использовали рисунки, потому что мозг лучше приспособлен для восприятия
графики, чем текста. С точки зрения мозга рисунок стоит 1024 слов. А когда текст
комбинируется с графикой, мы внедряем текст прямо в рисунки, потому что мозг при
этом работает эффективнее.
Мы используем избыточность: повторяем одно и то же несколько раз, применяя разные
средства передачи информации, обращаемся к разным чувствам — и все для повышения
вероятности того, что материал будет закодирован в нескольких областях вашего мозга.
Мы используем концепции и рисунки несколько неожиданным образом, потому что
мозг лучше воспринимает новую информацию. Кроме того, рисунки и идеи обычно
имеют эмоциональное содержание, потому что мозг обращает внимание на биохимию
эмоций. То, что заставляет нас чувствовать, лучше запоминается — будь то шутка, удивление или интерес.
Мы используем разговорный стиль, потому что мозг лучше воспринимаетинформацию,
когда вы участвуете в разговоре, а не пассивно слушаете лекцию. Это происходит и при
чтении.
В книгу включены многочисленные упражнения, потому что мозг лучше запоминает,
когда вы что-то делаете. Мы постарались сделать их непростыми, но интересными — то,
что предпочитает большинство читателей.
Мы совместили несколько стилей обучения, потому что одни читатели предпочитают пошаговые описания, другие стремятся сначала представить «общую картину», а третьим
хватает фрагмента кода. Независимо от ваших личных предпочтений полезно видеть
несколько вариантов представления одного материала.
Мы постарались задействовать оба полушария вашего мозга; это повышает вероятность
усвоения материала. Пока одна сторона мозга работает, другая часто имеет возможность отдохнуть; это повышает эффективность обучения в течение продолжительного
времени.
А еще в книгу включены истории и упражнения, отражающие другие точки зрения. Мозг
глубже усваивает информацию, когда ему приходится оценивать и выносить суждения.
В книге часто встречаются вопросы, на которые не всегда можно дать простой ответ,
потому что мозг быстрее учится и запоминает, когда ему приходится что-то делать.
Невозможно накачать мышцы, наблюдая за тем, как занимаются другие. Однако мы
позаботились о том, чтобы усилия читателей были приложены в верном направлении.
Вам не придется ломать голову над невразумительными примерами или разбираться в
сложном, перенасыщенном техническим жаргоном или слишком лаконичном тексте.
В историях, примерах, на картинках используются люди — потому что вы тоже человек.
И ваш мозг обращает на людей больше внимания, чем на неодушевленные предметы.

30 введение

введение

×òî ìîæåòå ñäåëàòü ÂÛ, ÷òîáû
çàñòàâèòü ñâîé ìîçã ïîâèíîâàòüñÿ
Мы свое дело сделали. Остальное за вами. Эти советы станут
отправной точкой; прислушайтесь к своему мозгу и определите, что вам подходит, а что не подходит. Пробуйте новое.

Вырежьте и прик
ре­
пите на холодиль
ник.

1

Не торопитесь. Чем больше вы поймете, тем
меньше придется запоминать.

Просто читать недостаточно. Когда книга задает вам вопрос, не переходите к ответу. Представьте, что кто-то действительно задает вам
вопрос. Чем глубже ваш мозг будет мыслить,
тем скорее вы поймете и запомните материал.
2

3 Читайте врезки.
Это значит: читайте все. Врезки — часть основного материала! Не пропускайте их.
Не читайте другие книги после этой перед сном.

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

Говорите вслух.

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

Пейте воду. И побольше.

Мозг лучше всего работает в условиях высокой
влажности. Дегидратация (которая может наступить еще до того, как вы почувствуете жажду)
снижает когнитивные функции.
7

Выполняйте упражнения, делайте заметки.

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

4

6

Прислушивайтесь к своему мозгу.

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

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

Пишите побольше кода!

Освоить программирование на Go можно только одним способом: писать побольше кода.
Именно этим мы и будем заниматься в книге.
Программирование — искусство, и добиться
мастерства в нем можно только практикой. Для
этого у вас будут все возможности: в каждой
главе приведены упражнения, в которых вам
придется решать задачи. Не пропускайте их —
работа над упражнениями является важной частью процесса обучения. К каждому упражнению
приводится решение — не бойтесь заглянуть в
него, если окажетесь в тупике! (Споткнуться
можно даже о маленький камешек.) По крайней
мере постарайтесь решить задачу, прежде чем
заглядывать в решение. Обязательно добейтесь
того, чтобы ваше решение заработало, прежде
чем переходить к следующей части книги.
дальше 4  31

как работать с этой книгой

Примите к сведению
Это учебник, а не справочник. Мы намеренно убрали из книги все, что могло бы помешать изучению материала, над которым вы работаете. И при первом чтении книги начинать следует с
самого начала, потому что книга предполагает наличие у читателя определенных знаний и опыта.
Предполагается, что у вас уже есть опыт программирования на другом языке.
Большинство разработчиков приходит в Go после знакомства с другим языком программирования (причем нередко они спасаются бегством от него). Мы рассмотрим основы в достаточной
степени, чтобы материал был понятен даже начинающему, но не будем подробно рассказывать,
что такое переменная и как работает конструкция if. Читателю будет проще, если у него есть
хотя бы небольшой опыт программирования.
Мы не описываем каждый существующий тип, функцию и пакет.
Go поставляется с огромным количеством встроенных пакетов. Безусловно, все они представляют интерес, но мы не смогли бы описать их даже в книге вдвое большего размера. Мы сосредоточились на основных типах и функциях, которые особенно важны для вас, то есть для новичка.
Мы стремились к тому, чтобы вы глубоко поняли эти темы и четко представляли, когда и как
их следует применять.
Упражнения ОБЯЗАТЕЛЬНЫ.
Упражнения являются частью основного материала книги. Одни упражнения способствуют
запоминанию материала, другие помогают лучше понять его, третьи ориентированы на его
практическое применение. Не пропускайте упражнения.
Повторение применяется намеренно.
У книг этой серии есть одна принципиальная особенность: мы хотим, чтобы вы действительно
хорошо усвоили материал. И чтобы вы запомнили все, что узнали. Большинство справочников
не ставит своей целью успешное запоминание, но это не справочник, а учебник, поэтому некоторые концепции излагаются в книге по нескольку раз.
Мы постарались сделать примеры по возможности компактными.
Никому не захочется продираться через 200 строк кода в поисках двух строк, которые важно
понять. Большинство примеров в книге приводится в минимальном возможном контексте,
чтобы та часть, которую вы изучаете, была по возможности простой и ясной. Не стоит ожидать, что код примеров защищен от ошибок или хотя бы полон. Этими аспектами займетесь
вы после прочтения книги. Книга написана именно для обучения, поэтому приводимый код не
всегда является полнофункциональным.
Все файлы примеров доступны в интернете. Их можно загрузить по адресу http://headfirstgo.com/.
32 введение

введение

Благодарности
Основателям серии
Огромное спасибо основателям серии Head First Кэти Сьерра и Берту Бэйтсу.
Эта серия книг понравилась мне еще более десяти лет назад, когда я впервые
столкнулся с ней, но я никогда не думал, что буду писать для нее. Спасибо за
создание этого замечательного стиля обучения!
Издательству O’Reilly
Спасибо всем сотрудникам O’Reilly, которые принимали участие в работе над
книгой, особенно редактору Джеффу Блейлю, а также Кристер Браун, Рэчел
Монахан и другим участникам производственной группы.
Научным редакторам
Все мы совершаем ошибки. К счастью, научные редакторы Тим Хекман, Эдвард
Ю Шун Вон и Стефан Покман постарались отыскать все мои ошибки. Вы даже
не представляете, сколько проблем они нашли, потому что я быстро уничтожил
все доказательства. Но их помошь и обратная связь были безусловно необходимы,
и я им вечно благодарен!
И многим другим
Спасибо Лео Ричардсону за дополнительную корректуру.
И пожалуй, самое важное: спасибо Кристин, Кортни, Брайану, Ленни и Джереми за их терпение и поддержку (уже за две книги!).

дальше 4  33

1 Знакомство с Go

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

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

о языке go

На старт... внимание... Go!
В 2007 году у поисковой системы Google возникли проблемы.
Разработчикам приходилось заниматься сопровождением
кодовой базы, состоящей из миллионов строк кода. Прежде
чем тестировать новые изменения, они должны были откомпилировать код в исполняемую форму, а процесс занимал в
лучшем случае около часа. Не стоит и говорить, что это плохо
сказывалось на эффективности разработки.
Так инженеры Google Роберт Гризмер (Robert Griesemer), Роб
Пайк (Rob Pike) и Кен Томпсон (Ken Thompson) наметили
следующие цели для создания нового языка.


Быстрая компиляция.



Компактный код.



Автоматическое освобождение неиспользуемой памяти
(уборка мусора).



Простота написания программ, способных одновременно
выполнять несколько операций (параллелизм).



Качественная поддержка многоядерных процессоров.

Через пару лет работы Google создала Go:
язык, на котором код пишется быстро, а полученные программы легко компилируются
и выполняются. В 2009 году проект перешел
на лицензию с открытым исходным кодом.
Сейчас этот язык могут свободно использовать все желающие. И поверьте, он того
стоит! Go стремительно набирает популярность из-за своей простоты и мощи.
Если вы пишете программы командной
строки, Go сможет строить исполняемые
файлы для Windows, macOS и Linux из
одного исходного кода. Если вы пишете
веб-сервер, Go поможет организовать обслуживание одновременного подключения
многих пользователей. Словом, что бы вы
ни писали, язык Go упростит сопровождение и расширение вашего кода.
Готовы узнать больше? Тогда вперед!

36

глава 1

знакомство с go

Интерактивная среда Go Playground
Чтобы поэкспериментировать с Go, проще всего открыть
страницу https://play.golang.org в браузере. Команда разработчиков Go создала простой редактор, в котором вы можете
ввести код и выполнить его на внешнем сервере. Результат
выводится прямо в браузере.
(Конечно, этот способ работает только при наличии хорошего
подключения к интернету. Если этот вариант вам не подходит,
на с. 60 рассказано о том, как загрузить и запустить компилятор
Go прямо на вашем компьютере. В этом случае приведенные
примеры следует запускать в компиляторе.)
Давайте опробуем Go в деле!

Задание!
1

 ткройте страницу https://play.golang.org в браузере. (Не беспокойтесь, если то, что
О
вы увидите, отличается от приведенного скриншота; это говорит лишь о том, что
с момента издания книги сайт был улучшен!)

2

Очистите область редактирования и введите следующий код:

p ackage main
import "fmt"

все это
Спокойно! Мы объясним, что
це!
ани
стр
значит, на следующей

func main() {
fmt.Println("Hello, Go!")
}
3

Щелкните на кнопке Format. Ваш код автоматически переформатируется в соответствии с общепринятыми соглашениями Go.

4

Щелкните на кнопке Run.

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

Результат.

Hello, Go!
дальше 4   37

структура программы

Что это все означает?
Вы только что выполнили свою первую программу на языке Go! А теперь рассмотрим код и разберемся,
что же это все означает...
Каждый файл Go начинается с директивы package. Пакет представляет собой набор блоков кода,
выполняющих похожие операции — например, форматирование строк или построение графических
изображений. Директива package задает имя пакета, частью которого станет код этого файла. В нашем
случае используется специальный пакет main; это необходимо для того, чтобы код можно было запускать
напрямую (чаще всего в терминале).
Файлы Go почти всегда содержат
одну или несколько директив
import. Каждый файл должен
импортировать другие пакеты,
чтобы использовать код, содержащийся в этих пакетах. Но если бы
в оперативную память загружался
весь код Go, который только найдется на компьютере, программы
стали бы слишком громоздкими
и медленными, поэтому вы указываете только те пакеты, которые вам действительно нужны, —
то есть импортируете эти пакеты.

остальной код
Эта строка сообщает, что
пакету «main».
этого файла относится к

package main
import "fmt"

ользовать код
Означает, что мы будем исп
пакета «fmt».
форматирования текста из

Функция «main» играет особую роль — именно
она выполняется при запуске программы.
func main() {
fmt.Println("Hello, Go!")
}
Эта строка выводит
сообщение «Hello, Go!»
Для этого она вызывав терминале (или браует функцию «Println»
зере, если вы используеиз пакета «fmt».
те среду Go Playground).

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

Структура типичного файла Go
Вы быстро привыкнете к тому, что эти
три части (именно в таком порядке)
встречаются практически во всех файлах Go, с которыми вы будете работать:
1.

Директива package.

2.

Директива import.

3.

Собственно код программы.

РАССЛАБЬТЕ

С Ь Если что-то показалось непонятным —
не огорчайтесь!

Все эти темы будут более подробно рассмотрены на нескольких ближайших
страницах.

Директива package. package main
Директива import. import "fmt"
Собственно код func main() {
fmt.Println("Hello, Go!")
программы.
}

Поговорка гласит: «Всему свое место и все хорошо на своем месте». Go — чрезвычайно последовательный
язык. И это хорошо: часто вы просто знаете, где в вашем проекте находится тот или иной код, и вам даже
не приходится над этим задумываться!
38

глава 1

знакомство с go

часто

В:

Задаваемые
вопросы

Другие языки программирования требуют, чтобы каждая команда завершалась символом «точка
с запятой» (;). А в Go есть такое требование?

О:
В:
О:

В Go команды можно разделять символом ";", но это необязательно (и более того, обычно считается
нежелательным).
Что это за кнопка Format? Почему мы нажимали ее перед запуском своего кода?

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

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

Playground.
Имя файла, используемое Go
Номер строки, в которой
произошла ошибка.

Строка

1
2
3
4
5
6
7

package main
import "fmt"

Допустим,
вы забыли
поставить
здесь круглые
скобки...

func main() {
fmt.Println "Hello, Go!"
}

Описание ошибки.

prog.go:6:14: syntax error: unexpected literal "Hello, Go!" at end of statement
Позиция в строке, в которой произошла ошибка.

Компилятор сообщает, в каком исходном файле и в какой строке следует искать ошибку. (Среда Go
Playground сохраняет ваш код во временном файле, прежде чем выполнять его; отсюда имя файла
prog.go.) Далее следует описание ошибки. В нашем случае из-за отсутствия круглых скобок компилятор
не осознает, что мы пытаемся вызвать функцию Println, и поэтому не понимает, что делает сообщение
"Hello, Go" в конце строки 6.
дальше 4   39

вызов функций

Сломай и изучи!

Чтобы получить представление о правилах, которые должны соблюдаться в программах Go,
попробуем намеренно сломать нашу программу разными способами. Возьмите этот пример
кода, внесите одно из указанных изменений и запустите программу, затем отмените изменение
и переходите к следующему. Посмотрите, что из этого выйдет!
package main
import "fmt"

Попробуйте сломать наш пример кода
и посмотрите, что получится!

func main() {
fmt.Println("Hello, Go!")
}
Если...

...программа не будет работать, потому что...

Удалить директиву package... package main

Каждый файл Go должен начинаться с директивы
package

Удалить инструкцию
import...

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

import "fmt"

Импортировать второй
(неиспользуемый) пакет...

import "fmt"
import "strings"

Файлы Go должны импортировать только те пакеты,
которые в них используются. (Это ускоряет компиляцию кода!)

Переименовать функцию
main...
func mainhello

Компилятор Go ищет функцию с именем main, чтобы
выполнить ее при запуске программы

Преобразовать имя Println к нижнему регистру...

В Go учитывается регистр символов. fmt.Println —
действительное имя, но имени fmt.println не
существует

fmt.Pprintln("Hello, Go!")

Удалить имя пакета перед Println...
fmt.Println("Hello, Go!")

Функция Println не принадлежит пакету main, поэтому перед вызовом функции необходимо указать
имя пакета

Давайте вместе проведем первый эксперимент.
Удалите
директиву
package...
... и получите
сообщение
об ошибке!

40

глава 1

import "fmt"
func main() {
fmt.Println("Hello, Go!")
}

can't load package: package main:
prog.go:1:1: expected 'package', found 'import'

знакомство с go

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

package main

Вскоре мы
объясним эту
часть!

func main() {
fmt.Println("Hello, Go!")
}

Имя функции.

fmt.Println()

import "fmt"

Вызов
функции
Println.

Круглые скобки.

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

ются
В круглых скобках перечисля
ятыми.
зап
ые
енн
аргументы, раздел

fmt.Println("First argument", "Second argument")
Результат.

First argument Second argument

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

Функция Println
При помощи функции Println можно узнать, как идет выполнение программы. Все аргументы, переданные этой функции,
выводятся в терминале (а их значения разделяются пробелами).
После вывода всех аргументов Println переходит на следующую строку в терминале. (Отсюда суффикс «ln» — сокращение
от «line» — в конце имени.)
fmt.Println("First argument", "Second argument")
fmt.Println("Another line")
Результат.

First argument Second argument
Another line
дальше 4   41

функции и пакеты

Использование функций из других пакетов
Весь код нашей первой программы является частью пакета main, но функция Println принадлежит пакету fmt (сокращение от «format»). Чтобы в программе можно было вызвать
функцию Println, необходимо сначала импортировать пакет, содержащий эту функцию.
package main
import "fmt"

ортировать,
Пакет «fmt» необходимо имп
ю Println.
чтобы вызвать его функци

func main() {
fmt.Println("Hello, Go!")
}
Сообщает, что вызываемая функция
является частью пакета «fmt».

После того как пакет будет импортирован, вы сможете вызывать функции из этого пакета.
Для этого укажите имя пакета, поставьте точку (.) и введите имя нужной функции.
Имя пакета.

Имя функции.

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

инструкции «impor t»
Альтернативный формат
сразу несколько пакетов.
позволяет импортировать
import (
Импортируем пакет «math», чтобы
использовать функцию math.Floor.
"math"
"strings"
Импортируем пакет «strings», чтобы
)
использовать функцию strings.Title.

package main

Вызываем функцию Floor func main() {
math.Floor(2.75)
из пакета «math».
strings.Title("head first go")
}
Программа ничего не выводит.
Вызываем функцию Title
(Сейчас мы объясним почему!)
из пакета «strings».

После того как пакеты math и strings будут импортированы, вы сможете вызвать функцию
Floor из пакета math в форме math.Floor, а также функцию Title из пакета strings
в форме strings.Title.
Вероятно, вы заметили, что несмотря на включение двух вызовов функций в код программы,
этот пример не выводит никакого результата. Сейчас мы покажем, как решить эту проблему.
42

глава 1

знакомство с go

Возвращаемые значения функций
В приведенном примере мы вызываем
функции math.Floor и strings.Title,
но эти функции ничего не выводят:

package main
import (
"math"
"strings"
)
func main() {
math.Floor(2.75)
strings.Title("head first go")
}
Программа ничего
не выводит!

При вызове функции fmt.Println вам не нужно обмениваться с ней дополнительной информацией. Вы
передаете Println одно или несколько выводимых значений и ожидаете вывод. Но иногда программа
должна вызвать функцию и получить от нее дополнительные данные. Из-за этого в большинстве языков программирования функции имеют возвращаемые значения, которые вычисляются функциями
и возвращаются на сторону вызова.
Функции math.Floor и strings.Title относятся к числу функций, имеющих возвращаемое значение.
Функция math.Floor получает число с плавающей точкой, округляет его до ближайшего меньшего целого и возвращает полученное число. А функция strings.Title получает строку, преобразует первую
букву каждого слова к верхнему регистру и возвращает полученную строку.
Чтобы увидеть результаты этих вызовов функций, необходимо взять возвращаемые значения и передать их fmt.Println:
package main

Функция fmt.Println
вызывается для
возвращаемого
значения функции
math.Floor.

import (
"fmt"
"math"
"strings"
)

Также импортируется пакет «fmt».

его
Получает число, округляет
равозв
и
у
в меньшую сторон
Результат.
щает полученное значение.

func main() {
fmt.Println(math.Floor(2.75))
2
fmt.Println(strings.Title("head first go"))
Head First Go
}
Получает строку и возвращает нову
ю
Функция fmt.Println вызыстроку, в которой все слова начинамого
ащае
вается для возвр
ются с буквы верхнего регистра.
значения функции strings.Title.

После внесения этого изменения возвращаемые значения выводятся в терминале, где вы сможете ознакомиться с результатами.
дальше 4   43

структура программы

У бассейна
Выловите из бассейна фрагменты кода и разместите их в пустых строках кода.
Каждый фрагмент может использоваться только один раз; использовать
все фрагменты необязательно. Ваша задача: построить код, который
будет успешно выполняться и выведет показанный результат.
Мы вставили первый
фрагмент за вас!

package main
import (
)

}

main() {
fmt.Println(

Результат.

)

Cannonball!!!!

Примечание: каждый
предмет из бассейна
может использоваться
только один раз!

main
Println
"fmt"

"Cannonball!!!!"

"math"
func

Ответ на с. 63.

44

глава 1

знакомство с go

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

Вставьте сюда
свой код!

package main
import "fmt"

А еще лучше — введите эту программу
в среде Go Playground, а потом вставляйте фрагменты по одному и наблюдайте за
тем, что они делают!

func main() {
fmt.Println(
}

)

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

Закрывающая
двойная кавычка

"Hello, Go!"

Результат.

Hello, Go!

Некоторые управляющие символы, которые неудобно вводить с клавиатуры
(символы новой строки, табуляции и т. д.), внутри строк могут представляться в виде служебных последовательностей: символа «обратный слеш»,
за которым следует другой символ (или символы).
Новая строка
внутри строки.

"Hello,\nGo!"

Результат.

Hello,
Go!

Значение

\n

Символ новой строки

\t

Символ табуляции

\\

Обратный слеш

\"

"Hello,\tGo!"

Hello,

"Quotes: \"\""

Quotes: ""

"Backslash: \\"

Последовательность
sequence

Двойная кавычка

Go!

Backslash: \
дальше 4   45

типы go
package main

И снова наш шаблон...
Вставьте
import "fmt"
сюда свой
код!
func main() {
fmt.Println(
)
}

Руны
Если строки обычно используются для представления последовательностей символов, то руны
в языке Go представляют отдельные символы.
Строковые литералы заключаются в двойные
кавычки ("), а рунные литералы записываются
в одиночных кавычках (').

В программах Go могут использоваться практически любые символы
любых мировых языков, потому что в Go для хранения рун используется стандарт Юникод. Руны хранятся в виде числовых кодов,
а не в виде символов; если передать руну функции fmt.Println, то
выведется числовой код, а не исходный символ.
'A'

'Җ'

'B'

65

Результат.

1174

66

Выводит код символа в Юникоде.

В рунных литералах (как и в строковых) можно использовать служебные последовательности для представления символов, которые
неудобно вводить с клавиатуры для включения в программу:
'\t'

9

'\\'

'\n'

10

Логические значения
Логические величины принимают всего два возможных значения:
true или false. Они особенно удобны в условных командах, в которых выполнение блока кода зависит от того, истинно или ложно
некоторое условие. (Условные команды рассматриваются в следующей главе.)
true

true

46

глава 1

false

false

92

знакомство с go
package main

И снова наш шаблон...
Вставьте
import "fmt"
сюда свой
код!
func main() {
fmt.Println(
)
}

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

Целое число.

42

42

Число с плавающей точкой.

3.1415

Результат.

3.1415

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

Математические операции и сравнения
Основные математические операторы Go работают так же,
как и в большинстве других языков. Оператор + выполняет
сложение, оператор - выполняет вычитание, оператор * —
умножение, и оператор / — деление.
1 + 2

5.4 - 2.2

3

3 * 4

3.2

7.5 / 5

12

1.5

При помощи операторов < и > можно сравнить два значения и проверить,
какое из них больше другого. Оператор == (два знака равенства) проверяет, что два значения равны, а оператор != проверяет, что два значения
не равны, = проверяет, что второе значение больше или равно первому.
Результатом сравнения является логическое значение (true или false).
4 < 6

2+2 == 5

4 6

2+2 != 5

4 >= 4

false

true

true
дальше 4   47

типы go

Типы
В предыдущем примере кода использовалась функция math.Floor, округляющая число
с плавающей точкой с уменьшением, и функция strings.Title, преобразующая первую
букву каждого слова в строке к верхнему регистру. Логично ожидать, что в аргументе
функции Floor передается число, а в аргументе функции Title передается строка.
Но что произойдет, если передать функции Floor строку, а функции Title число?
package main
import (
"fmt"
"math"
"strings"
)

Обычно получает число
с плавающей точкой!

func main() {
fmt.Println(math.Floor("head first go"))
fmt.Println(strings.Title(2.75))
}
Обычно получает строку!
Ошибки.

cannot use "head first go" (type string) as type float64 in argument to math.Floor
cannot use 2.75 (type float64) as type string in argument to strings.Title
Go выводит два сообщения об ошибках — по одному для каждого вызова
функции, а программа даже не запускается!
Объекты в окружающем мире часто можно разделить на типы в зависимости
от того, для чего они используются. Машину или грузовик нельзя съесть на
завтрак, а на омлете или чашке с кукурузными хлопьями не поедешь на работу — они предназначены для другого.
Аналогичным образом значения в Go делятся на разные типы, которые
определяют, для чего они могут использоваться. Целые числа могут использоваться в математических операциях, а в строке не могут. В строках можно
преобразовать регистр символов, а в числах нельзя... И так далее.
В языке Go используется статическая типизация — это означает, что типы
всех значений известны еще до запуска программы. Функции ожидают, что их
аргументы относятся к конкретным типам, а их возвращаемые значения тоже
имеют типы (которые могут совпадать или не совпадать с типами аргументов).
Если случайно использовать неправильный тип значения в неподходящем
месте, компилятор Go выдаст сообщение об ошибке. И это очень хорошо: вы
узнаете о существовании проблемы до того, как о ней узнают пользователи!
48

глава 1

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

знакомство с go

Типы (продолжение)
Чтобы узнать тип любого значения, передайте его функции
TypeOf из пакета reflect. Давайте узнаем типы некоторых
значений, которые уже встречались в примерах программ:
package main
import (
"fmt"
"reflect"
)

Импортируем пакет
«reflect», чтобы использовать его функцию TypeOf.

Возвращает тип
своего аргумента.

func main() {
fmt.Println(reflect.TypeOf(42))
fmt.Println(reflect.TypeOf(3.1415))
fmt.Println(reflect.TypeOf(true))
fmt.Println(reflect.TypeOf("Hello, Go!"))
}

Результат.

int
float64
bool
string

Эти типы предназначены для следующих целей:
Тип

Описание

int

Целое число (не имеющее дробной части)

float64

Число с плавающей точкой. Тип используется для хранения чисел, имеющих дробную часть. (Для хранения числа используются 64 бита данных, отсюда суффикс 64
в имени. Значения типа float64 обеспечивают очень высокую, хотя и не бесконечную
точность.)
Логическое значение (true или false)

bool

string

Упражнение

Соедините каждый фрагмент кода с правильным типом.
Некоторым типам могут соответствовать несколько фрагментов.
reflect.TypeOf(25)

int

reflect.TypeOf(5.2)

float64

reflect.TypeOf(false)

bool

reflect.TypeOf("hello")

string

reflect.TypeOf(true)
reflect.TypeOf(1)

reflect.TypeOf(1.0)

Ответы на с. 63.

Строка — последовательность данных, которые обычно представляют символы текста

дальше 4   49

переменные go

Объявление переменных
В языке Go переменная представляет собой область памяти, в которой хранится
значение. Чтобы к переменной можно было обращаться по имени, используйте объявление переменной. Объявление состоит из ключевого слова var, за
которым следует имя и тип значений, которые будут храниться в переменной.
Ключевое слово «var».

Имя переменной.

Тип значения, которое будет храниться
var quantity
в переменной.
var quantity int
Несколько переменных одного типа
var length, width float64
можно
объявить одновременно.
var customerName string

Имя переменной.

int

После того как переменная будет объявлена, ей можно будет присвоить любое
значение этого типа оператором = (один знак равенства):
quantity = 2
customerName = "Damon Cole"

В одной команде можно присвоить значения сразу нескольким переменным.
Для этого перечислите имена переменных слева от = и такое же количество
значений в правой части, разделяя их запятыми.
length, width = 1.2, 2.4

Одновременное присваивание значений
нескольким переменным.

После того как переменной будет присвоено значение, вы сможете использовать
ее в любом контексте, где может использоваться исходное значение:
package main
import "fmt"
func main() {
Объявление var quantity int
переменных. var length, width float64
var customerName string

Присваивание
значений переменным.

quantity = 4
length, width = 1.2, 2.4
customerName = "Damon Cole"

fmt.Println(customerName)
Использование
fmt.Println("has ordered", quantity, "sheets")
переменных. fmt.Println("each with an area of")
fmt.Println(length*width, "square meters")
}

50

глава 1

Damon Cole
has ordered 4 sheets
each with an area of
2.88 square meters

Tип.

знакомство с go

Объявление переменных (продолжение)
Если значение переменной известно заранее, можно объявить переменную и присвоить ей значение в одной строке:
ние в конец команды.
Просто добавьте присваива
Объявление переменных И присваивание значений

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

var quantity int = 4
var length, width float64 = 1.2, 2.4
var customerName string = "Damon Cole"

Существующим переменным можно присваивать новые значения, но эти значения
должны относиться к тому же типу. Статическая типизация в Go гарантирует, что
переменной не будет случайно присвоено значение неподходящего типа.
Типы присваиваемых зна- quantity = "Damon Cole"
чений не соответствуют customerName = 4
объявленным типам!

Ошибки.

cannot use "Damon Cole" (type string) as type int in assignment
cannot use 4 (type int) as type string in assignment

Если значение переменной присваивается одновременно с ее объявлением, тип
переменной в объявлении обычно не
указывают. Тип значения, присвоенного переменной, будет считаться типом
этой переменной.

Тип переменной не указан.
var quantity = 4
var length, width = 1.2, 2.4
var customerName = "Damon Cole"
int
fmt.Println(reflect.TypeOf(quantity))
float64
fmt.Println(reflect.TypeOf(length))
float64
fmt.Println(reflect.TypeOf(width))
fmt.Println(reflect.TypeOf(customerName))
string

Нулевые значения
Если переменная объявляется без присваивания значения, то она будет содержать
нулевое значение для этого типа. Для числовых типов нулевое значение равно 0:
var myInt int
var myFloat float64
fmt.Println(myInt, myFloat)

Нулевое значение
для переменных
«int» — 0.

0 0

Нулевое значение для переменных «float64» — 0.

Но для других типов значение 0 будет недействительным, поэтому нулевое значение для этого типа будет отличаться. Скажем, для строковых переменных нулевым
значением является пустая строка, а для переменных bool — значение false.
var myString string
Нулевое значение
var myBool bool
fmt.Println(myString, myBool)
для переменных «string» —
пустая строка.

false

Нулевое значение
для переменных
«bool» — false.
дальше 4   51

объявление переменных

Развлечения с магнитами

Результат.

На холодильнике была выложена программа Go.
К сожалению, некоторые магниты упали на пол.
Удастся ли вам расставить фрагменты кода по
местам и создать работоспособную программу,
которая будет выводить нужный результат?
, "apples.")
func main() {

I started with 10 apples.
Some jerk ate 4 apples.
There are 6 apples left.
, "apples left.")

, "apples.")

}

var

var

int

fmt.Println("I started with",
fmt.Println("Some jerk ate",
fmt.Println("There are",

package main

int

=
10

=
4

originalCount-eatenCount

originalCount
originalCount
eatenCount

eatenCount
import (
"fmt"
)

Ответ на с. 64.

52

глава 1

знакомство с go

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

Объявление переменных и присваивание значений

ние
Просто добавьте присваива
в конец команды.
var quantity int = 4
Если вы объявляете нескольvar length, width float64 = 1.2, 2.4
ко переменных, укажите
var customerName string = "Damon Cole"
несколько переменных.

Но если вы знаете исходное значение переменной на момент ее объявления, то можно применить короткое объявление переменной.
Вместо того чтобы явно объявлять тип переменной и позднее присваивать ей значение оператором =, вы совмещаете эти две операции
с помощью синтаксиса :=.
Давайте изменим предыдущий пример, чтобы в нем использовались
короткие объявления переменных:
package main
import "fmt"
func main() {
Объявление пере- quantity := 4
менных и присваи- length, width := 1.2, 2.4
вание значений customerName := "Damon Cole"

}

fmt.Println(customerName)
fmt.Println("has ordered", quantity, "sheets")
fmt.Println("each with an area of")
fmt.Println(length*width, "square meters")

Damon Cole
has ordered 4 sheets
each with an area of
2.88 square meters

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

дальше 4   53

короткие объявления переменных

Сломай и изучи!

Возьмите программу, в которой используются переменные, внесите одно
из указанных изменений и запустите программу; затем отмените изменение и переходите к следующему. Посмотрите, что из этого выйдет!
package main
import "fmt"
func main() {
quantity := 4
length, width := 1.2, 2.4
customerName := "Damon Cole"

}

fmt.Println(customerName)
fmt.Println("has ordered", quantity, "sheets")
fmt.Println("each with an area of")
fmt.Println(length*width, "square meters")
Если...

Добавить второе объявление для той же переменной

Убрать символ «:» из короткого объявления переменной

Damon Cole
has ordered 4 sheets
each with an area of
2.88 square meters

...программа не будет работать, потому что...

quantity := 4
quantity := 4

quantity = 4

Переменную можно объявить только один раз. (Хотя
при желании ей можно сколько угодно раз присваивать новые значения. Также можно объявлять другие переменные с тем же именем, но для этого они
должны принадлежать другой области видимости.
Тема областей видимости рассматривается в следующей главе.)
Если вы забудете поставить двоеточие, конструкция
рассматривается как присваивание, а не как объявление, а переменной, которая не была объявлена,
нельзя будет присвоить значение

Присвоить строковое значе- quantity := 4
ние переменной int
quantity = "a"

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

Количества переменных и значений не совпадают

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

Удалить код,
в котором
используется
переменная

54

глава 1

length, width := 1.2

fmt.Println(customerName)

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

знакомство с go

Правила выбора имен
В Go существует один простой набор правил, применяемых к именам переменных, функций и типов:


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



Если имя переменной, функции или типа начинается с буквы верхнего регистра, оно считается
экспортируемым и может использоваться в других пакетах, кроме текущего. (Именно поэтому
буква P в fmt.Println имеет верхний регистр: это нужно для того, чтобы его можно было
использовать в main или любом другом пакете.) Если имя переменной/функции/типа начинается с буквы нижнего регистра, оно считается неэкспортируемым. Такие имена доступны
только в текущем пакете.
length
Нормально. stack2
Недопустимо. 2stack
sales.total
sales.Total

ры!
Не может начинаться с циф

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

Эти правила должны обязательно выполняться на уровне языка. Но сообщество Go также соблюдает ряд дополнительных соглашений:


Если имя состоит из нескольких слов, каждое слово после первого должно начинаться с буквы
верхнего регистра, и они должны следовать друг за другом без разделения пробелами: topPrice,
RetryConnection и т. д. (Первая буква имени имеет верхний регистр только в том случае,
если оно должно экспортироваться из пакета.) Этот стиль записи часто называется верблюжьим
регистром, потому что буквы верхнего регистра напоминают горбы у верблюда.



Если смысл имени очевиден по контексту, в сообществе Go принято сокращать его: использовать i вместо index, max вместо maximum и т. д. (Однако мы в Head First считаем, что во
время изучения нового языка ничего очевидного нет, и поэтому не будем придерживаться этого
соглашения в книге.)

sheetLength
Нормально. TotalUnits
i

Нарушают sheetlength
соглашения. Total_Units
index

иОстальные слова должны нач
ра!
ист
рег
го
хне
наться с буквы вер

Допустимо, но слова должны
записываться подряд!

Хорошо бы заменить сокращением!

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

преобразования типов

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

Создаем переменную
типа int.

Ошибки.

Если использовать
var length float64 = 1.2
float64 и int в матеvar width int = 2
матической операции...
fmt.Println("Area is", length*width)
fmt.Println("length > width?", length > width)
...или при
!
сравнении...
ошибке
об
ние
сообще
те
получи
...вы

invalid operation: length * width (mismatched types float64 and int)
invalid operation: length > width (mismatched types float64 and int)

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

Создаем переменную типа int.

var length float64 = 1.2
var width int = 2
Если присвоить значение
length = width
fmt.Println(length) int переменной float64...
...вы получите сообщение об ошибке!

Ошибка.

cannot use width (type int) as type float64 in assignment

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

var myInt int = 2
float64(myInt)

Тип, к которому преобразуется значение.

Преобразуемое
значение.

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

Без преобразования...
var myInt int = 2
int
fmt.Println(reflect.TypeOf(myInt))
float64
fmt.Println(reflect.TypeOf(float64(myInt)))

56

глава 1

После преобразования...

Тип изменен.

знакомство с go

Преобразования (продолжение)
Давайте исправим наш пример неработоспособного кода и преобразуем
значение int к типу float64 перед его использованием в математических
операциях или сравнениях с другими значениями float64.

float64
Преобразование int к типу
var length float64 = 1.2
значегое
дру
на
м
ние
оже
перед умн
var width int = 2
ние float64.
fmt.Println("Area is", length*float64(width))
fmt.Println("length > width?", length > float64(width))

Area is 2.4
length > width? false

Преобразование int к типу
float64 перед сравнением
с другим значением float64.

Теперь математические операции и операции сравнения работают правильно!
Попробуем преобразовать значение int к типу float64 перед присваиванием
переменной float64:
var length float64 = 1.2
var width int = 2
length = float64(width)
fmt.Println(length)

2

Преобразование int к типу
float64 перед присваиванием
переменной float64.

И снова после преобразования присваивание проходит успешно.
Всегда держите в голове, как преобразования изменяют выходные значения. Например, переменные float64 могут хранить дробные значения,
а переменные int — нет. Когда вы преобразовываете float64 в int,
дробная часть просто отбрасывается! Это может внести путаницу в любые
операции, выполняемые с результирующим значением.
var length float64 = 3.75
var width int = 5
В результате этого преобразоваwidth = int(length)
ния дробная часть теряется!
fmt.Println(width)

3

Полученное значение стало
на 0,75 меньше!

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

дальше 4   57

изучаем преобразования

Упражнение

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

var price int = 100
fmt.Println("Price is", price, "dollars.")
var taxRate float64 = 0.08
var tax float64 = price * taxRate
fmt.Println("Tax is", tax, "dollars.")
var total float64 = price + tax
fmt.Println("Total cost is", total, "dollars.")
var availableFunds int = 120
fmt.Println(availableFunds, "dollars available.")
fmt.Println("Within budget?", total go fmt hello.go
>go build hello.go
>hello.exe
Hello, Go!
>
Компиляция и запуск
программы hello.go
в Windows

знакомство с go

Инструменты Go
При установке Go в окружение командной строки добавляется
исполняемый файл с именем go. Исполняемый файл go предоставляет в ваше распоряжение различные команды, в числе которых:
Команда
go build
go run

go fmt

go version

Описание

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

Мы выполнили команду go fmt, которая переформатирует код
по стандартам Go. Она делает то же, что и кнопка Format на сайте
Go Playground. Мы рекомендуем выполнять команду go fmt для
каждого созданного вами исходного файла.
Также мы использовали команду go build для компиляции
кода в исполняемый файл. Такие исполняемые файлы могут
распространяться среди пользователей, причем пользователи
смогут запускать их, даже если на их компьютерах не установлен
компилятор Go.
Но мы еще не опробовали команду go run. Давайте сделаем это!

Многие редакторы можно настроить
так, чтобы они автоматически выполняли команду go fmt при каждом
сохранении файла! См. https://blog.
golang.org/go-fmt-your-code.

Быстрый запуск кода командой «go run»
Команда go run компилирует и запускает исходный файл
без сохранения исполняемого файла в текущем каталоге.
Она прекрасно подходит для быстрой проверки простых
программ. Воспользуемся ею для выполнения примера hello.go.
1

 ткройте новое окно терминала или коО
мандной строки.

2

 терминале перейдите в каталог, в котоВ
ром был сохранен файл hello.go.

3

Введите команду go run hello.go и нажмите Enter/Return. (Эта команда выглядит одинаково во всех операционных
системах.)

package main

func main() {
fmt.Println("Hello, Go!")
}

Перейдите в каталог,
в котором был сохранен файл hello.go.

Запуск файла
с исходным кодом.

Результаты работы программы немедленно появляются на
экране. Если вы внесли изменения в исходный код, выполнять
отдельный шаг компиляции не нужно; просто запустите свой
код командой go run и сразу получите результат. При работе
с небольшими программами команда go run очень удобна!

hello.go

import "fmt"

Shell Edit View Window Help

$ cd try_go
$ go run hello.go
Hello, Go!
$
Выполнение hello.go
командой
go run (в любой ОС)
дальше 4   61

инструментарий

ГЛАВА 1

Ваш инструментарий Go
Глава 1 подошла к концу.
В ней ваш инструментарий
пополнился вызовами
функций и типами.

Вызовы

ий

функц

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

Типы

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

62

глава 1

КЛЮЧЕВЫЕ
МОМЕНТЫ


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



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



Строка — последовательность байтов, обычно представляющих символы текста.

� Руна представляет отдельный символ текста.


Два самых распространенных числовых типа Go — int
(для хранения целых чисел) и float64 (для хранения
чисел с плавающей точкой).



Тип bool используется для хранения логических значений (true или false).



Переменная представляет собой блок памяти для
хранения значения заданного типа.



Если переменной не присвоено значение, то она содержит нулевое значение для своего типа. Примеры нулевых значений: 0 для переменных int или float64,
"" для строковых переменных.



Объявление переменной можно совместить с присваиванием ей значения при помощи короткого объявления
переменной :=.



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



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



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



Команда go run компилирует и выполняет программу
без сохранения в исполняемом файле в текущем каталоге.

знакомство с go

У бассейна. Решение
package main
import (
“fmt”
)
func main() {
fmt.Println( “Cannonball!!!!” )
}

Результат.

Cannonball!!!!

Соедините каждый фрагмент кода с правильным типом.
Некоторым типам могут соответствовать несколько фрагментов.

reflect.TypeOf(25)

int

reflect.TypeOf(5.2)

float64

reflect.TypeOf(false)

bool

reflect.TypeOf("hello")

string

reflect.TypeOf(true)
reflect.TypeOf(1)

reflect.TypeOf(1.0)

дальше 4   63

решение упражнений

Развлечения с магнитами. Решение
package main

import (
"fmt"
)
func main() {
var

originalCount

=

int

fmt.Println("I started with",

var

eatenCount

=

int

fmt.Println("Some jerk ate",
fmt.Println("There are",

10

originalCount

, "apples.")

4

eatenCount

, "apples.")

originalcount-eatenCount

}

, "apples left.")

Результат.

I started with 10 apples.
Some jerk ate 4 apples.
There are 6 apples left.

Обновите код, заполнив пропуски в следующем коде. Исправьте ошибки, чтобы программа выдавала желаемый результат. (Подсказка: перед выполнением математических операций или
сравнений необходимо провести преобразование, чтобы обеспечить совместимость типов.)
var price int = 100
fmt.Println("Price is", price, "dollars.")
var taxRate float64 = 0.08
float64(price) * taxRate
var tax float64 =
fmt.Println("Tax is", tax, "dollars.")
float64(price) + tax
var total float64 =
fmt.Println("Total cost is", total, "dollars.")

Ожидаемый
результат.

Price is 100 dollars.
Tax is 8 dollars.
Total cost is 108 dollars.
120 dollars available.
Within budget? true

var availableFunds int = 120
fmt.Println(availableFunds, "dollars available.")
total = 60 {
fmt.Println("You pass.")
} else {
fmt.Println("You fail!")
}
дальше 4

73

команды if

Условные команды (продолжение)
Условные команды используют логическое выражение (результат которого равен true или
false), чтобы определить, должен ли выполняться содержащийся в них код.
if 1 == 1 {
fmt.Println("I'll be printed!")
}

if 1 >= 2 {
fmt.Println("I won't!")
}

if 1 > 2 {
fmt.Println("I won't!")
}

if 2 12 {
fmt.Println("12 > 12")
}
if 12 >= 12 {
fmt.Println("12 >= 12")
}
if 12 == 12 && 5.9 == 5.9 {
fmt.Println("12 == 12 && 5.9 == 5.9")
}
if 12 == 12 && 5.9 == 6.4 {
fmt.Println("12 == 12 && 5.9 == 6.4")
}
if 12 == 12 || 5.9 == 6.4 {
fmt.Println("12 == 12 || 5.9 == 6.4")
}

(Мы вписали первые два
результата за вас.)

Результат:

true
!false

Ответ на с. 109.
дальше 4

75

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

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

input, err := reader.ReadString('\n')
log.Fatal(err)
Сообщает о возвращаемом значении ошибки.

Shell Edit View Window Help

$ go run pass_fail.go
Enter a grade: 100
2018/03/11 18:27:08
exit status 1
$

Ошибка выдается даже
в том случае, если все
работает правильно!

Значение ошибки
равно «nil».

Мы знаем, что если значение в переменной err равно nil, это говорит о том,
что данные с клавиатуры были прочитаны успешно. Теперь, познакомившись
с командами if, попробуем обновить код, чтобы сообщение об ошибке и
завершение программы происходило только в том случае, если значение err
не равно nil.
// pass_fail сообщает, сдал ли пользователь экзамен.
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
Если «ошибка»
Сообщить об ошибке и прервать
log.Fatal(err)
не равна nil...
выполнение программы.
}
fmt.Println(input)
}

Запустите программу: вы увидите, что в новой версии она успешно работает.
А если при чтении введенных данных возникнут ошибки, вы увидите и их!
Запускаем pass_fail.go.
Программа выводит
введенное число.

76

глава 2

Shell Edit View Window Help

$ go run pass_fail.go
Enter a grade: 100
100

$

какой код будет выполняться?

Развлечения с магнитами

На холодильнике была выложена программа Go, которая выводит размер файла.
Для этого программа вызывает функцию os.Stat, которая возвращает значение
os.FileInfo и, возможно, значение ошибки. Затем она вызывает метод Size для
значения FileInfo, чтобы получить размер файла.
Однако исходная программа использует пустой идентификатор _, чтобы проигнорировать значение ошибки из os.Stat. Если в программе произойдет ошибка (например,
если файл не существует), программа будет работать некорректно.
Используйте дополнительные фрагменты кода и создайте программу, которая в основном работает так же, как исходная, но еще проверяет значение ошибки, полученной от
os.Stat. Если ошибка от os.Stat не равна nil, программа должна сообщить об
ошибке и аварийно завершиться. Снимите магнит с пустым идентификатором _; он не
будет использоваться в программе.
Эта программа работает! Но она игнорирует все
ошибки, которые могут возникнуть при выполнении....

Пустой идентификатор игнорирует
все значения ошибок. Снимите этот
магнит и замените
его одним из изображенных внизу!

import (
"fmt"
"log"
"os"
)
func main() {

Содержит размер
файла, дату его изменения и т. д.
Добавьте сюда
свой код. Если код
ошибки не равен nil,
передайте его
log.Fatal.

fileInfo,

_

:=

Получает значение
FileInfo с данными,
относящимися
к файлу my.txt.

os.Stat("my.txt")

Ответ на с. 110.

package main

fmt.Println(fileInfo.Size())

}

Возвращает
размер файла.

мму!
Это дополнительные магниты. Добавьте их в програ

{

!=

}

nil

err

err

if

log.Fatal(err)

дальше 4

77

замещение имен

Избегайте замещения имен
Меня еще кое-что беспокоит.
Ранее вы говорили, что стараетесь
избегать сокращений в этой книге.
А здесь переменной присваивается
имя err вместо error!

fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}

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

Объявляя переменную, проследите за тем, чтобы ее имя не
совпадало с именами существующих функций, пакетов, типов
или других переменных. Если такое имя уже существует во
внешней области видимости (вскоре мы поговорим об областях
видимости), ваша переменная заместит его, то есть будет перехватывать все обращения к нему. И часто это нежелательно.
В следующем фрагменте объявляется переменная с именем int,
которая замещает имя типа, переменная с именем append,
которая замещает имя встроенной функции (функция append
будет представлена в главе 6), а также переменная с именем fmt,
которая замещает имя импортированного пакета. Эти имена
создают путаницу, но сами по себе не порождают ошибок...
package main

Переменная с имеПеременная с именем
нем «int» замещаго
нно
«append» замещает имя
ет имя встрое
func main() {
встроенной функции
var int int = 12
типа «int»!
var append string = "minutes of bonus footage"
«append»!
var fmt string = "DVD"
}
Переменная с именем «fmt» замещает имя импортированного
пакета «fmt»!

import "fmt"

78

глава 2

какой код будет выполняться?

Избегайте замещения имен (продолжение)
...Но если вы попытаетесь обратиться к типу, функции или пакету, имя которых
замещается переменной, то вы получите значение из переменной. В следующем
примере это приводит к ошибке компиляции:
func main() {
var int int = 12
var append string = "minutes of bonus footage"
й выше,
var fmt string = "DVD" «int» теперь относится к переменной, объявленно
у!
тип
у
var count int
а не к числовом
var languages = append([]string{}, "Español")
fmt.Println(int, append, "on", fmt, languages)
Имя «append» теперь
}
обозна
чает переменИмя «fmt» теперь обозначает переменную,
ную, а не функцию!
а не пакет!
Ошибки компиляции

imported and not used: "fmt"
int is not a type
cannot call non-function append (type string), declared at prog.go:7:6
fmt.Println undefined (type string has no field or method Println)

Чтобы не путаться самому и не путать коллег, старайтесь по возможности избегать
замещения имен. В данном случае проблема решается простым выбором неконфликтующих имен для переменных:
Переименуем переменfunc main() {
Переименуем переменvar count int = 12
ную «int».
ную «append».
var suffix string = "minutes of bonus footage"
var format string = "DVD"
Переименуем переменную «fmt».
var languages = append([]string{}, "Español")
fmt.Println(count, suffix, "on", format, languages)
}

12 minutes of bonus footage on DVD [Español]

Как было показано в главе 3, в Go существует встроенный тип с именем error. Вот
почему при объявлении переменных, предназначенных для хранения ошибок, мы
присваивали им имя err вместо error — мы хотели предотвратить замещение
имени типа ошибки именем переменной.
«err», а не «error»!

fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}

Если вы все же назовете свою переменную error, возможно, ваш код будет работать.
По крайней мере до того момента, как вы забудете, что имя типа ошибки было замещено, попробуете воспользоваться типом и получите вместо него переменную. Лучше
не рисковать; используйте имя err для своих переменных со значениями ошибок!
дальше 4

79

получение чисел из строк

Преобразование строк в числа
Условные команды также могут быть использованы для проверки
введенного значения. Давайте воспользуемся командой if/else для
определения того, прошел ли пользователь экзамен или нет. Если введенный процент правильных ответов равен 60 и выше, переменной
status присваивается строка "passing". В противном случае переменной будет присвоена строка "failing".
// Директивы package и import пропущены
func main() {
fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
if input >= 60 {

status := "passing"
} else {

status := "failing"
}
}

Однако в текущем варианте этой программы выдается ошибка
компиляции.
Ошибка.

cannot convert 60 to type string
invalid operation: input >= 60 (mismatched types string and int)

Дело в том, что ввод с клавиатуры читается как строка. Go может
сравнивать числа только с другими числами; сравнить число со
строкой не удастся. А прямого преобразования типа из строки
в число не существует:
float64("2.6")
Ошибка.

cannot convert "2.6" (type string) to type float64

Мы должны решить две задачи:


В конце строки input находится символ новой строки, появившийся в результате нажатия клавиши Enter в процессе ввода.
Его необходимо удалить.



Остальные символы строки необходимо преобразовать в число
с плавающей точкой.

80

глава 2

какой код будет выполняться?

Преобразование строк в числа (продолжение)
Удалить символ новой строки из конца входного текста несложно.
Пакет strings содержит функцию TrimSpace, которая удаляет все
символы-пропуски (символы новой строки, табуляции и обычные
пробелы) в начале и в конце строки.
s := "\t formerly surrounded by space \n"
fmt.Println(strings.TrimSpace(s))

formerly surrounded by space

Таким образом, чтобы убрать символ новой строки из входной строки,
следует передать ее TrimSpace, а затем снова присвоить возвращенное
значение переменной input.
input = strings.TrimSpace(input)

После этого в строке input должно остаться только число, введенное
пользователем. Мы воспользуемся функцией ParseFloat из пакета
strconv, чтобы преобразовать его в значение float64.
В аргументах передается
преобразуемая строка...

...и количество битов точности для результата.

grade, err := strconv.ParseFloat(input, 64)
Возвращаемые значения — float64...

...и возможная
ошибка.

Функции ParseFloat передается строка, которую необходимо
преобразовать в число, а также количество битов точности
для результата. Поскольку строка преобразуется в значение
float64, мы передаем число 64. (Кроме float64, в Go также
поддерживается менее точный тип float32, однако им лучше
не пользоваться, если только у вас нет на это веских причин.)
Функция ParseFloat преобразует число в строку и возвращает
его в форме float64. Как и ReadString, она также имеет второе возвращаемое значение — значение ошибки. Оно должно
быть равно nil, если только в ходе преобразования строки не
возникли какие-то проблемы. (Например, переданная строка
не может быть преобразована в число. Да и какой может быть
числовой эквивалент у строки "hello"...)

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

дальше 4

81

получение чисел из строк

Преобразование строк в числа (продолжение)
Дополним программу pass_fail.go вызовами TrimSpace и ParseFloat:
// pass_fail сообщает, сдал ли пользователь экзамен.
package main
import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
)

испольДобавляем «strconv», чтобы
.
oat
seFl
Par
е
мм
зовать в програ

Добавляем «strings», чтобы использовать в программе функцию TrimSpace.

func main() {
fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
Удаляем символ новой
строки из входной строки.
input = strings.TrimSpace(input)
Преобразовать строку
Как и в случае grade, err := strconv.ParseFloat(input, 64)
в
значение float64.
с ReadString, сообщить if err != nil {
log.Fatal(err)
о любых ошибках при
еменной «grade»,
}
Сравниваем с float64 в пер
преобразовании.
й «input».
а не со строкой в переменно
if grade >= 60 {

status := "passing"
} else {

status := "failing"
}
}

Сначала нужные пакеты включаются в директиву import. Мы добавляем код
удаления символа новой строки из входного текста, после чего передаем ввод
функции ParseFloat и сохраняем полученное значение float64 в новой переменной grade.

Как и в случае с ReadString, мы проверяем, возвращает ли ParseFloat значение
ошибки. В этом случае программа сообщает об ошибке и аварийно завершается.
Наконец, мы обновляем условную команду, чтобы она проверяла число в grade,
а не строку в input. На этом все ошибки, происходящие от сравнения строки
с числом, должны быть исправлены.
При запуске обновленной программы мы уже не получаем сообщение о несовпадении типов string и int. На первый взгляд проблема решена, однако
в программе осталась еще пара ошибок. Сейчас мы займемся их устранением.
82

глава 2

Ошибки.

status declared
and not used
status declared
and not used

какой код будет выполняться?

Блоки

Ошибки.

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

if grade >= 60 {
status := "passing"
} else {
status := "failing"
}

status declared
and not used
status declared
and not used

Как было показано ранее, объявление переменной — такой, как status, — без ее
последующего использования в программе Go считается ошибкой. Немного странно, что ошибка повторяется дважды, но пока не будем обращать на это внимания.
Добавим вызов Println для вывода введенного значения и значения status.
func main() {
// Часть кода пропущена...
if grade >= 60 {

status := "passing"
Выводим пере} else {
менную status.
Ошибка.

status := "failing"
}
fmt.Println("A grade of", grade, "is", status)
undefined: status
}

Но теперь появляется новая ошибка — в сообщении говорится, что переменная
status не определена при попытке использовать ее в команде Println! Что происходит?
Код Go делится на блоки (сегменты). Блоки обычно заключаются в фигурные скобки
({}) и могут существовать как на уровне файлов с исходным кодом, так и на уровне
пакетов. Блоки могут вкладываться друг в друга.
package main
func main() {
// Код
if true {

// Еще код
}

Блок «if».

Блок
функции.

Блок
пакета.

Блок
в файле.

}

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

83

блоки и области видимости

Блоки и область видимости переменной
Каждая объявленная переменная обладает областью видимости: частью кода, в которой она «видна».
К объявленной переменной можно обращаться в любой точке ее области видимости, однако при
попытке обратиться к ней за пределами этой области видимости вы получите сообщение об ошибке.
Область видимости переменной состоит из блока, в котором она
была объявлена, и всех блоков, вложенных в этот блок.
package main
import "fmt"
var packageVar = "package"
func main() {
var functionVar = "function"
if true {

var conditionalVar = "conditional"
fmt.Println(packageVar)
Все еще в области видимости.
fmt.Println(functionVar)
Все еще в области видимости.
Все еще в области видимости.
fmt.Println(conditionalVar)
}
Область видимости
fmt.Println(packageVar)
Все еще в области видимости.
conditionalVar.
fmt.Println(functionVar)
Все еще в области видимости.
Не определено вне области видимости!
fmt.Println(conditionalVar)
}
Область видимости
functionVar.
Ошибка.

undefined: conditionalVar

Области видимости переменных в приведенном выше коде:


Областью видимости packageVar является весь пакет main. К packageVar
можно обращаться в любой точке любой функции, определенной в пакете.



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



Область видимости conditionalVar ограничивается блоком if. При попытке
обратиться к conditionalVar после закрывающей фигурной скобки } блока
if вы получите сообщение об ошибке, в котором говорится, что переменная
conditionalVar не определена!

84

глава 2

Область видимости
packageVar.

какой код будет выполняться?

Блоки и область видимости переменной (продолжение)
Теперь, когда вы понимаете смысл областей видимости переменных, мы можем объяснить, почему переменная status не была определена в программе. Мы объявили переменную status в условных блоках.
(Собственно, она была объявлена дважды, поскольку программа содержит два разных блока. Именно
поэтому мы получили две ошибки «переменная status объявлена, но не использована».) Но затем мы
попытались обратиться к status за пределами этих блоков — в месте, не входившем в область видимости
этой переменной.
func main() {
// Omitting code up here...
if grade >= 60 {
Блок «if».

status := "passing"
} else {
Блок «else».

status := "failing"
}
fmt.Println("A grade of", grade, "is", status)
}
Ошибка.

Блок функции.
В этой области видимости переменная «status»
НЕ ОПРЕДЕЛЕНА!

undefined: status

Проблема решается перемещением объявления переменной status
из блоков условных команд в блок функции. После этого переменная
status будет находиться в области видимости как во вложенных условных блоках, так и в конце блока функции.
func main() {
// Часть кода пропущена...
var status string
Объявление перемещается сюда.
if grade >= 60 {
Блок функции.
Заменяются

status = "passing"
командами при} else {
сваивания.

status = "failing"
}
Теперь переменная
fmt.Println("A grade of", grade, "is", status)
«status» будет на}
ходиться в области
видимости в конце
блока функции.

Будьте
осторожны!

Не забудьте заменить короткие
объявления переменных во вложенных
блоках командами присваивания!

Если не заменить оба вхождения := на =,
вы случайно создадите новые переменные
с именем status внутри вложенных условных блоков, и эти
переменные не будут находиться в области видимости внешнего
блока функции!
дальше 4

85

полный код

Работа над программой завершена!
Вот и все! Программа pass_fail.go готова к работе. Давайте еще
раз взглянем на ее полный код:
// pass_fail сообщает, сдал ли пользователь экзамен.
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"strconv"
)

Функция «main»
вызывается при запуске программы.

Запрашиваем у пользователя значение.

func main() {
fmt.Print("Enter a grade: ")
reader := bufio.NewReader(os.Stdin)
Если произошла ошиб- input, err := reader.ReadString('\n')
ка, вывести сообщение if err != nil {
log.Fatal(err)
и прервать работу
программы. }
input = strings.TrimSpace(input)

Создаем bufio.Reader для
чтения ввода с клавиатуры.

Читает данные, вводимые
пользователем до нажатия
клавиши Enter.

Удалить символ новой строки

из введенных данных.
Если произошла ошиб- grade, err := strconv.ParseFloat(input, 64)
ка, вывести сообщение if err != nil {
Преобразовать введенную строку
log.Fatal(err)
и прервать выполн
в значение float64 (число).
ение
программы. }

Переменная «status» объявляется здесь, чтобы она

Если значение grade равно var status string
находилась в области видимости в границах функции.
60 и более, переменной if grade >= 60 {

status
=
"passing"
status присваиваетс
я
Выводим
...и результат сдачи
строка «passing». В про- } else {
ое
енн
введ
экзамена.
status = "failing"
тивном случае
пере.
ие..
}
чен
зна
менной присваивается
строка «failing». fmt.Println("A grade of", grade, "is", status)

}

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

86

глава 2

Shell Edit View Window Help

$ go run pass_fail.go
Enter a grade: 56
A grade of 56 is failing
$ go run pass_fail.go
Enter a grade: 84.5
A grade of 84.5 is passing
$

какой код будет выполняться?

Упражнение

Некоторые строки этого кода приводят к ошибкам компиляции, потому что обращаются
к переменной, находящейся вне области видимости. Вычеркните строки, содержащие
ошибки.
package main
import (
"fmt"
)
var a = "a"
func main() {
a = "a"
b := "b"
if true {

c := "c"

if true {
d := "d"
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
}

Ответ на с. 111.

дальше 4

87

объявление новых переменных

Только одна переменная в коротком объявлении должна быть новой
И еще один момент! В этом коде есть странность.
В главе 1 вы сказали, что переменную нельзя объявить
дважды. Тем не менее переменная err встречается
в двух разных коротких объявлениях!
ся здесь.
Переменная «err» объявляет

input, err := reader.ReadString('\n')
// ...
grade, err := strconv.ParseFloat(input, 64)
Но похоже, мы объявляем переменную
«err» во второй раз!

Действительно, если дважды объявить одно имя переменной в одной
области видимости, компилятор выдаст сообщение об ошибке:
Попытаемся снова
объявить «a».

a := 1
a := 2

Ошибка компиляции.

no new variables on left side of :=

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

Объявляем «a».
Объявляем «b», присваиваем «a».

a := 1
b, a := 2, 3
a, c := 4, 5
Присваиваем «a»,
fmt.Println(a, b, c) объявляем «c».

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

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

4 2 5

var a, b float64
var err error
a, err = strconv.ParseFloat("1.23", 64)
b, err = strconv.ParseFloat("4.56", 64)

Вместо этого Go позволяет использовать короткие объявления переменных, даже
если для одной из переменных в действительности выполняется присваивание.
Объявляем «a»
...Можно просто воспользоваться синтаксисом a, err := strconv.ParseFloat("1.23", 64)
b, err := strconv.ParseFloat("4.56", 64)
короткого объявления пеfmt.Println(a, b, err)
ременных.

1.23 4.56

88

глава 2

и «err».

Объявляем «b»
и присваиваем «err».

какой код будет выполняться?

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

Этот пример впервые появился в книге
Head First Ruby.
(Еще одна хорошая книга, которую
также стоит купить!)
Он оказался настолько удачным, что
мы снова используем его здесь.

Составим список дел, которые нам предстоит сделать:

1 до 100
айное число от
уч
сл
ть
ва
ро
Сгенери
о.
и сохранить ег
манное число
року угадать заду
иг
ь
ит
ож
дл
ре
П
о ответ.
и сохранить ег
загаданигрока меньше
ие
ен
ж
ло
по
ед
опустим,
Если пр
и сообщение (д
ст
ве
вы
а,
сл
ка игроного чи
W»). Если оцен
LO
s
wa
s
es
gu
ur
и другое
«Oops. Yo
о числа, вывест
ог
нн
да
га
за
е
ш
H»).
ка боль
ur guess was HIG
Yo
s.
op
O

ие
сообщен
10 раз. Перед
ку угадывать до
ро
иг
ь
ит
еш
зр
Ра
щить игроку,
ложением сооб
по
ед
пр
м
ды
ж
ка
сь.
к у него остало
сколько попыто
загаданигрока равно
ие
ен
ж
ло
по
ед
об успехе
Если пр
и сообщение
ст
ве
вы
у,
сл
ному чи
ложения.
ашивать предпо
и перестать запр
а число так
ились попытки,
нч
ко
ка
ро
иг
у
Если
щение: «Sorry.
о, вывести сооб
ан
ад
уг
ло
бы
и не
[загаданное
y number. It was:
m
s
es
gu
’t
dn
di
You
число]».

Я составил список
требований за вас.
Как вы думаете,
осилите?

Гэри Ришардо,
гейм-дизайнер

Создадим новый файл с исходным кодом, который будет называться guess.go.
Похоже, начать нужно с генерирования случайного числа. За дело!

дальше 4

89

пути импортирования пакетов

Имена пакетов и пути импортирования
Пакет math/rand содержит функцию
Intn, которая сгенерирует случайное
число за нас, поэтому в программу следует импортировать math/rand. После
этого можно будет вызвать функцию
rand.Intn для генерирования случайного числа.

package main
import (
"fmt"
"math/rand"
)

Импортируем пакет «math/rand».

Вызываем функцию rand.Intn,
которая генерирует целое число.
func main() {
target := rand.Intn(100) + 1
fmt.Println(target)
}

Постойте-ка! Вы только что сказали,
что Intn входит в пакет math/rand.
Тогда почему вы ввели только rand.
Intn, а не math/rand.Intn?

Потому, что первое — путь импортирования пакета, а второе — имя пакета.
Упоминая math/rand, мы имеем в виду путь импортирования пакета, а не
его имя. Путь импортирования — всего лишь уникальная строка, которая
идентифицирует пакет и используется в директиве import. После того
как пакет будет импортирован, к нему можно обращаться по имени пакета.
Для всех пакетов, которые использовались
до сих пор, путь импортирования совпадал
с именем пакета. Несколько примеров:

Путь
импортирования
"fmt"
"log"

"strings"

Однако путь импортирования и имя пакета
могут различаться. Многие пакеты Go классифицируются по категориям — например,
«сжатие» или «комплексные вычисления».
По этой причине они часто группируются
по префиксам пути импортирования (например, "archive/" или "math/"). (Их
можно рассматривать как аналоги путей
каталогов на жестком диске.)
90

глава 2

Имя пакета

fmt
log
strings

Путь
импортирования
"archive"

"archive/tar"
"archive/zip"
"math"

"math/cmplx"
"math/rand"

Имя пакета

archive
tar
zip
math
cmplx
rand

какой код будет выполняться?

Имена пакетов и пути импортирования (продолжение)
Язык Go не требует, чтобы имя пакета было
как-то связано с путем импортирования.
Но по соглашению последний (или единственный) сегмент пути импортирования
также используется в качестве имени пакета. Таким образом, для пути импортирования "archive" именем пакета также
будет archive, а для пути импортирования
"archive/zip" будет использоваться имя
пакета zip.

Путь импортирования
"archive"

"archive/tar"
"archive/zip"
"math"

"math/cmplx"
"math/rand"

Именно по этой причине в директиве import
используется путь "math/rand", а в функции
main имя пакета — rand.

Генерирование случайных чисел
Передайте число функции rand.Intn, и
функция вернет случайное число в диапазоне от 0 до переданного числа. Другими
словами, если передать аргумент 100,
будет получено случайное число в диапазоне 0–99. Так как нам требуется число
в диапазоне 1–100, остается прибавить 1
к полученному случайному значению.
Результат сохраняется в переменной
target. Пока мы ограничимся простым
выводом переменной target.

package main
import (
"fmt"
"math/rand"
)

Имя пакета

archive
tar
zip
math
cmplx
rand

Используем полный
путь импортирования
для «math/rand».

Используем имя пакета:
«rand».
func main() {
target := rand.Intn(100) + 1
fmt.Println(target)
}

package main
import (
"fmt"
"math/rand"
)

Генерируем случайное
число от 0 до 99.

func main() {
target := rand.Intn(100) + 1
fmt.Println(target)
}

Попытавшись запустить программу в таком виде, вы получите случайное число. Однако вы будете получать одно и то же случайное
число раз за разом! Дело в том, что случайные числа, генерируемые
компьютерами, на самом деле не настолько уж случайны. Тем не
менее их можно сделать более случайными...
Получаем одно и то же
случайное число при каждом запуске программы!

Прибавить 1,
чтобы целое
число лежало
в диапазоне
от 1 до 100.

Shell Edit View Window Help

$ go run guess.go
82
$ go run guess.go
82
$ go run guess.go
82
$
дальше 4

91

инициализация генератора

Генерирование случайных чисел (продолжение)
Чтобы получать разные случайные числа, необходимо передать значение функции rand.
Seed. Тем самым вы «инициализируйте» генератор случайных чисел, то есть предоставляете значение, которое будет использоваться для генерирования других случайных чисел.
Но если передавать одно и то же значение инициализации, то и случайные значения
будут теми же и мы снова вернемся к тому, с чего начинали.
Ранее было показано, что функция time.Now выдает значение Time, представляющее
текущую дату и время. Его можно использовать для того, чтобы получать разное значение
инициализации при каждом запуске программы.
package main
import (
"fmt"
"math/rand" Также импортируем пакет «time».
"time"
)

вреПолучаем текущую дату и
мя в формате целого числа.

func main() {
seconds := time.Now().Unix()
rand.Seed(seconds)
Функция генератора случайных чисел.
Теперь генерируемые
target := rand.Intn(100) + 1
числа будут разными
fmt.Println("I've chosen a random number between 1 and 100.")
при каждом запуске!
fmt.Println("Can you guess it?")
fmt.Println(target)
Сообщаем игроку о том,
}
что число выбрано.

Функция rand.Seed ожидает получить целое число, поэтому передать ей значение
Time напрямую не удастся. Вместо этого для Time следует вызвать метод Unix, который преобразует его в целое число. (А конкретно значение будет преобразовано в
формат времени Unix — целое количество секунд, прошедших с 1 января 1970 года.
Впрочем, запоминать это не нужно.) Это число передается rand.Seed.
Мы также добавим пару вызовов Println, чтобы уведомить пользователя о выборе случайного числа. Но помимо этого, оставшуюся часть кода, включая вызовы
rand.Intn, можно оставить как есть. Инициализация генератора должна стать
единственным внесенным изменением.
Теперь при каждом запуске программы
вы будете видеть сообщение со случайным числом. Похоже, изменения были
успешными!
Разные числа при каждом
запуске программы!

92

глава 2

Shell Edit View Window Help

$ go run guess.go
I've chosen a random number between 1 and 100.
Can you guess it?
73
$ go run guess.go
I've chosen a random number between 1 and 100.
Can you guess it?
18
$

какой код будет выполняться?

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

от 1 до 100
учайное число
сл
ть
ва
ро
ри
не
Сге
о.
и сохранить ег
думанное
року угадать за
иг
ь
ит
ож
дл
Пре
ить его ответ.
число и сохран

Есть только одно отличие: вместо того,
чтобы преобразовывать входное значение в float64, мы преобразуем его
в int (поскольку в игре используются
только целые числа).
По этой причине
package main
строка, прочитанная
с клавиатуры, переimport (
дается функции Atoi
"bufio"
пакета strconv —
"fmt"
Импортировать дополнительные пакеты.
вместо функции
"log"
(Все эти пакеты использовались в проParseFloat. Функ"math/rand"
е проверки результатов экзамена!)
грамм
ция Atoi также воз"os"
вращает целое число.
"strconv"
"strings"
(Как и ParseFloat,
"time"
Atoi может вернуть
)
ошибку, если преобразовать строку не
func main() {
удастся. В таком слуseconds := time.Now().Unix()
чае программа снова
rand.Seed(seconds)
сообщает об ошибке
target := rand.Intn(100) + 1
и завершается.)
fmt.Println("I've chosen a random number between 1 and 100.")
fmt.Println("Can you guess it?")
fmt.Println(target)

reader := bufio.NewReader(os.Stdin)

Создаем bufio.Reader
для чтения ввода
с клавиатуры.

fmt.Print("Make a guess: ")
Запросить число.
input, err := reader.ReadString('\n')
Если произошла ошибка, про- if err != nil {
Прочитать данные, введенные польграмма выводит сообще
log.Fatal(err)
ние
зователем до нажатия Enter.
и завершается. }
Удаление символа
input = strings.TrimSpace(input)
новой строки.
guess, err := strconv.Atoi(input)
Если произошла ошибка, про- if err != nil {
Входная строка преобграмма выводит сообще
log.Fatal(err)
ние
разуется в целое число.
и завершается. }
}
дальше 4

93

сравнение чисел

Сравнение предположения с загаданным числом
Еще одно требование выполнено. Да и со
следующим должно быть просто... Нужно
сравнить предположение пользователя
со сгенерированным числом и сообщить,
было ли предположение больше или
меньше загаданного числа.

число
адать задуманное
уг
ку
ро
иг
ь
ит
Предлож
о ответ.
и сохранить ег
загаданигрока меньше
ие
ен
ж
ло
по
ед
устим,
Если пр
сообщение (доп
и
ст
ве
вы
а,
сл
оценка
ного чи
sLOW»). Если
wa
s
es
gu
ur
Yo
вывести
«Oops.
данного числа,
га
за
е
ш
ль
бо
ess was
игрока
(«Oops. Your gu
ие
ен
щ
об
со
другое
HIGH»).

Если предположение guess меньше target, необходимо вывести
соответствующее сообщение. В противном случае, если предположение
guess больше target, следует вывести сообщение об этом. Похоже,
здесь будет уместна команда if...else. Мы добавим ее под остальным
кодом функции main.
// Директивы package и import не изменились
func main() {
// Предшествующий код тоже не изменился

Если предположение игрока
меньше загаданного числа,
сообщить об этом.
if guess < target {

fmt.Println("Oops. Your guess was LOW.")
} else if guess > target {

fmt.Println("Oops. Your guess was HIGH.")
}
Если предположение игрока
}
больше загаданного числа,
сообщить об этом.
Теперь попробуем запустить обновленную программу в терми-

нале. Программа все еще выводит target при каждом запуске,
что может быть полезно для отладки. Введите меньшее число —
и программа сообщит об этом. Если запустить программу повторно, вы получите Shell Edit View Window Help
новое значение target. Введите большее
$ go run guess.go
число, и получите другое сообщение.
81
I've chosen a random
Can you guess it?
Make a guess: 1
Oops. Your guess was
$ go run guess.go
54
I've chosen a random
Can you guess it?
Make a guess: 100
Oops. Your guess was
$
94

глава 2

number between 1 and 100.
LOW.
number between 1 and 100.
HIGH.

какой код будет выполняться?

Циклы

ньше загаданжение игрока ме
тим,
Если предполо
общение (допус
со
и
ст
ве
вы
а,
игроного числ
»). Если оценка
W
LO
s
wa
s
es
gu
«Oops. Your
вести другое
нного числа, вы
ка больше загада
was HIGH»).
ops. Your guess
сообщение («O
10 раз. Перед
ку угадывать до
року,
Разрешить игро
м сообщить иг
ие
ен
ж
ло
по
ед
каждым пр
сь.
к у него остало
сколько попыто

И еще одно требование выполнено! Переходим
к следующему.
Пока у игрока есть только одна попытка, но мы
хотим, чтобы игрок мог угадывать до 10 раз.
Код для вывода запроса уже готов. Остается
лишь организовать его повторное выполнение.
Мы можем воспользоваться циклом для многократного выполнения блока кода. Если у вас
есть опыт работы на других языках программирования, вероятно, вы уже сталкивались
с циклами. Если вы хотите, чтобы одна или
несколько команд выполнялись снова и снова,
разместите их в цикле.
Ключевое
слово «for».

Команда
инициализации.

Условное
выражение.

Оператор
приращения.

for x := 4; x 0, а оператор приращения уменьшает t
на 1 при каждом выполнении цикла. В конечном
итоге t уменьшается до 0 и цикл завершается.

Перед выполВыполнять
нением цикла
цикл, пока
После каждой итеинициализи- «t» больше 0.
рации уменьшить
ровать «t»
на 1.
«t»
значением 3.
for t := 3; t > 0; t-- {
3
fmt.Println(t)
2
}
1
fmt.Println("Blastoff!")

Blastoff!

дальше 4

95

циклы for

Циклы (продолжение)
x := 0
x++
fmt.Println(x)
x++
fmt.Println(x)
x-fmt.Println(x)

1
2
1

for x := 3; x >= 1; x-- {
fmt.Println(x)
}

3
2
1

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

В циклах ++ и -- удобны для выполнения прямого или обратного отсчета.
for x := 1; x = 60 {
Не от
status = "passing"
личается
} else {
кода
от
status = "failing"
из главы 2. }
Enter a grade: 89.7
fmt.Println("A grade of", grade, "is", status)
A grade of 89.7 is passing
}

148 глава 4

запаковка кода

Разные программы, одна функция (продолжение)
На этой странице приведена новая программа tocelsius.go, которая запрашивает у пользователя
температуру в градусах по Фаренгейту и преобразует введенное значение в градусы по Цельсию.
Обратите внимание: функция getFloat в tocelsius.go идентична функции getFloat из pass_fail.go.
// tocelsius преобразует температуру в градусах по Фаренгейту
// в градусы по Цельсию.
package main

tocelsius.go

import (
"bufio"
"fmt"
"log"
"os"
"strconv"
"strings"
)
func getFloat() (float64, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {

return 0, err
}

Не отличается
от функции getFloat
на предыдущей странице!

input = strings.TrimSpace(input)
number, err := strconv.ParseFloat(input, 64)
if err != nil {

return 0, err
}
return number, nil
}
func main() {
fmt.Print("Enter a temperature in Fahrenheit: ")
fahrenheit, err := getFloat()
Вызываем getFloat для получения температуры.
if err != nil {
Если функция вернула ошибку, программа

log.Fatal(err)
сообщает об этом и завершается.
Цельсия...
}
разуется к шкале
Температура преоб
celsius := (fahrenheit - 32) * 5 / 9
fmt.Printf("%0.2f degrees Celsius\n", celsius)
}
...и выводится с двумя знаками
после точки.

Enter a temperature in Fahrenheit: 98.6
37.00 degrees Celsius
дальше 4   149

создание пакетов

Пакеты и повторное использование кода в программах
И снова повторяющийся код... Если когда-нибудь
в функции getFloat обнаружится ошибка, ее придется исправлять в двух местах. С другой стороны,
это две разные программы, и наверное, с этим
ничего не сделать...
func getFloat() (float64, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {

return 0, err
}
input = strings.TrimSpace(input)
number, err := strconv.ParseFloat(input, 64)
if err != nil {

return 0, err
}
return number, nil
}

На самом деле кое-что сделать можно, а именно
переместить общую функцию в новый пакет!
Go позволяет нам определять собственные пакеты. Как упоминалось в главе 1, пакет представляет собой блок кода, который
выполняет похожие операции. Пакет fmt форматирует вывод,
пакет math работает с числами, пакет strings работает со
строками и т. д. Мы уже использовали функции из всех этих
пакетов в своих программах.
Возможность повторного использования кода в разных программах — одна из основных причин для существования пакетов. Если какие-то части вашего кода используются в разных
программах, возможно, их стоит переместить в пакеты.

Если какие-то части вашего кода
используются в разных программах,
возможно, их стоит переместить в пакеты.
150 глава 4

запаковка кода

Хранение кода пакетов
Инструменты Go ищут код пакетов в специальном каталоге (папке) на вашем компьютере,
который называется рабочей областью. По умолчанию рабочей областью является каталог
с именем go в домашнем каталоге текущего пользователя.
Каталог рабочей области содержит три подкаталога:


bin для хранения откомпилированных двоичных исполняемых программ. (Каталог bin
будет подробно описан позднее в этой главе.)



pkg для хранения откомпилированных двоичных файлов пакетов. (Каталог pkg также
будет описан в этой главе.)



src для хранения исходного кода Go.

В каталоге src код каждого пакета размещается в отдельном подкаталоге. По соглашениям
имя подкаталога должно совпадать с именем пакета (так что код пакета gizmo должен храниться в подкаталоге с именем gizmo).
Подкаталог каждого пакета должен содержать один или несколько файлов с исходным
кодом. Имена файлов могут быть любыми, но они должны иметь расширение .go.

(домашний каталог пользователя)
Каталог рабочей области.

go
bin

Исполняемые программы.

pkg

Откомпилированный код пакетов.

src

часто

В:

Задаваемые
вопросы

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

О:

Исходный код.
doodad
gizmo
gizmo.go
plug.go

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

Все что угодно! Весь код пакета можно
хранить в одном файле, а можно разбить
его на несколько файлов. В любом случае
они станут частью одного пакета.
дальше 4   151

создание пакетов

Создание нового пакета
Попробуем создать пакет в рабочей области. Это будет простой пакет с именем greeting, который
выводит приветствия на разных языках.
При установке Go каталог рабочей области не создается по умолчанию, так что вам придется создать
его самостоятельно. Для начала перейдите в свой домашний каталог. (Путь имеет вид C:\Users\ в большинстве систем Windows, /Users/ на Mac и /home/
в большинстве систем Linux.) В домашнем каталоге создайте каталог с именем go — он станет новым
каталогом рабочей области. Внутри каталога go создайте каталог с именем src.
Наконец, нам понадобится каталог для хранения кода пакета. По правилам имя каталога пакета должно
совпадать с именем пакета. Так как наш пакет будет называться greeting, это имя должно быть присвоено и каталогу.
Да, мы знаем, кажется, что вложенных каталогов слишком много (а на самом деле их будет еще больше).
Но поверьте, после того как вы построите коллекцию собственных пакетов, а также пакетов из других
источников, такая структура поможет сохранить нормальную организацию кода.
И что еще важнее, эта структура помогает инструментам
(домашний каталог пользователя)
Go находить код. Поскольку
код всегда находится в каталоСоздайте этот каталог
go
ге src, инструменты Go точно
в домашнем каталоге.
знают, где следует искать код
src
импортируемых пакетов.
Создайте этот каталог в каталоге «go».
Затем создайте файл в катаgreeting
Создайте в каталоге «src».
логе greeting и присвойте ему
имя greeting.go. Файл должен
Сохраните этот файл
greeting.go
содержать код, приведенный
в каталоге «greeting».
ниже. Вскоре мы рассмотрим
его более подробно, а пока
следует обратить внимание на пару моментов...
Как и все файлы с исходным кодом, встречавшиеся вам до этого, файл начинается с директивы package.
Но в отличие от других файлов, этот код не является частью пакета main; он принадлежит пакету
с именем greeting.
Также обратите внимание на
определения двух функций.
Они мало чем отличаются
от других функций, встречавшихся вам ранее. Но так
как мы хотим, чтобы к ним
можно было обращаться за
пределами пакета greeting,
их имена начинаются с букв
верхнего регистра, чтобы эти
функции экспортировались.
152 глава 4

package greeting
import "fmt"

Принадлежит не пакету «main»,
а пакету «greeting»!

func Hello() {
fmt.Println("Hello!")
}

Первые буквы в верхнем регистре, чтобы функции экспортировались!

func Hi() {
fmt.Println("Hi!")
}
greeting.go

запаковка кода

Импорт пакета в программу
Теперь попробуем использовать новый
пакет внутри программы. В каталоге
рабочей области, внутри подкаталога src,
создайте новый подкаталог с именем hi.
(Хранить код исполняемых программ
внутри рабочей области необязательно,
но это хорошая мысль.)

(домашний каталог пользователя)
go
src

Теперь в новом каталоге hi необходимо создать
новый исходный файл. Этому файлу можно присвоить любое имя (важно лишь, чтобы оно имело
расширение .go), но поскольку программа будет
оформлена в виде исполняемой команды, мы
присвоим ему имя main.go. Сохраните в файле код,
приведенный ниже.

greeting

hi

greeting.go
Создается внутри каталога «src», на одном уровне
с «greeting».
main.go

Сохраните файл
внутри «hi».

Как и любой файл с исходным
и
кодом Go, этот код начинается с
package main
Чтобы использовать функци
чала
директивы package. Но поскольпакета, его необходимо сна
import "greeting"
импортировать.
ку предполагается, что это будет
исполняемая команда, необходиПри вызове функций других
func main() {
мо использовать имя пакета main.
пакетов необходимо укаgreeting.Hello()
зать имя пакета и точку.
В общем случае имя пакета должно
greeting.Hi()
соответствовать имени каталога,
}
в котором он хранится, но пакет
main является исключением из этого правила.
main.go
Затем необходимо импортировать пакет greeting, чтобы использовать его функции. Инструменты
Go ищут код пакета в подкаталоге каталога src рабочей области, имя которого соответствует имени в
команде import. Чтобы приказать Go искать код в каталоге src/greeting из каталога рабочей области,
мы используем директиву import "greeting".
Наконец, поскольку пакет содержит исполняемый код, нам понадобится функция main, которая вызывается при запуске программы. В main вызываются обе функции, определенные в пакете greeting.
В обоих вызовах указывается имя пакета и точка, чтобы компилятор Go знал, частью какого пакета
являются функции.
Все готово! Попробуем запустить программу. В окне терминала или командной строки
Вызов функций
введите команду cd, чтобы перейти в каталог
из пакета!
src/hi в каталоге рабочей области. (Путь зависит от местонахождения домашнего каталога.)
Затем запустите программу командой go run main.go.

Shell Edit View Window Help

$ cd /Users/jay/go/src/hi
$ go run main.go
Hello!
Hi!
$

При достижении строки import "greeting" Go обращается к каталогу greeting в каталоге src вашего
рабочего каталога и ищет в нем исходный код пакета. Этот код компилируется и импортируется, после
чего мы получаем возможность вызывать функции пакета greeting!
дальше 4   153

содержимое файла пакета

Файлы пакетов имеют одинаковую структуру
Помните, в главе 1 говорилось о трех разделах, которые присутствуют практически во всех файлах с исходным кодом Go?

Вы быстро привыкнете к тому, что эти
три части (именно в таком порядке)
встречаются практически во всех файлах Go, с которыми вы будете работать:
1.
2.
3.

Директива package.

package main

Директива import.

import "fmt"

Директива package.
Директива import.

Собственно код программы.

Собственно код
программы.

func main() {
fmt.Println("Hello, Go!")
}

Естественно, это правило выполняется и для пакета main в файле main.go.
В нашем коде присутствует директива package, за которой следует директива import и собственно код нашего пакета.
Директива package. package main
Директива import. import "greeting"

func main() {
greeting.Hello()
Собственно код
greeting.Hi()
программы.
}

Другие пакеты, кроме main, имеют тот же формат. Как видите,
файл greeting.go тоже содержит директиву package, директиву
import и завершается самим кодом пакета.
Директива package. package greeting
Директива import. import "fmt"

func Hello() {
fmt.Println("Hello!")
}

Собственно код
программы. func Hi() {
fmt.Println("Hi!")
}

154 глава 4

запаковка кода

Сломай и изучи!

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

greeting

hi

greeting.go

main.go

package greeting

package main

import "fmt"

import "greeting"

func Hello() {
fmt.Println("Hello!")
}

func main() {
greeting.Hello()
greeting.Hi()
}

func Hi() {
fmt.Println("Hi!")
}

Если...

Изменить имя каталога
greeting

...программа не будет работать, потому что...
greeting
salutation

Изменить имя в строке
package файла greeting. package salutation
go

Преобразовать имена
функций в файлах
greeting.go и main.go
к нижнему регистру

func Hhello()
func Hhi()

greeting.Hhello()
greeting.Hhi()

Инструменты Go используют имя в пути импорта
как имя каталога, из которого должен загружаться
исходный код пакета. Если имена не совпадают, то
код не загрузится
Содержимое каталога greeting загрузится как пакет
с именем salutation. Но поскольку в вызовах функций в main.go упоминается пакет greeting, вы получите сообщения об ошибке
Функции, имена которых начинаются с букв нижнего
регистра, не экспортируются — это означает, что они
могут использоваться только в своем пакете. Чтобы
в программе можно было использовать функцию из
другого пакета, эта функция должна экспортироваться, а для этого ее имя должно начинаться с буквы
верхнего регистра

дальше 4   155

о пакетах

У бассейна
Выловите из бассейна фрагменты кода и разместите их в пустых строках кода. Каждый фрагмент может использоваться только один раз; использовать все фрагменты необязательно. Ваша задача: создать в рабочей области Go пакет calc, чтобы функции могли использоваться в коде main.go.
(домашний каталог пользователя)

package

Запишите
здесь имена
каталогов!

func

(first float64, second float64) float64 {
return first + second

}
func

(first float64, second float64) float64 {
return first - second

}

calc.go

calc.go
package main
import (
"calc"
"fmt"
)
func main() {
fmt.Println(calc.
fmt.Println(calc.
}

main.go

Примечание: каждый
предмет из бассейна
может использоваться
только один раз!

go
source

Результат.

(1, 2))
(7, 3))

add
add

calc

Subtract

Subtract

main
calc

src

Add

Add

gopath

subtract

subtract

Ответ на с. 181.
156 глава 4

3
4

запаковка кода

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


Имя пакета должно быть записано только символами нижнего регистра.



Имя следует сокращать, если его смысл очевиден (например, fmt).



По возможности имя должно состоять из одного слова. Если необходимы два слова, они
не должны разделяться символами подчеркивания, а второе слово не должно начинаться
с буквы верхнего регистра (пример — пакет strconv).



Импортированные имена пакетов могут конфликтовать с именами локальных переменных, поэтому не используйте имя, которое с большой вероятностью может быть выбрано
пользователями пакета. (Например, если бы пакет fmt назывался format, то импорт этого
пакета создавал бы риск конфликта с локальной переменной format.)

Уточнение имен
При обращении к функции, переменной или чему-то еще, экспортируемому из другого пакета, необходимо уточнить имя функции или переменной, поставив перед именем функции
или переменной имя пакета. Но при обращении к функции или переменной, определенной
в текущем пакете, не следует уточнять имя пакета.
hi

greeting
main.go

Так как в нашем файле
package main
main.go код является частью пакета main, необхоimport "greeting"
димо указать, что функции
Hello и Hi принадлежат
func main() {
пакету greeting — для
Пакетные greeting.Hello()
классифиэтого следует использокаторы. greeting.Hi()
вать имена greeting.
}
Hello и greeting.Hi.
Но предположим, что функции Hello и Hi вызываются из
другой функции пакета greeting. В этом случае достаточно
указать имена Hello и Hi (без классификатора, то есть уточняющего имени пакета), потому что вызываются функции
из того пакета, в котором были определены.

greeting.go
package greeting
import "fmt"
func Hello() {
fmt.Println("Hello!")
}
func Hi() {
fmt.Println("Hi!")
}
func AllGreetings() {

Классифи- Hello()
каторы отсутствуют. Hi()

}

дальше 4   157

использование пакетов

Перемещение общего кода в пакет
Теперь вы понимаете, как добавлять новые
пакеты в рабочую область Go, и мы наконец готовы вынести функцию getFloat
в пакет, который может использоваться
программами pass_fail.go и tocelsius.go.

(домашний каталог пользователя)
go
src

Назовем наш пакет keyboard, так как он
предназначен для чтения ввода с клавиатуры. Начнем с создания нового каталога
с именем keyboard в каталоге src рабочей
области.

keyboard

Создайте этот
каталог в «src».

keyboard.go

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

Сохраните
этот файл
в каталоге
«keyboard».

В начало файла необходимо добавить директиву package с именем
пакета: keyboard.
Так как мы создаем отдельный файл, необходимо включить директиву
import со всеми пакетами, используемыми в коде: bufio, os, strconv
и strings. (Пакеты fmt и log
не указываются, так как они
package keyboard
Добавляем директиву package.
используются только в файлах
pass_fail.go и tocelsius.go.)
import (
Наконец, код старой функции getFloat можно скопировать в неизменном виде.
Но не забудьте переименовать функцию в GetFloat,
чтобы функция экспортировалась, она должна начинаться с буквы верхнего регистра.

Импортируются только
пакеты, используемые в этом
файле.

)

"bufio"
"os"
"strconv"
"strings"

буквы верхнего
Имя функции начинается с
ортировалась.
эксп
кция
фун
бы
регистра, что

func GetFloat() (float64, error) {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {

return 0, err
Этот код
}

идентичен
старому
input = strings.TrimSpace(input)
(дублированnumber, err := strconv.ParseFloat(input, 64)
нуому) коду
if err != nil {
функци
и.

return 0, err

}

}
return number, nil

keyboard.go

158 глава 4

запаковка кода

Перемещение общего кода в пакет (продолжение)
Теперь можно обновить программу pass_fail.go, чтобы в ней
использовался новый пакет
keyboard.

// pass_fail сообщает, сдал ли пользователь экзамен.
package main

В функции main старый вызов
g e t F l o a t заменяется вызовом новой функции keyboard.
GetFloat. Остальной код — без
изменений.

var status string
if grade >= 60 {

status = "passing"
} else {

status = "failing"
}
fmt.Println("A grade of", grade, "is", status)
}

Не забудьте импортиimport (
ровать новый пакет.
"fmt"
Импортируются только пакеты,
"keyboard"
используемые в этом файле.
"log"
)
Так как мы удаляем старую функь.
Функцию getFloat, которая здесь находилась, можно удалит
цию getFloat, также необхо- func main() {
димо удалить неиспользуемые
fmt.Print("Enter a grade: ")
директивы bufio, os, strconv
grade, err := keyboard.GetFloat()
и strings. Вместо них будет
if err != nil {
Вызываем функцию

log.Fatal(err)
импортироваться новый пакет
из пакета «keyboard».
}
keyboard.

При запуске обновленной программы будет выведен тот же
результат.
Те же изменения вносятся в программу tocelsius.go.

Enter a grade: 89.7
A grade of 89.7 is passing

// tocelsius преобразует температуру...
package main

import (
Не забудьте импортиИмпортируются только пакеты, "fmt"
ровать новый пакет.
используемые в этом файле. "keyboard"
"log"
)
Мы обновляем директивы имь.
Функцию getFloat, которая здесь находилась, можно удалит
порта, удаляем старую функцию
func main() {
getFloat и вызываем вместо нее
fmt.Print("Enter a temperature in Fahrenheit: ")
keyboard.GetFloat.
fahrenheit, err := keyboard.GetFloat()
if err != nil {
Вызываем функ
log.Fatal(err)
цию
из пакета
И снова при запуске обновлен}
«keyboard».
ной программы будет получен
celsius := (fahrenheit - 32) * 5 / 9
тот же результат. Но на этот раз
fmt.Printf("%0.2f degrees Celsius\n", celsius)
вместо дублирующегося кода бу- }

дет использоваться функция из
нашего нового пакета!

Enter a temperature in Fahrenheit: 98.6
37.00 degrees Celsius

дальше 4   159

пакетные константы

Константы
Многие пакеты экспортируют константы: именованные значения, которые не изменяются за время
работы программы. Объявление константы очень похоже на объявление переменной: в нем также указывается имя, необязательный тип и значение. Тем не менее правила несколько отличаются:




Вместо ключевого слова var используется ключевое слово const.
Значение константы должно быть задано в момент ее объявления. Вы не сможете присвоить его
позднее, как с переменными.
Для переменных доступен синтаксис короткого объявления :=, а у констант аналогичной конструкции не существует.
Ключевое
слово «const».

Имя
константы.

Тип.

Значение.

const TriangleSides int = 3
Как и при объявлении переменных, тип можно опустить, он будет
автоматически определен по присваиваемому значению:
const SquareSides = 4

Присваивается целое число, поэтому для
константы будет выбран тип «int».

Значение переменной может изменяться, но значение константы должно оставаться
постоянным. Попытка присвоить новое значение константе приведет к ошибке компиляции. Эта особенность констант обеспечивает безопасность: константы должны
использоваться для значений, которые не должны изменяться.
const PentagonSides = 5
PentagonSides = 6

Попытка присвоить
новое значение константе!

Ошибка компиляции.

cannot assign to PentagonSides

Если ваша программа включает «фиксированные» значения литералов (особенно
если эти значения используются в нескольких местах), подумайте о том, чтобы заменить их константами (даже если программа не разбита на пакеты). Ниже приведен
пакет с двумя функциями, в обеих функциях целочисленный литерал 7 представляет
количество дней в неделе:
dates
dates.go

160 глава 4

package dates

Получаем количество недель.

func WeeksToDays(weeks int) int {
Умножаем на количество дней в неделе,
}
чтобы вычислить общее количество дней.
func DaysToWeeks(days int) float64 {
Делим на количество дней
return float64(days) / float64(7)
в неделе, чтобы получить
}
количество недель.

weeks * 7
Получаем колreturn
ичество дней.

запаковка кода

Константы (продолжение)
Заменяя литералы константой DaysInWeek, мы тем самым документируем их смысл. (Другие разработчики увидят имя DaysInWeek и немедленно поймут, что число 7 было выбрано неслучайно.) Кроме того,
если позднее в программу будут добавлены новые функции, для предотвращения возможных расхождений
можно использовать константу DaysInWeek и в этих функциях.
Обратите внимание: константа объявляется за пределами любых функций, на уровне пакета. И хотя
константу можно объявить внутри функции, это ограничит ее область видимости блоком этой функции.
Вариант с объявлением констант на уровне пакета гораздо более типичен, так как эти константы будут
доступны для всех функций этого пакета.
dates
dates.go

package dates
const DaysInWeek int = 7

Объявляем константу.

вместо
Константа используется
func WeeksToDays(weeks int) int { целочисленного литерала.
return weeks * DaysInWeek
Константа используется
}
вместо целочисленного
func DaysToWeeks(days int) float64 {
литерала.
return float64(days) / float64(DaysInWeek)
}

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

package main
import (
"dates"
"fmt"
)

ором
Импортируем пакет, в кот
объявлена константа.

func main() {
days := 3
fmt.Println("Your appointment is in", days, "days")
fmt.Println("with a follow-up in", days + dates.DaysInWeek, "days")
}
Используется константа из пакета
Уточняется именем пакета.
«dates».

Your appointment is in 3 days
with a follow-up in 10 days
дальше 4   161

пути импорта пакетов

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

Однако путь импорта и имя пакета могут различаться. Многие пакеты Go классифицируются
по категориям, например «сжатие» или «комплексные вычисления». По этой причине они
часто группируются по префиксам пути импорта
(например, "archive/" или "math/"). (Их можно рассматривать как аналоги путей каталогов
на жестком диске.)

Некоторые наборы пакетов группируются по
префиксам путей импорта, например "archive/"
или "math/". Мы предлагали рассматривать эти
префиксы как аналоги путей каталогов на жестком
диске... и это неслучайно. Эти префиксы путей импорта формируются на основе структуры каталогов!
Группы взаимосвязанных пакетов можно разместить в каталоге в рабочей области Go. Этот каталог становится частью пути импорта для всех
содержащихся в нем пакетов.

"math"

Имя пакета
archive
tar
zip
math

"math/rand"

rand

Путь импорта

"archive"

"archive/tar"
"archive/zip"

cmplx

"math/cmplx"

(домашний каталог пользователя)
go
src
greeting

Допустим, вы хотите добавить пакеты для вывода
приветствий на дополнительных языках. Если
все эти пакеты будут размещаться прямо в пакете
src, он быстро станет слишком громоздким и неудобным. Но если разместить все новые пакеты в
каталоге greeting, они будут удобно организованы.
Размещение пакетов в каталоге greeting также влияет на их путь импорта. Например, если бы пакет
dansk хранился непосредственно в каталоге src,
то ему бы соответствовал путь импорта "dansk". Но если разместить его
в каталоге greeting, путь импорта преобразуется к виду "greeting/dansk".
Переместите пакет deutsch в каталог greeting, и ему будет назначен путь
импорта "greeting/deutsch". Исходный пакет greeting будет доступен
по пути импорта "greeting" при условии, что файл с его исходным кодом
хранится прямо в каталоге greeting (а не в одном из его подкаталогов).

162 глава 4

Код пакета
«greeting/
dansk».
dansk.go

dansk

deutsch

Код пакета
«greeting/
deutsch».

deutsch.go
greeting.go

Код исходного
пакета «greeting».

запаковка кода

Вложенные каталоги и пути импорта пакетов (продолжение)
Предположим, имеется пакет deutsch, вложенный в каталог
пакета greeting, и его код выглядит примерно так:
src

package deutsch
greeting

import "fmt"

deutsch

Код пакета «greeting/
deutsch».

deutsch.go

func GutenTag() {
fmt.Println("Guten Tag!")
}

greeting.go
hi

Код исходного пакета «greeting».

main.go

func Hallo() {
fmt.Println("Hallo!")
}

Наша программа, использующая эти пакеты.

Давайте обновим код hi/main.go, чтобы в нем
также использовался пакет deutsch. Поскольку каталог этого пакета вложен в каталог
greeting, необходимо использовать путь импорта "greeting/deutsch". Но после того, как
пакет будет импортирован, к нему можно будет
обращаться просто по имени пакета: deutsch.

deutsch.go

package main

Импортируем пакет
«greeting», как и прежде.
import (
Также импор"greeting"
"greeting/deutsch"
тируем па)
кет «deutsch».

func main() {
greeting.Hello()
greeting.Hi()
deutsch.Hallo()
deutsch.GutenTag()
}

Добавляем вызовы
функций нового
пакета.

main.go

Как и прежде, чтобы запустить код, мы используем команду cd для перехода в каталог src/hi
каталога рабочей области, после чего запускаем программу командой go run main.go.
Результаты вызовов функций пакета deutsch
видны в выходных данных.

Shell Edit View Window Help

Результат
пакета
«deutsch».

$ cd /Users/jay/go/src/hi
$ go run main.go
Hello!
Hi!
Hallo!
Guten Tag!
дальше 4   163

go install

Установка исполняемых файлов командой «go install»
При использовании команды go run программа должна быть сначала откомпилирована, как и все пакеты,
от которых она зависит. И весь откомпилированный код будет потерян после завершения программы.
В главе 1 была описана команда go build, которая компилирует и сохраняет исполняемый двоичный файл
(файл, который может выполняться даже без установки Go). Но если вы будете использовать ее слишком
часто, ваша рабочая область будет забита исполняемыми файлами в случайных и неподходящих местах.
Команда go install также сохраняет откомпилированные бинарные версии исполняемых программ,
но в четко определенном и легкодоступном месте: в каталоге bin вашей рабочей области Go. Просто передайте go install имя каталога из src, содержащего код исполняемой программы (то есть файлов .go,
начинающихся с package main). Программа будет откомпилирована, а исполняемый файл будет сохранен в стандартном каталоге.
Попробуем установить исполняемый файл для нашей программы hi/main.go. Как и прежде, введите
в терминале команду go install, пробел и имя подкаталога в каталоге src (hi). Еще раз подчеркнем:
неважно, из какого каталога вы это сделаете — компилятор будет искать каталог внутри каталога src.
Shell Edit View Window Help

$ go install hi
$

(Не забудьте, что команде «go install» должно передаваться имя каталога в «src», а не имя файла .go!
По умолчанию команда «go install» не настроена для
прямой работы с файлами .go.)

(домашний каталог пользователя)
go

Когда компилятор Go видит, что файл в каталоге hi
содержит объявление package main, он понимает,
что это код исполняемой программы. Он компилирует
исполняемый файл и сохраняет результат в каталоге
с именем bin в рабочей области Go. (Каталог bin будет
создан автоматически, если он до этого не существовал.)
В отличие от команды go build, которая присваивает
исполняемому файлу имя файла .go, на основе которого
он был создан, go install назначает имя исполняемого
файла по имени каталога, содержащего код. Так как мы
компилируем содержимое каталога hi, исполняемый
файл будет назван hi (или hi.exe в Windows).
Теперь воспользуйтесь командой cd для перехода к каталогу
bin в рабочей области Go. В каталоге bin исполняемый файл
запускается командой ./hi (или hi.exe в Windows).
Shell Edit View Window Help

$ cd /Users/jay/go/bin
$ ./hi
Hello!
Hi!
Hallo!
Guten Tag!

164 глава 4

(Часть файлов/каталогов пропущена для экономии места.)

Создается автоматически.

bin

Ваш откомпилированный исполняемый
файл. (Он будет
называться hi.exe
в Windows.)

hi
pkg
src
greeting

greeting.go
hi
main.go

Вы также можете включить каталог «bin» своей рабочей области в системную переменную «PATH». Тогда
вы сможете запускать исполняемые файлы из «bin»
из любого каталога вашей системы! Современные программы установки Go для Mac и Windows автоматически вносят изменения в «PATH».

запаковка кода

Переменная GOPATH и смена рабочих областей
На разных веб-сайтах можно увидеть, как разработчики говорят о «настройке GOPATH»
при обсуждении рабочей области Go. GOPATH — переменная среды, к которой инструменты Go обращаются за информацией о местонахождении рабочей области. Большинство разработчиков хранит весь свой код Go в одной рабочей области и не меняет
ее местонахождения по умолчанию. Но при желании можно использовать GOPATH для
перемещения рабочей области в другой каталог.
Переменная среды предназначена для хранения и чтения значений (как и переменные
Go), но управляет ею операционная система, а не Go. Некоторые программы настраиваются при помощи переменных среды; к их числу относится и компилятор Go.
Допустим, вместо домашнего каталога вы разместили свой пакет greeting в каталоге
code в корневом каталоге своего жесткого диска. И теперь вы хотите запустить файл
main.go, который зависит от greeting.

Нестандартная
рабочая область!

code

package main
import "greeting"

src
greeting
greeting.go

func main() {
greeting.Hello()
greeting.Hi()
}

main.go

Но вы получаете сообщение об ошибке. В нем говорится, что
пакет greeting не найден, так как компилятор продолжает
искать пакет в подкаталоге go вашего домашнего каталога:

Shell Edit View Window Help

$ go run main.go
command.go:3:8: cannot find package "greeting" in any of:
/usr/local/go/libexec/src/greeting (from $GOROOT)
/Users/jay/go/src/greeting (from $GOPATH)

дальше 4   165

использование gopath

Настройка GOPATH
Если ваш код хранится в другом каталоге (вместо каталога по умолчанию), вы должны настроить компилятор Go и передать информацию о
местонахождении кода. Для этого можно воспользоваться переменной
среды GOPATH, а конкретный способ зависит от операционной системы.

Системы Mac и Linux systems:

Системы Windows:

Для настройки переменной среды используется команда export. В приглашении терминала введите команду:

Для настройки переменной среды используется команда set. В приглашении
командной строки введите команду:

export GOPATH="/code"

set GOPATH="C:\code"

Для каталога с именем code в корневом
каталоге жесткого диска используется
путь «/code». Если вы храните код
в другом месте, укажите нужный путь.

Для каталога с именем code в корневом
каталоге жесткого диска используется
путь «C:\code». Если вы храните код
в другом месте, укажите нужный путь.

После того как это будет сделано, при выполнении команды go run
(как и других средств Go) будет использоваться рабочая область, определяемая заданным каталогом. Таким образом, библиотека greeting
будет найдена, а программа успешно выполнится!
В Mac/Linux.

Shell Edit View Window Help

$ export GOPATH="/code"
$ go run main.go
Hello!
Hi!

В Windows.

Shell Edit View Window Help

C:\Users\jay>set GOPATH="C:\code"
C:\Users\jay>go run main.go
Hello!
Hi!

Обратите внимание: описанные выше способы настраивают переменную среды
GOPATH только для текущего окна терминала/командной строки. Вам придется
настраивать ее заново для каждого нового окна. Впрочем, есть способы выполнить постоянную настройку переменной среды. Конкретный способ также
зависит от операционной системы, и у нас нет возможности описывать их
здесь. Введите строку «переменные среды» и название вашей ОС в поисковой
системе — вы наверняка найдете полезные инструкции в результатах поиска.

166 глава 4

запаковка кода

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

(домашний каталог пользователя)
package keyboard
import (
"bufio"
"os"
"strconv"
"strings"
)

go
src
keyboard
keyboard.go

func GetFloat() (float64, error) {
// Код GetFloat...
}

Создадим репозиторий для хранения нашего кода на GitHub —
популярном веб-сайте, специализирующемся на публикации и
распространении кода. Другие разработчики смогут загрузить
пакет и использовать его в своих проектах! На GitHub мы
используем имя пользователя headfirstgo, а репозиторий
назовем keyboard, поэтому URL-адрес будет выглядеть так:
https://github.com/headfirstgo/keyboard
Мы сохраним в репозитории только файл keyboard.go без каких-либо дополнительных подкаталогов.

URL-адрес репозитория.

Имя пользователя
GitHub — «headfirstgo».

Репозиторию присвоено имя
«keyboard» (по имени пакета).

Загружается только исходный
файл без каталогов.

дальше 4   167

предотвращение конфликтов имен пакетов

Публикация пакетов (продолжение)
Спасибо, но кажется, мы не сможем пользоваться
вашим пакетом. В моем приложении для магазина
музыкальных инструментов уже есть пакет keyboard,
и если я установлю ваш пакет keyboard, возникнет
конфликт!

Да, опасения вполне обоснованны. В каталоге src рабочего
пространства Go может быть только один каталог keyboard.
Похоже, может быть только один пакет с именем keyboard!

Погодите... А если снова воспользоваться
вложением каталогов? В одном каталоге будет
храниться наш пакет keyboard, а в другом —
их пакет keyboard!

src
ours

Создаем новый
каталог...
…и перемещаем
keyboard
в него свой пакет keyboard!
keyboard.go

theirs

Создаем новый
каталог...
…и перемещаем
keyboard
в него их пакет
keyboard!
keyboard.go

Но как назвать каталоги, в которых
содержатся пакеты? Где их пакеты, где
наши?

168 глава 4

запаковка кода

Публикация пакетов (продолжение)
Возможно, нам бы пригодился более универсальный идентификатор
автора пакета. Наш пакет keyboard доступен только по адресу

http://github.com/headfirstgo/keyboard.

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

(домашний каталог пользователя)
go
src
Каталог для имени
домена...

github.com
headfirstgo
Тогда мое приложение может
использовать каталоги с именами компонентов URL-адреса,
по которому размещен наш пакет
keyboard. Никаких конфликтов.
Мне нравится!

keyboard

Каталог для имени
пользователя...
Сюда перемещается
каталог package.

Этот файл изменять не нужно!

keyboard.go

Давайте попробуем переместить наш пакет в структуру каталогов, представляющих URL-адрес его размещения. Создайте в каталоге src другой
каталог с именем github.com. Внутри этого каталога создайте каталог с
именем следующего сегмента URL-адреса headfirstgo. А после этого переместите каталог keyboard из каталога src в каталог headfirstgo.
Хотя перемещение пакета в новый подкаталог
изменяет его путь импорта, оно не изменит имя
пакета. А раз в самом пакете для обращений к
пакету используется только имя, вносить изменения в коде пакета не придется!

package keyboard
import (
"bufio"
"os"
"strconv"
"strings"
)

Имя пакета не изменилось, поэтому изменять
код пакета не нужно.

keyboard.go

// Сюда перемещается код
keyboard.go...

дальше 4   169

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

Публикация пакетов (продолжение)
Тем не менее нам придется обновить программы, зависящие от этого пакета, потому что
путь импорта программы изменился. Так как имена подкаталогов были выбраны по частям
URL-адреса, по которому размещается пакет, новый путь импорта очень похож на URL:
"github.com/headfirstgo/keyboard"

Необходимо только обновить
директиву import в каждой
программе. Так как имя пакета
осталось неизменным, все обращения к пакету в остальном коде
не изменяются.
После внесения этих изменений все программы, зависящие
от нашего пакета keyboard,
должны работать нормально.

// pass_fail сообщает, сдал ли пользователь экзамен.
package main
import (
"fmt"
"github.com/headfirstgo/keyboard"
"log"
)

Обновляем путь
импорта.

func main() {
fmt.Print("Enter a grade: ")
grade, err := keyboard.GetFloat()
if err != nil {
Здесь ничего не изменилось: имя

log.Fatal(err)
}
пакета осталось прежним.
// ...
}

Enter a grade: 89.7
A grade of 89.7 is passing

// tocelsius converts a temperature...
package main

Конечно, нам хотелось бы быть
авторами идеи об использовании доменных имен и путей для
обеспечения уникальности путей импорта, но на самом деле
это не наша заслуга. Сообщество
Go с первых дней использовало
эту схему именования пакетов
как стандартную. Аналогичные
схемы уже десятилетиями применяются в других языках (например, в Java).
170 глава 4

import (
"fmt"
"github.com/headfirstgo/keyboard"
"log"
)

Обновляем путь
импорта.

func main() {
fmt.Print("Enter a temperature in Fahrenheit: ")
fahrenheit, err := keyboard.GetFloat()
if err != nil {
Здесь ничего не изме
log.Fatal(err)
нило
сь: имя пакета
}
оста
лось прежним.
// ...
}

Enter a temperature in Fahrenheit: 98.6
37.00 degrees Celsius

запаковка кода

Загрузка и установка пакетов командой «go get»
У использования URL-адреса размещения пакета в качестве пути импорта есть еще одно преимущество.
У команды go существует еще одна субкоманда go get, которая может автоматически загружать и устанавливать пакеты за вас.
Создадим репозиторий Git для описанного выше пакета greeting по следующему URL-адресу:
https://github.com/headfirstgo/greeting
Это означает, что на любом компьютере с установленным экземпляром Go необходимо ввести в терминале следующую команду:
go get github.com/headfirstgo/greeting
После команды go get следует URL-адрес репозитория с отсеченным сегментом «схемы» («https://»).
Команда подключается к github.com, загружает репозиторий Git по пути /headfirstgo/greeting и сохраняет
его в каталоге src вашей рабочей области Go. (Примечание: если в вашей системе не установлена поддержка Git, вам будет предложено установить ее при выполнении команды go get. Просто следуйте
инструкциям на экране. Команда go get также работает с репозиториями Subversion, Mercurial и Bazaar.)
Команда go get автоматически создает
подкаталоги, необходимые для настройки
подходящего пути импорта (каталог github.
com, каталог headfirstgo и т. д.). Пакеты, сохраненные в каталоге src, выглядят так:
Пакеты, сохраненные в рабочей области Go,
готовы к использованию в программах. Например, если вы хотите использовать пакеты
greeting, dansk и deutsch в программе,
включите в нее директиву импорта, которая
выглядит примерно так:

(домашний каталог
пользователя)
go

(Примечание: иногда
команда «go get» не находит Git после установки.
В таком случае попробуйте закрыть окно терминала или командной строки
и открыть его заново.)

src
github.com

import(
"github.com/headfirstgo/greeting"
"github.com/headfirstgo/greeting/dansk"
"github.com/headfirstgo/greeting/deutsch"
)

Команда go get работает также и с другими пакетами. Если вы еще не
настроили пакет keyboard так, как было показано выше, команда установит его:
go get github.com/headfirstgo/keyboard
Команда go get работает с любыми пакетами, которые были правильно
настроены в сервисе размещения, независимо от того, кто был его автором. Все, что для этого нужно — выполнить команду go get и передать ей
путь импорта пакета. Команда анализирует часть пути, соответствующую
адресу хоста, подключается к хосту и загружает пакет по URL-адресу, представленному остальной частью пути импорта. Это значительно упрощает
использование кода других разработчиков!

headfirstgo
greeting
dansk
dansk.go
deutsch
deutsch.go
greeting.go

дальше 4   171

пути импорта пакетов

Мы создали рабочую область Go с простым пакетом mypackage. Завершите приведенную программу, чтобы она импортировала пакет mypackage и вызывала его функцию MyFunction.

Упражнение

(домашний каталог пользователя)
go
src
my.com
me
package mypackage

myproject
mypackage
mypackage.go

func MyFunction() {
}

mypackage.go

Запишите свой код:
package main
import
func main() {
}

Ответ на с. 181.
172 глава 4

запаковка кода

Чтение документации пакетов командой «go doc»
Я установил ваш пакет

keyboard. Но я понятия

не имею, как им пользоваться! Где
можно найти эту информацию?

Введите команду go doc, чтобы вывести документацию
по любому пакету или функции.

Чтобы вывести документацию по пакету, передайте его путь импорта команде go doc.
Например, информация о пакете strconv выводится командой go doc strconv.

Вывести документацию
по пакету strconv.

Имя и путь импорта
пакета.

(Часть вывода пропущена для экономии места.)
Shell Edit View Window Help

$ go doc strconv
package strconv // import "strconv"
Package strconv implements conversions to and from
string representations of basic data types.
Numeric Conversions

Описание пакета.

The most common numeric conversions are Atoi (string
to int) and Itoa (int to string).
i, err := strconv.Atoi("-42")
s := strconv.Itoa(-42)
[...Further description of the package here...]

Включенные функции.

[...Function names...]
func Itoa(i int) string
func ParseBool(str string) (bool, error)
func ParseFloat(s string, bitSize int) (float64, error)
[...More function names...]

В выходных данных приводится имя и путь импорта пакета (в данном случае
это одно и то же), описание пакета в целом и список всех функций, экспортируемых пакетом.
дальше 4   173

go doc

Чтение документации пакета командой «go doc» (продолжение)
Команда go doc также может использоваться для получения подробной информации
по конкретной функции. Для этого укажите имя функции после имени пакета. Предположим, вы увидели функцию ParseFloat в списке функций пакета strconv и хотите узнать
о ней больше. Для этого можно воспользоваться командой go doc strconv ParseFloat.
Вы получите описание самой функции и того, что она делает:
Получить документацию для
функции strconv.ParseFloat.

Имя функции, параметры
и возвращаемые значения.
Описание функции.

Shell Edit View Window Help

$ go doc strconv ParseFloat
func ParseFloat(s string, bitSize int) (float64, error)
ParseFloat converts the string s to a floating-point
number with the precision specified by bitSize: 32
for float32, or 64 for float64. When bitSize=32, the
result still has type float64, but it will be
convertible to float32 without changing its value.

Первая строка выглядит как обычное объявление функции в коде. Она состоит из имени
функции, за которым следуют круглые скобки с именами и типами параметров (если они есть).
Если у функции есть возвращаемые значения, они указываются после параметров.
Далее идет подробное описание того, что делает функция, а также другая информация, необходимая для использования этой функции.
Таким же способом можно запросить информацию о пакете keyboard: передайте go doc
его путь импорта. Давайте посмотрим, есть ли в ней что-то, что может пригодиться будущим
пользователям. В терминале выполните команду:
go doc github.com/headfirstgo/keyboard

Команда go doc сможет извлечь основную информацию — имя пакета, путь импорта — из кода.
Но пакет не содержит описания, поэтому польза от этих данных весьма ограниченна.
Получить документацию
для пакета «keyboard».

Имя и путь импорта пакета.

Описание пакета
отсутствует!
Функции пакета.

Shell Edit View Window Help

$ go doc github.com/headfirstgo/keyboard
package keyboard // import "github.com/headfirstgo/keyboard"
func GetFloat() (float64, error)

Запросив информацию о функции GetFloat, вы также не увидите описания:

Получение документации
для функции GetFloat.

Описание функции
отсутствует!

174 глава 4

Shell Edit View Window Help

$ go doc github.com/headfirstgo/keyboard GetFloat
func GetFloat() (float64, error)

запаковка кода

Документирование пакетов
Команда go doc старается получить полезную информацию на основании анализа кода. Имена пакетов
и пути импорта включаются автоматически, как и имена функций, параметры и возвращаемые типы.
И все же команда go doc не умеет творить чудеса. Если вы хотите, чтобы пользователи смогли узнать из
документации о предназначении пакета или функции, придется добавить эту информацию самостоятельно.
К счастью, это делается просто: нужно добавить в код документирующие комментарии. Обычные комментарии Go, размещенные непосредственно перед директивой package или объявлением функции,
считаются документирующими комментариями и включаются в вывод go doc.
Давайте добавим doc-комментарии для пакета keyboard. В начале файла keyboard.go, непосредственно
перед строкой package, мы добавим комментарий с описанием того, что делает пакет. А сразу же перед
объявлением GetFloat будет добавлена пара строк комментариев с описанием этой функции.

Перед строкой
«package» добавляются обычные
комментарии.

// Package keyboard reads user input from the keyboard.
package keyboard
import (
"bufio"
"os"
"strconv"
"strings"
)

Перед объявлением
функции добавляются // GetFloat reads a floating-point number from the keyboard.
// It returns the number read and any error encountered.
обычные комментарии.
func GetFloat() (float64, error) {
// No changes to GetFloat code
}

При следующем выполнении для пакета команда go doc найдет комментарий перед строкой
package и преобразует его в описание пакета. А при выполнении команды go doc для функции
GetFloat вы увидите описание из комментария, добавленного перед объявлением GetFloat.
File Edit Window Help

$ go doc github.com/headfirstgo/keyboard
package keyboard // import "github.com/headfirstgo/keyboard"

Описание пакета.

Package keyboard reads user input from the keyboard.
func GetFloat() (float64, error)
File Edit Window Help

Описание функции.

$ go doc github.com/headfirstgo/keyboard GetFloat
func GetFloat() (float64, error)
GetFloat reads a floating-point number from the
keyboard. It returns the number read and any error
encountered.
дальше 4   175

документирующие комментарии

Документирование пакетов (продолжение)
Возможность получения документации
командой go doc очень сильно поможет
разработчику, установившему пакет.

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

Да, это то, что нужно!
С этой документацией
я смогу уверенно пользоваться вашим кодом!

// Package keyboard reads user input from the keyboard.
package keyboard
ий
Комментар
для пакета.
import (
"bufio"
"os"
"strconv"
"strings"
)
Комментарий // GetFloat reads a floating-point number from the keyboard.
для функции. // It returns the number read and any error encountered.
func GetFloat() (float64, error) {
// Код GetFloat
}

При добавлении документирующих комментариев следует соблюдать ряд правил:


Комментарии должны состоять из полноценных предложений.



Комментарии для пакетов должны начинаться со слова «Package», за которым
следует имя пакета:
// Package mypackage enables widget management.



Комментарии для функций должны начинаться с имени функции, которую они
описывают:
// MyFunction converts widgets to gizmos.



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



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

176 глава 4

запаковка кода

Просмотр документации в браузере
Если вам удобнее работать в браузере, чем в терминале, существуют и другие способы просмотра документации пакетов. Самый простой способ — ввести в вашей любимой поисковой системе слово «golang», за
которым следует имя пакета. («Golang» обычно используется в поисковых запросах, относящихся к языку
Go, потому что слово «go» слишком часто встречается в других контекстах и не
позволяет отфильтровать большинство
нерелевантных результатов.) Например,
если вам нужна документация для пакета
fmt, проведите поиск по «golang fmt»:

Чтобы запрос вернул только результаты, относящиеся к Go.

Результаты должны включать сайты, которые предлагают документацию Go в формате HTML. Если вы
ищете пакет в стандартной библиотеке Go (например,
fmt), одним из лучших будет сайт golang.org, который
ведет команда разработчиков Go. Документация будет
похожа на выходные данные инструмента go doc,
с именами пакетов, путями импорта и описаниями.

Имя пакета, для которого запрашивается документация.

Имя пакета.
Путь импорта.

Описание пакета.

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

Параметры и возвращаемые типы.
Описание функции.

Впрочем, информация будет такой же, как при выполнении команды go doc в вашем терминале.
Она тоже определяется все теми же простыми документирующими комментариями в коде.
дальше 4   177

команда godoc

Запуск сервера документации HTML командой «godoc»
Программа, которая обслуживает раздел документации сайта
golang.org, на самом деле доступна и на вашем компьютере. Эта программа называется godoc (не путайте с командой go doc), и она
автоматически устанавливается вместе с Go. Программа godoc
генерирует документацию HTML на основании кода основной
установки Go и вашей рабочей области. Она включает веб-сервер,
который может передавать полученные веб-страницы браузеру.
(Не беспокойтесь, с настройками по умолчанию godoc не будет
принимать подключения с других компьютеров, кроме вашего.)
Чтобы запустить godoc в режиме веб-сервера, введите в терминале команду godoc (еще раз: не перепутайте с go doc) со специальным параметром -http=:6060.
После того как сервер godoc будет запущен, введите URL-адрес

Запуск
веб-сервера godoc.
File Edit Window Help

$ godoc -http=:6060
Введите этот
URL-адрес.

http://localhost:6060/pkg

в адресной строке браузера и нажмите Enter. Браузер подключится к вашему собственному компьютеру, а сервер godoc ответит HTML-страницей. На
экране появится список всех пакетов, установленных на вашей машине.
Ссылки на документацию пакетов.

Каждое имя пакета в списке представляет собой
ссылку на документацию для этого пакета. Щелкните на ссылке, и вы увидите ту же документацию пакетов, которую обычно просматриваете
на golang.org.

Имя пакета.
Путь импорта.

Описание пакета.

178 глава 4

запаковка кода

Сервер «godoc» включает ВАШИ пакеты!
Прокрутив список пакетов локального сервера godoc, вы
обнаружите в нем нечто интересное: наш пакет keyboard!

Посмотрите-ка! Наш
пакет «keyboard»!

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

Документирующие
комментарии пакета.

Документирующие
комментарии для
функции.

Когда вы захотите остановить сервер godoc, вернитесь к окну
терминала и, удерживая нажатой клавишу Ctrl, нажмите C.
На экране снова появляется системное приглашение.
Нажмите Ctrl+C, чтобы
остановить godoc.

File Edit Window Help

$ godoc -http=:6060
^C
$

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

дальше 4   179

инструментарий

Ваш инструментарий Go

ГЛАВА 4

Глава 4 подошла к концу!
В ней ваш инструментарий
пополнился пакетами.

ии
Функц

u
that yo
f code
o
k
n
u
our
is a ch
es in y
ип
Ти

пыы om other placie
o
t
in
d
if ды
ll fr
lasаsн
can ca in Go arкeоcм
what
е
s
e
ы
specify n
н
lu
.
в
a
h
m
V
о
ic
a
h
л
r
g
с
w
a
o
prУ
es,
yeonutsc
ne,m
nt typ fu
aetd
for.
reencsuttsio
differe llin
anab
ls
g
a
n
eto be
io
a
it

da
s ca
nvи

eeon
vfidceodteh ristoons t
C
luл
o
o
r
Wth
k
p
c
o
lo

t
et
ana
p
mnes
ord
isio
uesentas b balo
cood
u
cam
cdk
it
lcaio
cfm
n

oe
na
usethaartg pecraautsio
.ncd

a
t
a
я
o
t
if
a
n
d
н
e
ly
s
r
o
h
pntъ
ouб
вoлn ntetdtlyyp. es a
d
яit
aLtetohcio
ew
Mn
d if
excО
fu
ereea
aet,rnatan
w
evd
a ts
o
td
ern
drifefp
a
e
uo
n
t
lu
void
e
u
a
oy
e
c
rav
e
w
e
p
a
c
r
t
is
n
s
la
beetx
n
a
c
n
c
p
e
rhre
c
io
o
sstdт
o
e.tm
n
nsu
fcocglo
in
raeb
teh
ne
yoеuлkи
on
oll
ed
cdtpc,io
x
d
d
n
u
a
e
а
d
o
u
in
f
in
з
n
Y
s
а
o
A
u
e
e
GallowУeк ommctoa
ed
steh if ne fo llaowe
ll
ntedy,rp
rnu
cu
se, w
ir
atged.
is
r”,peta
cu
en
eris
nurten
ek
h
o
c
lt
t
x
io
f
t
O
t

s
in
c
o
n
e
o
d
e
p
r
a
k
y
f
s e to
akhs.ebosd
c lu
am
w
tcteo
ba
yelo
kevegba
dede,rs ess n
a
alu
lu
vit
ы

chк
in
tio
entv
a
heit
h
a
rlт
o
or’s
raeen
tGton
е
it
n
Y
uh
am

w
gttsio
n
euw
ly
e
r
in
n
d
циаль
П
ls
o
m
p
еin
n
sio
r
e
e
y
o
e
o
t
n
t
с
c
c
h
a
a
c
m
cttyhe fun bGroan—
ittп
itsgsb
le
eentsehse
dfno
b
ble
in
r
n
ia
ia
le
ь
o
n
h
r
r
c
e
a
a
ip
т
b
v
in
a
с
v
lt
y
e
.
p
а
r кtоsмлria
leer,o
Gobby typoчrаtяs rm
igfhptam
оeбadu
t(yabp
pliбd
eеeмsen
p s)ir
а
tin
uia
when
scela
seР

ed
а
zоep
rabaytvoarTоh
Gothit
etаesrвtm
ir
o
н
e
г

f
ts tнhыeй для
w
o
л
.
n
of in
o
e
e
а
n
d
io
m
т
follcoыnйdit
t
а
u
a
:
.
к
g
н
e
h
e
r
е
t atm
n gn
/нeаls
he a lseнаifзo
io
e нrersia
th
,чand a
ble
in
lo ap
excp
a tasin
vn
ny).
m
е,oifпf/рteeеhsдe(if

o
oo
е
f
т
e
t
h
ю
u
t
ь
ir
eпreaka
u а.
s after
tatk
rriaebqle
ofubnm
Vnн
ctyio
коtдthat run ncw
nh a * е
я
и
n
е
e
н
ftu.e сtоioit
хра atem
rait
здайт
tlo
w
a
p
e
,
o
т
е
post st rdteycplaersoefattrh
e
oe
к
h
ees tth
ь па roef vvaalu
eratio
citaten
oin
u
г с одo таlu
o
соnздат
e
P
о
Y
p
m
л
ы
y
б
r
t
h
о
o
c
e
ea Чт udrnbyonteh асти ка ool, etc.
b йлами,
ete чей обл
olllorw
wfil
*int,и *ф
а
to: к
в рабeоr . points о
м
и
ь
л
к
ll
t
с
a
е
in
c
н
.
o
s
p
it им или
ый код
н
исходн
и
м
и
ащ
содерж

КЛЮЧЕВЫЕ
МОМЕНТЫ
ƒƒ По умолчанию рабочей областью является каталог
с именем go в домашнем каталоге пользователя.
ƒƒ Чтобы использовать в качестве рабочей области другой каталог, настройте переменную среды
GOPATH.
ƒƒ Go использует три подкаталога в рабочей области:
в каталоге bin хранятся откомпилированные исполняемые программы, в каталоге pkg — откомпилированный код пакетов, а в каталоге src — исходный
код Go.
ƒƒ Имена подкаталогов каталога src формируют путь
импорта пакета. Имена вложенных каталогов разделяются символами / в пути импорта.
ƒƒ Имя пакета определяется директивами package
в начале файла с исходным кодом в каталоге пакета. За исключением пакета main, имя пакета
должно совпадать с именем каталога, в котором
он находится.
ƒƒ Имена пакетов должны записываться в нижнем
регистре. В идеале они состоят из одного слова.
ƒƒ Функции пакета могут вызываться за пределами
пакета только в том случае, если они экспортированы. Функция экспортируется, если ее имя
начинается с буквы верхнего регистра.
ƒƒ Константой называется имя для обращения к значению, которое никогда не изменяется.
ƒƒ Команда go install компилирует код пакета
и сохраняет его в каталоге pkg для пакетов общего
назначения или в каталоге bin для исполняемых
программ.
ƒƒ В качестве пути импорта пакета принято использовать URL-адрес размещения пакета. В этом случае
команда go get может находить, загружать и
устанавливать пакеты, зная только их путь импорта.
ƒƒ Команда go doc выводит документацию пакетов.
В выходные данные go doc включаются документирующие комментарии в коде.

180 глава 4

запаковка кода

У бассейна. Решение упражнения
Выловите из бассейна фрагменты кода и разместите их в пустых строках кода. Каждый фрагмент может использоваться только один раз; использовать все фрагменты необязательно. Ваша задача: создать в рабочей области Go пакет calc, чтобы функции могли использоваться в коде main.go.
(домашний каталог пользователя)

go

package calc

Убедитесь, что имя начинается с буквы верхнего регистра!

func Add (first float64, second float64) float64 {
return first + second
}

src

Убедитесь, что имя начинается с буквы верхнего регистра!

calc
calc.go

func Subtract (first float64, second float64) float64 {
return first - second
}
calc.go
package main
import (
"calc"
"fmt"
)
func main() {
fmt.Println(calc. Add (1, 2))
fmt.Println(calc. Subtract (7, 3))
}

Результат.

main.go

3
4

Мы создали рабочую область Go с простым пакетом mypackage. Завершите приведенную программу, чтобы она импортировала пакет mypackage и вызывала его функцию
MyFunction.
package mypackage
func MyFunction() {
}

package main
import «my.com/me/myproject/mypackage»
func main() {
mypackage.MyFunction()

mypackage.go

}

дальше 4   181

5 И далее по списку

Массивы
У меня просто огромный список
дел на сегодня! Пройдусь
по каждому из них и сделаю
все в срок!

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

с циклами for...range.

что такое массив?

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

Поможете?
Мой бизнес в мясо!
Неделя A:
71,8 фунта

Неделя B:
56,2 фунта

Неделя C:
89,5 фунта

Задача не из сложных: нужно вычислить среднее значение,
то есть взять три числа, сложить их и разделить на 3. Среднее
значение дает неплохую оценку того, сколько мяса следует
заказывать.

(неделя A + неделя B + неделя C) ÷ 3 = среднее
Первая проблема — хранение значений. Было бы неудобно
объявлять три разные переменные, а если в будущем потребуется сложить больше значений, станет еще неудобнее. Но как
и большинство языков программирования, Go предоставляет
структуру данных, которая идеально подходит для подобных
задач...
Массив представляет собой набор значений, относящихся к
одному типу. Представьте себе таблетницу — вы можете класть
и доставать таблетки в любое отделение, но при этом ее удобно
переносить как единое целое.
Значения, хранящиеся в массиве, называются элементами.
Вы можете создать массив строк, массив логических значений
или массив любого другого типа Go (даже массив массивов).
Весь массив можно сохранить в одной переменной, а затем
обратиться к любому нужному элементу.

184

глава 5

и далее по списку

В массивах хранятся наборы значений (продолжение)
Массив содержит заранее заданное количество элементов,
а его размер не может увеличиваться или уменьшаться. Чтобы
объявить переменную для хранения массива, следует указать
количество хранящихся в нем элементов в квадратных скобках
([]), а затем тип элементов в массиве.

Количество
элементов
в массиве.

Тип элементов
в массиве.

var myArray [4]string

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

Например, если вы хотите создать массив с названиями нот,
первой ноте будет присвоен индекс 0, второй — индекс 1,
и т. д. Индекс задается в квадратных скобках.

Индекс 0.
Индекс 1.
сив
мас
тся
Индекс 2.
дае
Соз
Индекс 3.
ок.
стр
и
сем
из

var notes [7]string
notes[0] = "do"
Присваивается значение первого элемента.
notes[1] = "re"
Присваивается значение второго элемента.
notes[2] = "mi"
Присваивается значение третьего элемента.
fmt.Println(notes[0])
fmt.Println(notes[1])
Выводится первый элемент.

do
re

Массив целых чисел:

Выводится второй
элемент.

целых чисел.
Создается массив из пяти

.
var primes [5]int Присваивается значение первого элемента
primes[0] = 2
Присваивается значение второго элемента.
primes[1] = 3
fmt.Println(primes[0])
Выводится значение третьего элемента.
2

Массив значений time.Time:

значений Time.
Создается массив из трех
вого элемента.
var dates [3]time.Time
Присваивается значение пер
dates[0] = time.Unix(1257894000, 0)
Присваивается значение второго элемента.
dates[1] = time.Unix(1447920000, 0)
Присваивается значение третьего элемента.
dates[2] = time.Unix(1508632200, 0)
fmt.Println(dates[1])
Выводится второй
2015-11-19 08:00:00 +0000 UTC
элемент.

дальше 4   185

массивы и нулевые значения

Нулевые значения в массивах
Как и в случае с переменными, при создании массивов все содержащиеся в них
значения инициализируются нулевым значением для типа, содержащегося в массиве. Так массив значений int по умолчанию заполняется нулями:
Выводится явно присвоенный элемент.

var primes [5]int
primes[0] = 2
fmt.Println(primes[0])
Выводятся элементы, fmt.Println(primes[2])
которым не были явно fmt.Println(primes[4])
присвоены значения.

2
0
0

Явно присвоенное значение.
Нулевое значение.
Нулевое значение.

С другой стороны, нулевым значением для строк является пустая строка, так
что массив строковых значений по умолчанию заполняется пустыми строками:
var notes [7]string
Выводятся элементы, котоnotes[0] = "do"
рым не были явно присвоены fmt.Println(notes[3])
значения. fmt.Println(notes[6])
fmt.Println(notes[0])
Выводится явно присвоенное значение.

Нулевое значение (пустая строка).
Нулевое значение (пустая строка).
Явно присвоенное значение.

do

Нулевые значения позволяют безопасно выполнять операции с элементами
массивов, даже если им не были присвоены значения. Например, в следующем
массиве хранятся целочисленные счетчики. Любой элемент можно увеличить
на 1 даже без предварительного присваивания значения, потому что мы знаем,
что все значения счетчиков начинаются с 0.
var counters [3]int
counters[0]++
Первый элемент увеличивается с 0 до 1.
counters[0]++
Первый элемент увеличивается с 1 до 2.
counters[2]++
Третий элемент увеличивается с 0 до 1.
fmt.Println(counters[0], counters[1], counters[2])
Все еще нулевое
значение.

Увеличивается дважды.

Увеличивается 1 раз.

2 0 1

При создании массива все содержащиеся
в нем элементы инициализируются нулевым
значением для типа, хранящегося в массиве.
186

глава 5

и далее по списку

Литералы массивов
Если вам заранее известны значения, которые должны храниться в массиве, вы можете инициализировать массив этими значениями в форме литерала массива. Литерал
массива начинается как тип массива — с количества элементов в квадратных скобках,
за которым следует тип элементов. Далее в фигурных скобках идет список исходных
значений элементов массива. Значения элементов должны разделяться запятыми.
Количество
элементов
в массиве.

Тип элементов в массиве.

Список значений, разделенных запятыми.

[3]int{9, 18, 27}
Эти примеры почти не отличаются от предыдущих, если не считать того, что вместо
последовательного присваивания значений элементам массива весь массив инициализируется с использованием литерала массива.
var notes [7]string = [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
fmt.Println(notes[3], notes[6], notes[0])
Присваивание значений
var primes [5]int = [5]int{2, 3, 5, 7, 11}
в
форме литерала массива.
fmt.Println(primes[0], primes[2], primes[4])

fa ti do
2 5 11

Присваивание
значений
в форме
литерала
массива.

Литералы массивов также позволяют использовать короткие объявления
переменных с помощью :=.
нной.
Короткое объявление переме

notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
primes := [5]int{2, 3, 5, 7, 11}
Короткое объявление переменной.

Литералы массивов могут распространяться на несколько строк, но перед каждым
переносом строки в коде должна стоять запятая. Запятая даже должна стоять после
последнего элемента в литерале массива, если за ним следует перенос строки. (На
первый взгляд этот синтаксис выглядит неуклюже, но он упрощает последующее
добавление новых элементов в коде.)
text := [3]string{
Все это один массив.
"This is a series of long strings",
"which would be awkward to place",
"together on a single line",
Запятая в конце обязательна.
}
дальше 4   187

вывод массивов

Упражнение

Следующая программа объявляет пару массивов и выводит все их элементы. Запишите
результат, который будет выведен программой.
package main
import "fmt"
func main() {
var numbers [3]int
numbers[0] = 42
numbers[2] = 108
var letters = [3]string{"a", "b", "c"}
Результат:

fmt.Println(numbers[0])
fmt.Println(numbers[1])
fmt.Println(numbers[2])
fmt.Println(letters[2])
fmt.Println(letters[0])
}

fmt.Println(letters[1])

Ответ на с. 207.

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

var notes [3]string = [3]string{"do", "re", "mi"}
var primes [5]int = [5]int{2, 3, 5, 7, 11}
fmt.Println(notes)
[do re mi]
fmt.Println(primes)
[2 3 5 7 11]

Возможно, вы также помните глагол "%#v", используемый функциями Printf
и Sprintf, — он форматирует значения так, как они отображаются в коде Go. При
форматировании с "%#v" массивы отображаются в форме литералов массивов Go.

Массивы форматируются так, как они запи- fmt.Printf("%#v\n", notes)
fmt.Printf("%#v\n", primes)
сываются в коде Go.

188

глава 5

[3]string{"do", "re", "mi"}
[5]int{2, 3, 5, 7, 11}

и далее по списку

Обращение к элементам массива в цикле
Вы не обязаны явно записывать целочисленные индексы элементов массивов, к которым обращаетесь в своем коде. В качестве индекса также
можно использовать значение целочисленной переменной.
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
index := 1
fmt.Println(index, notes[index])
Выводит элемент массива с индексом 1.
index = 3
fmt.Println(index, notes[index])
Выводит элемент массива с индексом 3.

1 re
3 fa

Это означает, что элементы массивов можно перебирать в цикле for.
Цикл перебирает индексы массива, а переменная цикла используется
для обращения к элементу с текущим индексом.
notes := [7]string{"do", "re", "mi", "fa", "so", "la",
for i := 0; i и 90 {

return errors.New("invalid latitude")
}
c.latitude = latitude
return nil
}
func (c *Coordinates) SetLongitude(longitude float64) error {
if longitude < -180 || longitude > 180 {

return errors.New("invalid longitude")
}
c.longitude = longitude
return nil
}

348

глава 10

все при себе

Упражнение
(Продолжение)

Ниже приведена обновленная версия типа Landmark (из главы 8). Мы хотим, чтобы его поле name
было инкапсулировано, а обращения могли осуществляться исключительно через get-метод Name
и set-метод SetName. Метод SetName возвращает ошибку, если его аргумент является пустой
строкой, или задает поле name с возвращением ошибки nil в противном случае. Тип Landmark
также должен содержать анонимное поле Coordinates, чтобы методы Coordinates повышались до уровня Landmark.
package geo

Заполните пропуски и завершите код типа Landmark.

import "errors"
landmark.go

type Landmark struct {
string
}
func (l *Landmark)
return l.name
}

func (l *Landmark)
(name string) error {
if name == "" {

return errors.New("invalid name")
}
l.name = name
return nil
}

package main
// Директивы imports пропущены
func main() {
location := geo.Landmark{}
main.go
err := location.SetName("The Googleplex")
if err != nil {
log.Fatal(err)
}
err = location.SetLatitude(37.42)
if err != nil {
log.Fatal(err)
}
err = location.SetLongitude(-122.08)
if err != nil {
Результат.
log.Fatal(err)
}
fmt.Println(location.Name())
The Googleplex
fmt.Println(location.Latitude())
37.42
fmt.Println(location.Longitude())
-122.08
}

Ответ на с. 354.

Если пропуски в коде Landmark
заполнены правильно, код пакета
main должен выполняться и выводить показанный результат.

() string {

дальше 4   349

инструментарий

ГЛАВА 10

Ваш инструментарий Go
Глава 10 осталась позади! В ней
ваш инструментарий пополнился
инкапсуляцией и встраиванием.

tsionsnals
netsothat
FTuynpced
io
lakssrifoafietcdionodin
n
ula
hcc
ce
sttinoisn aD
CLoonopciio
t
e dcesetsoinhat
tin
sesaarree sotcatkhteoem
cn
raols
nn
cpoela
G
e
uuo
frsop
fP
t
cify w
AVF
i
n
s
e
g
be
lo
a
n
bewyhoicuhrcow
alu
kio
aso,rm
it
doepateocia
s
llsepcferla
drac
l a
t
a
na
y
u
c
r
a
a
e
f
c
CoLoP
t
n
r
o
s
s
in
k
cp
y
e
fm
o
c
t
o
a
d
s
m
o
uoA
p
lo
e
t
e
s
es t
n
is
b
.
h
a
n
yY
c
e
.
a
t
r
e
a
t
o
lu
ly
c
e
ll
f
r
eutrliecca
a
n
e
d
a
is
g
a
s
s
d
v
e
e
s
d
ofS
s
p
c
u
t
.
p
d
s
e
n
n
f
e
n
m
dtif
aaeoo
akd
ateretethssat
oG
seis
o
up
rn
“le
ctrG
tpeom
tegcsfn
eio
hse
t
ea
ucep
’s
c
d
a
fitip
i
w
grsrn
bTetliy
oerM
o
n
u
h
Y
n
a
o
r
i
t
d
h
u
a
s
t
u
s
e
c
li
w
n
t
S
g
,
in
y
a
r
s
я
u
h
a
a
m
D
en
poлlluя
ca
oetn
ritah
ehfxT


io
yto
tсyif
uksu
rfd
io
elu
danls
ly
ese e a
ta
otct’s
ctцto
k
n
v
a
yп
ca
p
yaa
D
escra
li
ois
o
bno
tto
h
eAncn
,hp
n
u
oeevn.a
lo
rtuis
le
cy
m
ep
ebe
esatd
a
efen
io

k
pd
li
o
u
oeoju
ia
lu
M
liif
stm
fy
bo
ru
d
m
isgrin

eis
ea
w
rela
к
b
AA
in
cll,totn
fhpsuio
exvd
aerogn
a
k
pah
aeollu
erсокры
,th
atutyn
d
chta
y
d
is
n
h
И
e
d
а
s
t
n
,
n
e
ig
o
t
e
к
e
t
a
n
u
ir
r
e
r
r
w
c
и
in
m
a
)
m
u
d
la
io
o
т
a
ll
it
r
e
ic
m
lu
toк ootw
n
arit n e,р
in
rccstot aettfic
gfа
r ( etfvo
ea
in
rarnde.d
nw
oio

.u
ralu
” yis
sey
eTya
peA
tio
п
fe tehat itrin
ehls
n
void
earrd
rnetto
is
in
deise
eG
fio
pp
uin
O
b
W
r

“yto
op
end
oecle
n
coo
pяetcrjo
p
d
h
иr
o
ag
brty
tu
s.uрrо
td
oaasof,ilyda
цta
ofn
everso
erсp
deя
foteo”fh
asrpvr.e
rein
sh
seeo
sп
Moa
esw
лls
th
m
eиy
ea
cygp
ty
ld
у
n
xo
htrh
p
p
ceсto
eeaerxn
toт
esk
eaA
y
n
hy
п
n
c
m
n
n
а
e
e
t
p
:
f
y
t
u
r
к
s
а
e
h
Asw
o
w
e
io
e
t
a
н
ч
t
t
n
h
in
e
f
k
r
it
t
p
g
o
t
m
n
a
И
if
r
ach
й
h
g
a
,
f
in
,
e
m
d
о
t
t
e
f
k
e
s
f
etit
n
o
e
н
in
t
e
r
c
y
u
t
if
n
д
d
r.anlyи.
a
it
e
d
a
d
r
ubb
a
le
ir
n
e
d
e
.
о
e
e
v
t
r
n
p
e
e
t
b
.
a
f enm
rcia
oy
t eraxasem
eta
toa
.n
isb
onn
aw
chdueio
io
seo
a
хm
sw
e
tp
tcy
sle
evsE
p

elt
crd
aнed
n
чаoсanт
shru
rw
ty
lu
н
u
le
cвeba
ooefctio
it
is
m
etsh
pcnea
btan
cllra
tseгit
saа
дu
uy
gоc. йa
itvo
rais
sted
o
eu
яn
em
d
у
in
in
n
a
e
it
a
и
o
e
e
r
р
iv
h
c
o
c
r
v
n
e
д
p
e
t
т
m
li
a
a
c
o
y
o
c
u
s
в
k
r
r
d
t
e
f
b
o
n
c
c
o
e
y
m
le
r
n
u
a
,
f
а
dneteheda
,n
e*asn
datt erm
an
lo
fubllYotow
a
eud
le
о
sce
e
ia
baдy
a
к
rda
ard
egla
erb
fbtain
sg
a
ia
syyp
ebsы
ordin
h
V
aeecrd
d
a
syatd
yit
lu
u
aconm
tw
io
ntin
eid
vifn
vw
oraоerrт
tseaah
h
n
h
ein
ab
p
p
a
lu
cerin
d
hhm
it
reo
a
in
a
s
a
ic
t
ia
t
м
w
s
v
s
E
c
if
m
.
a
m
e
e
e
м
)
c
o
n
e
a
e
z
n
a
h
s
а
e
e
y
t
t
a
r
ь
e
e
t
li
s
c
a
a
р
p
c
n
a
t
,
n
л
g
a
n
in
r
n
s
p
г
ia
r
s
e
a
h
oitW
оs зоs ’to
it
oпe
ldeip
sfie
cp
a
reoh
aew
noa
.сlu
xresea
esly
bk
(tcif
em
slt
in
w
taoey
edp
eodrrm
nveи
tecotsio
old
gasld
le
a
in
т
d
n
ir
erssbp
h
еd
ulincn
u
seog
fT
in
e. e твиly
a
ж
yy
qhherm
thtd
ais
uh
rrtan
va
rrn
oluS
esin
usoо
se
t
p
e
oh
e
r
o
r
t
n
e
e
G
a
d
м
in
.
r
c
s
r
rsd
y
e
e
r
s
t
m
r
a
w
e
n
e
t
a
g
t
я
d
y
e
t
e
lu
o
u
e
e
k
t
r
s
c
и
iv
n
io
r
io
ll
r
с.
a
s
n
n
e
t
o
e
e
ц
s
n
o
a
r
a
m
v
t
kяA
csu
Ap
uon
eoettlo
in

,o
cstep
ecт
cly
sp
esce
p
eeeh
rT
eurfm
o
u
on
reу
elin
sh
o
m
сlu
ir
eп
air
нnедtеyй
eh
esra
ro
apeftsы
usin
p
xo
ctaio
d
h
t
a
o
efP
y
n
e
t
а
t
t
c
n
a
e
.
s
к
t
G
f
f
e
u
о
o
;
m
н
y
s
o
r
t
t
s
e
s
n
e
a
n
t
t
r
.p
И
lv
f
h
e
s
a
k
a
a
le
h
c
o
n
e
t
o
p
t
v
e
t
b
т
s
e
f
T
)
a
e
e
y
d
r
a
t
t
y
o
и
.
ia
h
n
o
s
baocotvfhhaoeorw
m
r
m
e
n
.
h
w
u
b
t
n
l,
s
щ
e
o
s
o
t
a
p
o
e
le
t
o
t
m
e
а
io
d
e
sd
n
ee*slsnba
klm
trсh
it
n
.d
ea
d
a(e
in
a
toe
яd seзtille
ein
a
oflu
la
m
d
yee, sttyh
eit
eaao.n
a
h
m
лerlu
p
Gtthfocoeou
n
a
reto
,n
eяcv
дrh
llbncsw
y
t
n
u
e
n
y
r
/
h
a
t
*
u
e
if
s
n
t
ь
t
v
a
a
u
:
a
b
e
e
e
t
c
o
т
h
m
e
r
h
t
lsam
n r us
аain
oeen
oola
cm
eieeвw

st.ifetF
tin
tie
lue
/toe
oortvuh
dtle
fefep
rco
mY
ir
ы.yrхsa.oym
tm
ytrp
ugvha
erld
ahnosy
a
.eod
аrsн
dn
tferha
tch
usоtще
fpo
erb
дer’s
aum
se.a
earoн
p
lt
otp
yeh
stril
tD
in
r ecatlo
aka
osvE
ыtgoхoanp
pa
eein
ee,. ovan
m
e еm
fakil
ll
t
p
ь
n
a
e
a
lu
y
л
a
р
t
a
е
f
п
l
f
o
e
g
in
il
t
т
o
b
ll
ly
in ll th данны used.
wu
narnlyd
taostdn
Anm
diteeeertnrh
е monly
u iodce
ыm
anовa
eys ь увеaescleh
sm
eit
am
р. osаtнcнbou
allaeenrt.yпpсeу,лpиem
thеe бkы
т

sacИ
а
y
,
к
e
t
e
r
н
p
t
a
е
y
a
t
s
h
ж
t cet same Вы мо
peрaуsшит
tyн
soetfrtu
e
.
h
m
ь
а
a
s
т
b изменя
е
e
н
h
t
о
e
e tоoмb, что эт
v
a
h
аща’t
doрnены в т
да, обр
о
к
и
т
es. собнос
thрeаvбaоlu
тоспо
ным.
им дан
т
э
к
ся
ющего

ивание

Встра

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

350

глава 10

КЛЮЧЕВЫЕ
МОМЕНТЫ

ƒƒ В Go данные инкапсулируются в пакетах с помощью неэкспортированных переменных пакетов или полей структур.
ƒƒ К неэкспортированным переменным, полям
структур, функциям, методам и т. д. можно
обращаться из экспортируемых функций и методов, определенных в том же пакете.
ƒƒ Практика проверки действительности данных
перед их сохранением называется проверкой
данных.
ƒƒ Метод, используемый в основном для задания значения неинкапсулируемого поля,
называется set-методом. Set-методы часто
включают логику проверки данных, которая
гарантирует, что присваиваемое значение
будет допустимым.
ƒƒ Так как set-методы должны изменять своего
получателя, их параметр получателя должен
иметь тип указателя.
ƒƒ Традиционно set-методам присваиваются имена вида SetX, где X — имя присваиваемого
поля.
ƒƒ Метод, предназначенный для получения значения инкапсулированного поля, называется
get-методом.
ƒƒ Имена get-методов традиционно задаются
в форме X, где X — имя поля. В некоторых
других языках программирования для get-методов была выбрана форма GetX, но в Go
эту форму использовать не рекомендуется.
ƒƒ Методы, определяемые для внешнего типа
структуры, существуют на одном уровне с
методами, повышенными от встроенного типа.
ƒƒ Неэкспортируемые методы встроенного типа
не повышаются до уровня внешнего типа.

все при себе

Необходимо добавить в тип Coordinates set-методы для каждого из его
полей. Заполните пропуски в файле coordinates.go, чтобы код main.go выполнялся и выводил показанный результат.
package geo
type Coordinates struct {
Latitude

}
package main

func (c

import (

}

"fmt"

)

"geo"

func main() {

float64

Longitude float64

затель типа,
Должен использоваться ука
получатель.
ть
ени
чтобы мы могли изм
*Coordinates ) SetLatitude( latitude float64) {

c.Latitude = latitude
__________

затель типа,
Должен использоваться ука
получатель.
ть
ени
чтобы мы могли изм
func (c *Coordinates ) SetLongitude( longitude float64) {
c.Longitude = longitude

}

coordinates.go

coordinates := geo.Coordinates{}
coordinates.SetLatitude(37.42)

coordinates.SetLongitude(-122.08)
}

fmt.Println(coordinates)

Результат.

{37.42 -122.08}
main.go

дальше 4   351

решение упражнений

Ваша задача при обновлении этого кода — инкапсуляция полей типа Coordinates
и добавление проверки данных в его set-методах.

Обновите поля типа, чтобы они не экспортировались.

Добавьте get-метод для каждого поля.
Добавьте проверку данных в set-методы. Метод SetLatitude должен возвращать
ошибку, если переданное значение меньше -90 или больше 90. Метод SetLongitude
должен возвращать ошибку, если новое значение меньше -180 или больше 180.

package geo
import "errors"

coordinates.go
type Coordinates struct {
float64 Поля должны быть
latitude
longitude float64 неэкспортируемыми.
}
Имена get-методов
Тот же тип, что и в поле.
с буквы верхнего регистра.

func (c *Coordinates) Latitude () float64 {
return c.latitude
}
Имена get-методов
Тот же тип, что и в поле.
с буквы верхнего регистра.

func (c *Coordinates) Longitude () float64 {
return c.longitude
Необходимо вернуть
}

тип ошибки.

func (c *Coordinates) SetLatitude(latitude float64) error {
if latitude < -90 || latitude > 90 {
return errors.New ("invalid latitude")
}
Возвращает новое значение ошибки.
c.latitude = latitude
return nil
Необходимо вернуть
Возвращает nil,
если нет ошибки.
тип ошибки.
}
func (c *Coordinates) SetLongitude(longitude float64) error {
if longitude < -180 || longitude > 180 {
return errors.New ("invalid longitude")
}
Возвращает новое значение ошибки.
c.longitude = longitude
return nil
Возвращает nil, если нет ошибки.
}

352

глава 10

все при себе

Следующей задачей при обновлении кода пакета main было использование кода пакета,
чтобы в нем использовался обновленный тип Coordinates.

(Продолжение)





Для каждого вызова set-метода сохраните возвращаемое значение error.
Если значение error отлично от nil, вызовите функцию log.Fatal, чтобы вывести
сообщение об ошибке и завершить программу.
Если присваивание значений обошлось без ошибок, вызовите оба get-метода для вывода
значений полей.

Как показано ниже, вызов SetLatitude был успешным, но SetLongitude передается
недопустимое значение, поэтому в этот момент программа выводит сообщение об ошибке
и завершается.

package main
import (

"fmt"

main.go

"geo"
)

"log"

func main() {
Сохраняем возвращаемое значение ошибки.

coordinates := geo.Coordinates{}
err := coordinates.SetLatitude(37.42)
if err != nil

Если произошла ошибка,
программа выводит сооб log.Fatal(err)
щение об ошибке и завер}
шается.

{

err = coordinates.SetLongitude(-1122.08)

Если произошла ошибка,
{
программа выводит сооб log.Fatal(err) щение об ошибке и завершается.
}

if err != nil

Вызываем
get-методы.

(Недопустимое значение!)

fmt.Println(coordinates. Latitude ())

}

fmt.Println(coordinates. Longitude ())
Результат.

2018/03/23 20:12:49 invalid longitude
exit status 1

дальше 4   353

решение упражнений

Ниже приведена обновленная версия типа Landmark (из главы 8.) Мы хотим, чтобы его поле name
было инкапсулировано, а обращения могли осуществляться исключительно через get-метод Name
и set-метод SetName. Метод SetName возвращает ошибку, если его аргумент является пустой
строкой, или задает поле name с возвращением ошибки nil в противном случае. Тип Landmark
также должен содержать анонимное поле Coordinates, чтобы методы Coordinates повышались до уровня Landmark.
package geo
import "errors"
type Landmark struct {
name string
Coordinates
}
Встраивание с

landmark.go
Поле name не должно экспортироваться, чтобы оно было
инкапсулировано.

помощью анонимного поля.

func (l *Landmark) Name () string {
return l.name
То же имя, что и в поле
} То же имя, что и в поле,
(но экспортируемое).
но с преф
иксом Set.

func (l *Landmark) SetName(name string) error {
if name == "" {

return errors.New("invalid name")
}
l.name = name
return nil
}
package main
// Директивы imports пропущены
Создаем значение
func main() {
Landmark.
location := geo.Landmark{}
main.go
err := location.SetName("The Googleplex")
if err != nil {
Определяется для самого
типа Landmark.
log.Fatal(err)
Повышается от Coordinates.
}
err = location.SetLatitude(37.42)
if err != nil {
log.Fatal(err)
Повышается от Coordinates.
}
err = location.SetLongitude(-122.08)
if err != nil {
Определяется для
log.Fatal(err)
}
Landmark.
fmt.Println(location.Name())
fmt.Println(location.Latitude())
Повышается
fmt.Println(location.Longitude()) от Coordinates.
}

354

глава 10

Результат.

The Googleplex
37.42
-122.08

11 Что можно сделать?

Интерфейсы
Да, это не совсем автомобиль...
Но если есть метод, чтобы рулить,
я с этим как-нибудь справлюсь!

Иногда конкретный тип значения не важен. Вас не интересует,
с чем вы работаете. Вы просто хотите быть уверены в том, что оно может
делать то, что нужно вам. Тогда вы сможете вызывать для значения определенные методы. Неважно, с каким значением вы работаете — Pen или

Pencil; вам просто нужно нечто, содержащее метод Draw. Именно эту задачу
решают интерфейсы в языке Go. Они позволяют определять переменные и

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

типы переменных и параметров

Два разных типа с одинаковыми методами
Помните кассетные магнитофоны? (Хотя, наверное,
некоторые читатели их уже не застали.) Это были
полезные устройства. Они позволяли легко записать
на пленку все ваши любимые песни — даже созданные
разными исполнителями. Конечно, магнитофоны
были слишкомгромоздкими, чтобы постоянно носить их с собой. Если вам хотелось взять кассеты в
дорогу, обычно для этого использовались плееры
на батарейках. Плееры обычно не поддерживали
возможности записи. Ах, как же это было здорово —
создавать собственные миксы и обмениваться ими
с друзьями!

Плеер.
Магнитофон.

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

src

github.com

headfirstgo

gadget

tape.go

package gadget
import "fmt"

Тип TapePlayer содержит метод Play, моделирующий воспроизведение трека, и метод Stop для
остановки виртуального воспроизведения.

Тип TapeRecorder содержит методы Play и Stop,
а также метод Record.
Содержит метод Play, как
у TapePlayer.

Содержит метод Stop,
как у TapePlayer.

356

глава 11

type TapePlayer struct {
Batteries string
}
func (t TapePlayer) Play(song string) {
fmt.Println("Playing", song)
}
func (t TapePlayer) Stop() {
fmt.Println("Stopped!")
}
type TapeRecorder struct {
Microphones int
}
func (t TapeRecorder) Play(song string) {
fmt.Println("Playing", song)
}
func (t TapeRecorder) Record() {
fmt.Println("Recording")
}
func (t TapeRecorder) Stop() {
fmt.Println("Stopped!")
}

что можно сделать?

В параметре метода может передаваться только один тип
Ниже приведен пример программы, использующей пакет gadget. Мы определяем функцию playList, которая получает значение TapePlayer и сегмент
названий воспроизводимых песен. Функция перебирает все названия в сегменте
и передает их методу Play значения TapePlayer. Завершив воспроизведение
списка, функция вызывает Stop для TapePlayer.
Затем в методе main остается создать значение TapePlayer и сегмент названий
песен и передать их playList.
package main

Импортируем пакет.

import "github.com/headfirstgo/gadget"
func playList(device gadget.TapePlayer, songs []string) {
for _, song := range songs {
Перебираем все песни в цикле.
device.Play(song)
Воспроизведение текущей песни.
}
device.Stop()
Плеер останавливается после завершения.
}
Создание TapePlayer. Создается сегмент
с названиями песен.
func main() {
player := gadget.TapePlayer{}
mixtape := []string{"Jessie's Girl", "Whip It", "9 to 5"}
playList(player, mixtape)
Песни воспроизводятся при помощи TapePlayer.
}

Playing Jessie's Girl
Playing Whip It
Playing 9 to 5
Stopped!

Функция playList отлично работает со значениями TapePlayer. Казалось
бы, она также должна работать с TapeRecorder. (В конце концов, магнитофон по сути представляет собой плеер с дополнительной функцией записи.)
Но первый параметр playList имеет тип TapePlayer. Попробуем передать
аргумент любого другого типа, и сразу получим ошибку компиляции:
er
Создаем значение TapeRecord
вместо TapePlayer.

func main() {
player := gadget.TapeRecorder{}
mixtape := []string{"Jessie's Girl", "Whip It", "9 to 5"}
playList(player, mixtape)
Ошибка.
}
TapeRecorder передается
cannot use player (type gadget.TapeRecorder)
функции playList.

as type gadget.TapePlayer in argument to playList
дальше 4   357

что такое интерфейсы?

В параметре метода может передаваться только один тип (продолжение)
Плохо... На самом деле функции playList
необходимо значение, тип которого определяет методы Play и Stop. Эти методы есть как
у TapePlayer, так и у TapeRecorder!
func playList(device gadget.TapePlayer, songs []string) {
for _, song := range songs { Здесь должно быть значение, содержащее
device.Play(song)
метод Play со строковым параметром.
}
Здесь должно быть значение, содерdevice.Stop()
жащее метод Stop без параметров.
}
type TapePlayer struct {
Batteries string
}
TapePlayer содержит метод
func (t TapePlayer) Play(song string) {
Play со строковым параметром.
fmt.Println("Playing", song)
}
TapePlayer содержит метод
func (t TapePlayer) Stop() {
Stop без параметров.
fmt.Println("Stopped!")
}
type TapeRecorder struct {
Microphones int
}
TapePlayer тоже содержит
func (t TapeRecorder) Play(song string) {
метод Play со строковым
fmt.Println("Playing", song)
параметром.
}
func (t TapeRecorder) Record() {
fmt.Println("Recording")
}
TapePlayer также содержит
func (t TapeRecorder) Stop() {
метод Stop без параметров.
fmt.Println("Stopped!")
}

Похоже, в данном случае безопасность типов языка Go не столько
помогает, сколько мешает. Тип TapeRecorder определяет все методы,
необходимые функции playList, но мы не можем использовать его, так
как playList принимает только значения TapePlayer.
Что же делать? Может, написать вторую, почти идентичную функцию
playListWithRecorder, которая получает значение TapeRecorder?
Однако Go предлагает и другое решение...
358

глава 11

что можно сделать?

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

Интерфейс — набор методов, который должен
поддерживаться некоторыми значениями.
Ключевое слово «interface».

type myInterface interface {
Тип параметра.
Имя метода.
methodWithoutParameters()
methodWithParameter(float64)
Имя метода.
methodWithReturnValue() string
Имя метода.
Тип возвращае}
Однажды я купила кофе­
варку, у которой не было кнопки «варить кофе»!
Я такого не ожидала. Не стоит и говорить,
что я разочаровалась в покупке.

Любой тип, который содержит все методы, перечисленные в определении интерфейса, называется поддерживающим этот интерфейс.
Тип, поддерживающий интерфейс, может использоваться в любом
месте, где должен использоваться этот интерфейс.
Имена методов, типы параметров (если они есть) и типы возвращаемых значений (если они есть) должны совпадать с определениями
в интерфейсе. Тип может содержать методы помимо тех, которые
перечислены в интерфейсе, но в нем не могут отсутствовать такие
методы, иначе тип не будет поддерживать интерфейс.
Тип может поддерживать несколько интерфейсов, а интерфейс может
(и обычно должен) поддерживаться несколькими типами.

мого значения.

дальше 4   359

поддержка интерфейса

Определение типа, поддерживающего интерфейс
Приведенный ниже код создает небольшой экспериментальный пакет с именем mypkg. Он определяет тип интерфейса с именем MyInterface, содержащий три метода. Затем определяет тип
с именем MyType, поддерживающий MyInterface.
Для поддержки MyInterface необходимы три метода: MethodWithoutParameters,
MethodWithParameter с параметром float64 и MethodWithReturnValue, возвращающий
string.
Затем объявляется другой тип MyType. Базовый тип MyType в данном случае неважен; используется int. Мы определили все методы MyType, необходимые для поддержки MyInterface, а также
один дополнительный метод, который не является частью интерфейса.
ваша
рабочая
область

src

mypkg

myinterface.go

package mypkg

а.
Объявление типа интерфейс
ерфейс,
Тип поддерживает этот инт
.
од..
мет
type MyInterface interface {
если содержит этот
MethodWithoutParameters()
...а также этот метод
MethodWithParameter(float64)
(с параметром float64)...
MethodWithReturnValue() string
}
Объявление типа, поддер...и этот метод (с возвращаживающего myInterface.
емым значением string).
type MyType int

import "fmt"

func (m MyType) MethodWithoutParameters() {
Первый обязательный метод.
fmt.Println("MethodWithoutParameters called")
}
Второй обязательный метод
func (m MyType) MethodWithParameter(f float64) {
fmt.Println("MethodWithParameter called with", f) (с параметром float64).
}
Третий обязательный метод
func (m MyType) MethodWithReturnValue() string {
(с возвращаемым значением string).
return "Hi from MethodWithReturnValue"
}
Тип может поддерживать
func (my MyType) MethodNotInInterface() {
интерфейс даже в том слуfmt.Println("MethodNotInInterface called") чае, если содержит другие
}
методы, не входящие в этот
интерфейс.

Многие другие языки требуют явно указать, что MyType поддерживает MyInterface.
Но в Go это происходит автоматически. Если тип содержит все методы, объявленные в интерфейсе, то может находиться в любом месте, где должен использоваться
этот интерфейс, без каких-либо дополнительных объявлений.
360

глава 11

что можно сделать?

Определение типа, поддерживающего интерфейс (продолжение)
Ниже приведена простая программа для тестирования mypkg.
Переменная, объявленная с типом интерфейса, может содержать любое значение, тип
которого поддерживает этот интерфейс. В программе объявляется переменная value
с типом MyInterface, после чего программа создает значение MyType и присваивает
его value. (Это возможно потому, что MyType поддерживает MyInterface.) Затем
вызываются все методы этого значения, являющиеся частью интерфейса.
package main
import (
"fmt"
"mypkg"
)

Объявление переменной с использованием типа
интерфейса.

Значения myType поддерживают myInterface,
func main() {
var value mypkg.MyInterface поэтому это значение может быть присвоено
Мы можем вы- value = mypkg.MyType(5)
переменной с типом myInterface.
звать любой value.MethodWithoutParameters()
метод, входящий value.MethodWithParameter(127.3)
в myInterface. fmt.Println(value.MethodWithReturnValue())
}

MethodWithoutParameters called
MethodWithParameter called with 127.3
Hi from MethodWithReturnValue

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

Тип интерфейса.

«Мне нужно что-то,
чем можно писать».
Конкретные типы.

дальше 4   361

интерфейсы и присваивание

Присваивание любого типа, поддерживающего интерфейс
Если у вас имеется переменная с типом интерфейса, она
может принимать значения любого типа, поддерживающего
интерфейс.
Допустим, есть два типа, Whistle и Horn, каждый из которых содержит метод MakeSound. Мы можем создать интерфейс NoiseMaker, который представляет любой тип с
методом MakeSound. Если объявить переменную toy с типом
NoiseMaker, ей можно будет присваивать как значения
Whistle, так и значения Horn. (Или любых других типов,
которые будут объявлены позднее — при условии, что тип
содержит метод MakeSound.)
Затем можно будет вызвать метод MakeSound для любого
значения, присвоенного переменной toy. И хотя мы точно
не знаем, каким конкретным типом является значение переменной toy, мы знаем, что с ним можно делать: вызывать метод MakeSound. Если его тип не содержит метод MakeSound,
то он не поддерживает интерфейс NoiseMaker, и значение
нельзя будет присвоить переменной.

package main
import "fmt"
type Whistle string

Сод ержит метод MakeSound.

func (w Whistle) MakeSound() {
fmt.Println("Tweet!")
}
Также содержит
метод MakeSound.
type Horn string
func (h Horn) MakeSound() {
fmt.Println("Honk!")
}

Представляет любой тип
с методом MakeSound.

type NoiseMaker interface {
MakeSound()
Объявляется пере}
менная NoiseMaker.
Присваивает переменной значение типа, под- func main() {
держивающего интерфейс NoiseMaker..
var toy NoiseMaker
toy = Whistle("Toyco Canary")
toy.MakeSound()
Присваивает переменной значение другого типа,
toy = Horn("Toyco Blaster")
поддерживающего интерфейс NoiseMaker.
toy.MakeSound()
}

Параметры функций также могут объявляться с типами
интерфейсов. (В конце концов, параметры функций — это
по сути те же переменные.) Если объявить функцию play,
которая получает NoiseMaker, вы можете передать любое
значение типа, содержащего метод MakeSound:
func play(n NoiseMaker) {
n.MakeSound()
}
func main() {
play(Whistle("Toyco Canary"))
play(Horn("Toyco Blaster"))
}

362

глава 11

Tweet!
Honk!

Tweet!
Honk!

что можно сделать?

Вызывать можно только методы, определенные как часть интерфейса
После того как значение будет присвоено переменной (или
параметру метода) с типом интерфейса, для нее можно
будет вызывать только те методы, которые определяются
интерфейсом.
Предположим, вы создали тип Robot, который в дополнение к методу MakeSound также содержит метод Walk. Мы
добавляем вызов Walk в функцию play и передаем play
новое значение Robot.
Однако код не компилируется: в сообщении об ошибке говорится, что значения NoiseMaker не содержат метода Walk.
Почему? Значения Robot содержат метод Walk; определение
у вас прямо перед глазами!
Но функции p l a y передается не значение R o b o t ,
а NoiseMaker. Что, если play вместо него будет передано
значение Whistle или Horn? У них нет методов Walk!
Если у вас имеется переменная с типом интерфейса, то
она гарантированно содержит только те методы, которые
определены в интерфейсе. И только эти методы компилятор
Go позволит вызвать. (Вообще говоря, вы можете получить
информацию о конкретном типе значения для вызова более специализированных методов. Эта возможность будет
рассмотрена позже.)

package main
import "fmt"
type Whistle string
func (w Whistle) MakeSound() {
fmt.Println("Tweet!")
}
type Horn string
func (h Horn) MakeSound() {
fmt.Println("Honk!")
}
Объявление нового типа Robot.
type Robot string

Robot поддерживает интерфейс
NoiseMaker.

func (r Robot) MakeSound() {
fmt.Println("Beep Boop")
Дополнительный
}
метод.
func (r Robot) Walk() {
fmt.Println("Powering legs")
}
type NoiseMaker interface {
MakeSound()
}

func play(n NoiseMaker) {
n.MakeSound()
n.Walk()
Нельзя: не принадлежит NoiseMaker!
}

Можно: часть интерфейса NoiseMaker.

Обратите внимание: переменной с типом интерфейса можно
присвоить тип, содержащий другие методы. Пока вы не вызываете эти другие методы, все будет работать.

func main() {
play(Robot("Botco Ambler"))
}

func play(n NoiseMaker) { Вызываются только те
n.MakeSound()
методы, которые являются
}
частью интерфейса.
func main() {
play(Robot("Botco Ambler"))
}

Ошибка.

n.Walk undefined
(type NoiseMaker has no
field or method Walk)

Beep Boop
дальше 4   363

использование интерфейсов

Сломай и изучи!
Ниже приведена пара конкретных типов, Fan
и CoffeePot . Также имеется интерфейс
Appliance с методом TurnOn.

Fan и CoffeePot содержат метод TurnOn,
поэтому оба типа поддерживают интерфейс

Appliance.

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

type Appliance interface {
TurnOn()
}
type Fan string
func (f Fan) TurnOn() {
fmt.Println("Spinning")
}
type CoffeePot string
func (c CoffeePot) TurnOn() {
fmt.Println("Powering up")
}
func (c CoffeePot) Brew() {
fmt.Println("Heating Up")
}
func main() {
var device Appliance
device = Fan("Windco Breeze")
device.TurnOn()
device = CoffeePot("LuxBrew")
device.TurnOn()
}

Если...

Вызвать метод конкретного типа,
не определенный в интерфейсе:
device.Brew()

Удалить из типа метод, обеспечивающий поддержку интерфейса:

func (c CoffeePot) TurnOn() {
fmt.Println("Powering up")
}

Добавить новое возвращаемое значение или параметр в метод, обеспечивающий поддержку интерфейса:
func (f Fan) TurnOn() error {
fmt.Println("Spinning")
return nil
}

364

глава 11

...программа не будет работать, потому что...

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

Если количество и типы всех параметров и возвращаемых значений не соответствуют определению метода
конкретного типа и определению метода в интерфейсе,
то конкретный тип не поддерживает интерфейс

что можно сделать?

Исправление функции playList
с помощью интерфейса
Посмотрим, можно ли воспользоваться интерфейсом, чтобы функция playList работала с
методами Play и Stop обоих конкретных типов:
TapePlayer и TapeRecorder.

// ...Определение типа TapePlayer...
func (t TapePlayer) Play(song string) {
fmt.Println("Playing", song)
}
func (t TapePlayer) Stop() {
fmt.Println("Stopped!")
}
// ...Определение типа TapeRecorder...
func (t TapeRecorder) Play(song string) {
fmt.Println("Playing", song)
}
func (t TapeRecorder) Record() {
fmt.Println("Recording")
}
func (t TapeRecorder) Stop() {
fmt.Println("Stopped!")
}

В пакете main объявляется интерфейс Player. (Его также можно определить в пакете gadget, но определение интерфейса в том пакете, где он используется, обеспечивает большую гибкость.) Мы указываем,
что интерфейсу необходим как метод Play с параметром string, так и метод Stop без параметров. Это
означает, что оба типа — TapePlayer и TapeRecorder — будут поддерживать интерфейс Player.
Мы обновляем функцию playList так, чтобы она получала любое значение, поддерживающее Player
(вместо конкретного TapePlayer). Мы также меняем тип переменной player с TapePlayer на Player.
Это позволит присвоить player как TapePlayer, так и TapeRecorder. А затем значение любого из
этих типов передается playList!
package main
import "github.com/headfirstgo/gadget"
а.
Определяет тип интерфейс
type Player interface {
Play(string)
Должен содержать метод Play с параметром string.
Stop()
Также необходим метод Stop.
}
Допустимо любое значение, поддерживающее Player,
не только TapePlayer.
func playList(device Player, songs []string) {
for _, song := range songs {
device.Play(song)
}
device.Stop()
}
func main() {
mixtape := []string{"Jessie's Girl", "Whip It", "9 to 5"}
var player Player = gadget.TapePlayer{}
Обновляем пеplayList(player, mixtape)
TapePlayer
ременную для
player = gadget.TapeRecorder{} передается
хранения любого
playList(player, mixtape)
playList.
значения, под}
TapeRecorder держивающего
передается
Player.
playList.

Playing Jessie's Girl
Playing Whip It
Playing 9 to 5
Stopped!
Playing Jessie's Girl
Playing Whip It
Playing 9 to 5
Stopped!
дальше 4   365

интерфейсы и указатели

Будьте
осторожны!

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

Метод toggle для типа Switch, показанный ниже, должен использовать указатель на получателя, чтобы он мог изменить получателя.
package main
import "fmt"
type Switch string
func (s *Switch) toggle() {
if *s == "on" {

*s = "off"
} else {

*s = "on"
}
fmt.Println(*s)
}
type Toggleable interface {
toggle()
}
func main() {
s := Switch("off")
var t Toggleable = s
t.toggle()
t.toggle()
}

Но это приводит к ошибке при присваивании значения Switch
переменной с типом интерфейса Toggleable:
Switch does not implement Toggleable
(toggle method has pointer receiver)

Когда Go принимает решение о том, что значение поддерживает
интерфейс, то методы указателей не включаются для непосредственных значений, но включаются для указателей. Таким образом, проблема решается присваиванием переменной Toggleable
указателя на Switch вместо непосредственного значения
Switch:
var t Toggleable = &s

Присваивание указателя.

После внесения этого изменения код будет работать правильно.

366

глава 11

часто

Задаваемые
вопросы

В:

Должны ли имена типов интерфейсов начинаться с буквы
верхнего или нижнего регистра?

О:

Для имен типов интерфейсов действуют те же правила, что
и для любых других типов. Если
имя начинается с буквы нижнего
регистра, то тип интерфейса
будет неэкспортируемым и недоступным за пределами текущего
пакета. Иногда использовать
объявленные интерфейсы в
других пакетах не нужно, поэтому
интерфейс без экспортирования
вас вполне устроит. Но если вы
хотите использовать интерфейс
в других пакетах, его имя должно
начинаться с буквы верхнего
регистра — это необходимо для
экспортирования интерфейса.

что можно сделать?

Упражнение

package main
import "fmt"

Код справа определяет типы Car
и Truck, каждый из которых содержит
методы Accelerate, Brake и Steer.
Заполните пропуски, чтобы добавить
интерфейс Vehicle, содержащий эти
три метода; код функции main должен
компилироваться и выдавать показанный результат.

type Car string
func (c Car) Accelerate() {
fmt.Println("Speeding up")
}
func (c Car) Brake() {
fmt.Println("Stopping")
}
func (c Car) Steer(direction string) {
fmt.Println("Turning", direction)
}
type Truck string
func (t Truck) Accelerate() {
fmt.Println("Speeding up")
}
func (t Truck) Brake() {
fmt.Println("Stopping")
}
func (t Truck) Steer(direction string) {
fmt.Println("Turning", direction)
}
func (t Truck) LoadCargo(cargo string) {
fmt.Println("Loading", cargo)
}

Место для
вашего кода!

func main() {
var vehicle Vehicle = Car("Toyoda Yarvic")
vehicle.Accelerate()
vehicle.Steer("left")

}

vehicle = Truck("Fnord F180")
vehicle.Brake()
Speeding up
vehicle.Steer("right")

Turning left
Stopping
Turning right

Ответ на с. 382.
дальше 4   367

что такое утверждения типа?

Утверждения типа
Мы определили новую функцию TryOut для тестирования различных методов типов TapePlayer
и TapeRecorder. TryOut имеет один параметр с типом интерфейса Player, так что передать
можно как TapePlayer, так и TapeRecorder.
В TryOut вызываются методы Play и Stop, входящие в интерфейс Player. Мы также вызываем метод Record, который не является частью интерфейса Player, но определяется для типа
TapeRecorder. Сейчас мы передаем TryOut значение TapeRecorder, поэтому все должно быть
нормально, правда?
К сожалению, нет. Как было показано ранее, если значение конкретного типа присваивается переменной с типом интерфейса (включая параметры функций), то вызывать для него можно только
методы, входящие в этот интерфейс, независимо от того, какие еще методы содержит конкретный
тип. Внутри функции TryOut мы имеем дело не со значением TapeRecorder (конкретный тип),
а со значением Player (тип интерфейса). А в интерфейсе Player нет метода Record!
type Player interface {
Play(string)
Stop()
}
func TryOut(player Player) {
player.Play("Test Track")
player.Stop()
ие
Функции передается значен
player.Record()
е подоро
типа TapeRecorder (кот
Не входит в Player! }
er).
Play
держивает интерфейс
Нормально: эти методы
являются частью интерфейса Player.

func main() {
TryOut(gadget.TapeRecorder{})
}

Ошибка.

player.Record undefined (type Player
has no field or method Record)

Необходимо каким-то образом получить значение конкретного типа (который содержит метод
Record). Первое, что обычно приходит в голову — попытаться выполнить преобразование типа
для преобразования значения Player в значение TapeRecorder. Но преобразования типов не
предназначены для использования с типами интерфейсов, поэтому возникает ошибка. В тексте
сообщения рекомендуется воспользоваться чем-то иным:
func TryOut(player Player) {
отает!
Преобразование типа не раб
player.Play("Test Track")
player.Stop()
Ошибка.
recorder := gadget.TapeRecorder(player)
recorder.Record()
cannot convert player (type Player) to type
}

gadget.TapeRecorder: need type assertion

«Утверждение типа» (“type assertion”)? Что это такое?
368

глава 11

что можно сделать?

Утверждения типа (продолжение)
Если у вас имеется значение конкретного типа, присвоенное переменной с типом интерфейса,
утверждение типа позволяет получить информацию о конкретном типе. Утверждение типа
отчасти напоминает преобразование типа. Его синтаксис похож на гибрид вызова метода и
преобразования типа. После значения с типом интерфейса указывается точка, а за ней — пара
круглых скобок с конкретным типом. (А точнее, предполагаемым конкретным типом значения.)

var noiseMaker NoiseMaker = Robot("Botco Ambler")
var robot Robot = noiseMaker.(Robot)
Значение типа интерфейса.

Проверяемый тип.

На обычном языке такое утверждение типа означает примерно следующее: «Я знаю, что
эта переменная использует тип интерфейса NoiseMaker, но уверен, что это значение
NoiseMaker в действительности является Robot».
После использования утверждения типа для возвращения к значению конкретного
типа вы сможете вызывать для него методы, определенные для этого типа, но не являющиеся частью интерфейса.
Этот код присваивает Robot переменной с типом интерфейса NoiseMaker. Для
NoiseMaker можно вызвать метод MakeSound, потому что он является частью интерфейса. Но чтобы вызвать метод Walk, необходимо использовать утверждение типа
для получения значения Robot. После того как мы получим значение Robot (вместо
NoiseMaker), для него можно будет вызывать Walk.
type Robot string
func (r Robot) MakeSound() {
fmt.Println("Beep Boop")
}
func (r Robot) Walk() {
fmt.Println("Powering legs")
}
type NoiseMaker interface {
MakeSound()
па,
...и присваиваем значение ти
}
Определяем переменную
.
ейс
поддерживающего интерф
с типом интерфейса...
func main() {
var noiseMaker NoiseMaker = Robot("Botco Ambler")
noiseMaker.MakeSound()
Вызов метода, который является частью интерфейса.
var robot Robot = noiseMaker.(Robot)
Обратное преобразование к конrobot.Walk()
кретному типу с использованием
}
Вызов метода, опредеутверждения типа.
ленного для конкретного
Beep Boop
Powering legs типа (не для интерфейса).
дальше 4   369

ошибки утверждений типа

Ошибки утверждений типа
Раньше наша функция TryOut не могла вызвать метод Record для значения Player, так как
он не является частью интерфейса Player. Посмотрим, можно ли решить проблему с помощью утверждения типа.
Как и прежде, мы передаем значение TapeRecorder функции TryOut, где оно присваивается
параметру с типом интерфейса Player. Для значения Player можно вызвать методы Play
и Stop, потому что оба этих метода входят в интерфейс Player.
Затем для обратного преобразования Player в TapeRecorder используется утверждение
типа. После этого мы снова вызываем Record, но на этот раз для TapeRecorder.
type Player interface {
Play(string)
Stop()
}

Утверждение типа
func TryOut(player Player) {
используется для
player.Play("Test Track")
перехода к значению
player.Stop()
Сохраняем значение
TapeRecorder.
recorder := player.(gadget.TapeRecorder)
TapeRecorder.
recorder.Record()
}
Вызов метода, определенного

только для конкретного типа.
func main() {
TryOut(gadget.TapeRecorder{})
Playing Test Track
}

Stopped!
Recording

Вроде бы все работает нормально... с TapeRecorder. Но что произойдет, если передать TapePlayer функции TryOut? Сработает ли такое
преобразование, если учесть, что согласно утверждению типа параметр
TryOut в действительности является TapeRecorder?
func main() {
TryOut(gadget.TapeRecorder{})
TryOut(gadget.TapePlayer{})
}

Все компилируется успешно, но при
попытке запустить программу возникает ситуация паники! Как и следовало ожидать, утверждение о том,
что TapePlayer в действительности Паника!
является TapeRecorder, ничем хорошим не кончается. (В конце концов, оно просто неверно.)
370

глава 11

...
Также передаем TapePlayer

Playing Test Track
Stopped!
Recording
Playing Test Track
Stopped!
panic: interface conversion: main.Player
is gadget.TapePlayer, not gadget.TapeRecorder

что можно сделать?

Предотвращение паники при ошибках утверждений типов
Если утверждение типа используется в контексте, который предполагает
только одно возвращаемое значение, а исходный тип не совпадает с типом в утверждении, в программе возникнет ситуация паники на стадии
выполнения (не на стадии компиляции):
var player Player = gadget.TapePlayer{}
recorder := player.(gadget.TapeRecorder)
Паника!

ым типом
Утверждается, что исходн
я на самом
хот
er,
cord
является TapeRe
...
yer
деле это TapePla

panic: interface conversion: main.Player
is gadget.TapePlayer, not gadget.TapeRecorder

Если утверждения типа используются в контексте, в котором ожидаются
несколько возвращаемых значений, то у них есть второе (необязательное) возвращаемое значение, которое сообщает, успешно ли сработало
утверждение или нет. (И в случае неудачи паника не возникает.) Второе
значение имеет тип bool; оно равно true, если исходным типом значения был тип утверждения, или false, если нет. Со вторым возвращаемым
значением можно делать все что угодно, но по общепринятым соглашениям оно обычно присваивается переменной с именем ok.
Ниже приведена обновленная версия кода, которая присваивает результаты утверждения типа переменной с типом конкретного значения
и второй переменной ok. Значение ok используется в команде if для
определения того, безопасно ли вызвать метод Record для конкретного значения (потому что значение Player имеет исходный тип
TapeRecorder), или от вызова следует отказаться (потому что Player
имеет другое конкретное значение).

Это еще одно место для
применения идиомы «запятая-ОК», которая впервые
встретилась вам при рассмотрении ассоциативных
массивов в главе 7.

var player Player = gadget.TapePlayer{}
recorder, ok := player.(gadget.TapeRecorder)
if ok {
Второе возвращаемое значение присваивается переменной.
recorder.Record()
Если исходным типом был тип TapeRecorder,
} else {
для значения вызывается метод Record.
fmt.Println("Player was not a TapeRecorder")
В противном случае выда}
ется сообщение об ошибке
утверждения типа.

Player was not a TapeRecorder
В данном случае конкретным типом был тип TapePlayer, а не TapeRecorder, поэтому утверждение
типа не может быть выполнено, а переменная ok равна false. Выполняется секция else команды if
и выводится сообщение о том, что Player не является TapeRecorder. Ситуация паники во время выполнения предотвращается.
Если при использовании утверждений типов у вас нет полной уверенности в том, какой исходный тип
стоит за значением с типом интерфейса, используйте дополнительное значение ok — это позволит обработать возможные несоответствия типов и предотвратить панику.
дальше 4   371

использование утверждений типов

Тестирование TapePlayer и TapeRecorder с утверждениями типов
Посмотрим, удастся ли нам использовать новые знания для решения проблемы функции TryOut со
значениями TapePlayer и TapeRecorder. Вместо того чтобы игнорировать второе возвращаемое
значение утверждения типов, мы присвоим его переменной ok. Переменная ok содержит true, если
утверждение типа отработало успешно (это означает, что переменная recorder содержит значение
TapeRecorder, для которого можно вызвать Record), или false в противном случае (вызов Record
не безопасен). Вызов метода Record заключается в команду if, которая гарантирует, что метод будет
вызван только в случае правильно выполненного утверждения типа.
type Player interface {
Play(string)
Stop()
}
func TryOut(player Player) {
player.Play("Test Track")
player.Stop()
recorder, ok := player.(gadget.TapeRecorder)
Метод Record вызывается
if
ok {
Второе возвращаемое значение присваивается переменной.
только в том случае, если
исходное значение recorder.Record()
имело
тип TapeRecorder. }
}
func main() {
TryOut(gadget.TapeRecorder{})
TryOut(gadget.TapePlayer{})
}
TapeRecorder передается в...
…утверждение типа успешно, вызван Record.
TapePlayer передается в...

Playing Test Track
Stopped!
Recording
Playing Test Track
Stopped!

…утверждение типа не успешно, Record не вызван.

Как и прежде, в функции main мы сначала вызываем TryOut со значением TapeRecorder. TryOut берет
полученное значение с типом интерфейса Player и вызывает для него методы Play и Stop. Утверждение
о том, что конкретным типом Player является TapeRecorder, выполняется успешно, и для полученного
значения TapeRecorder вызывается метод Record.
Затем функция TryOut снова вызывается для TapePlayer. (Это тот самый вызов, который ранее приводил
к аварийному завершению программы из-за паники в утверждении типа.) Методы Play и Stop вызываются
так же, как в предыдущем случае. При выполнении утверждения типа происходит ошибка, потому что переменная Player содержит тип TapePlayer, а не TapeRecorder. Но поскольку второе возвращаемое значение
сохраняется в переменной ok, утверждение типа на этот раз не вызывает паники. Оно просто присваивает
ok значение false, в результате чего код из команды if не выполняется, а метод Record не вызывается.
(И это хорошо, потому что значения TapePlayer не содержат метода Record.)
Благодаря утверждениям типов наша функция TryOut работает со значениями как типа TapeRecorder,
так и типа TapePlayer!
372

глава 11

что можно сделать?

У бассейна
Справа приведен обновленный код
из предыдущего упражнения. Мы
создаем метод TryVehicle, который вызывает все методы из интерфейса Vehicle. Затем он должен
попытаться применить утверждение
типа для получения конкретного значения Truck, и в случае успеха —
вызвать LoadCargo для значения
Truck.
Выловите из бассейна фрагменты кода и разместите их в пустых
строках кода. Каждый фрагмент
может использоваться только один
раз; использовать все фрагменты не
обязательно. Ваша задача: создать
программу, которая работает и выводит показанный результат.

Результат.

Примечание: каждый
предмет из бассейна
может использоваться
только один раз!

Speeding up
Turning left
Turning right
Stopping
Loading test cargo

type Truck string
func (t Truck) Accelerate() {
fmt.Println("Speeding up")
}
func (t Truck) Brake() {
fmt.Println("Stopping")
}
func (t Truck) Steer(direction string) {
fmt.Println("Turning", direction)
}
func (t Truck) LoadCargo(cargo string) {
fmt.Println("Loading", cargo)
}
type Vehicle interface {
Accelerate()
Brake()
Steer(string)
}
func TryVehicle(vehicle
) {
vehicle.
vehicle.Steer("left")
vehicle.Steer("right")
vehicle.Brake()
truck,
:= vehicle.
if ok {

.LoadCargo("test cargo")
}
}
func main() {
TryVehicle(Truck("Fnord F180"))
}

truck
Accelerate()
Vehicle

(Vehicle)

(Truck)

Truck

vehicle
ok

Ответ на с. 382.
дальше 4   373

интерфейс error

Интерфейс error
В завершение этой главы мы рассмотрим несколько интерфейсов, встроенных в Go. Эти интерфейсы не рассматривались явно, но мы уже неоднократно использовали их в программах.
В главе 3 вы узнали, как создавать собственные значения ошибок. Тогда мы сказали: «Значение
ошибки — это любое значение с методом Error, который возвращает строку».
Возвращает значение ошибки.

33333)
of %0.2f is invalid", -2.
err := fmt.Errorf("a height
fmt.Println(err.Error())
d
a height of -2.33 is invalid
fmt.Println(err)
is invali

Выводит сообщение
об ошибке.
щение об ошибке.
Также выводит сооб

a height of -2.33

Тип, который включает любое
значение с конкретным методом...
Похоже на интерфейс!

Все верно. Тип error — это всего лишь интерфейс! Он выглядит примерно так:
type error interface {
Error() string
}

Объявление типа error как интерфейса означает, что если тип содержит метод
Error, который возвращает string, то он поддерживает интерфейс error и может
использоваться в качестве значения ошибки. А это означает, что вы можете определять собственные типы и использовать их везде, где требуется значение ошибки!
Например, ниже приведен простой определяемый тип ComedyError. Так как он
содержит метод Error, который возвращает string, то этот тип поддерживает
интерфейс error и его можно присвоить переменной с типом error.
Определяем тип с базовым
типом string.

type ComedyError string
Поддерживает интерфейс error.
func (c ComedyError) Error() string {
return string(c)
Метод Error должен возвращать строку,
}
поэтому выполняется преобразование типа.
Создается переменинтерфейс error, поэfunc main() {
ComedyError поддерживает
ная с типом error.
своить ComedyError.
var err error
тому переменной можно при
err = ComedyError("What's a programmer's favorite beer? Logger!")
fmt.Println(err)
}

What's a programmer's favorite beer? Logger!

374

глава 11

что можно сделать?

Интерфейс error (продолжение)
Если кроме значения ошибки требуется еще и отслеживать более подробную информацию о ней
в формате, отличном от строки, вы можете создать свой собственный тип, поддерживающий
интерфейс error, и сохранить в нем нужную информацию.
Допустим, вы пишете программу, которая следит за некоторым устройством и предотвращает
его перегрев. Ниже приведен тип OverheatError, который помогает в решении этой задачи.
Он содержит метод Error и поэтому поддерживает error. Но при этом в качестве базового
типа используется float64, что позволяет нам отслеживать степень перегрева.
Определяем тип с базовым
типом float64.
type OverheatError float64
Поддерживает
func (o OverheatError) Error() string {
интерфейс error.
return fmt.Sprintf("Overheating by %0.2f degrees!", o)
}
Температура ис-

пользуется в сообщении об ошибке.

Ниже приведена функция checkTemperature, которая использует
OverheatError. В параметрах она получает фактическую температуру системы и температуру, которая считается безопасной. Функция
указывает, что она возвращает значение типа error, а не конкретно
OverheatError, но это нормально, потому что OverheatError
поддерживает интерфейс error. Если фактическая температура
превышает безопасную, checkTemperature возвращает новое
значение OverheatError с превышением температурного порога.
Указывает, что функция возв
ращает обычное значение оши
бки.

func checkTemperature(actual float64, safe float64) error {
excess := actual - safe Если фактическая температуif excess > 0 {
ра превышает безопасную...
return OverheatError(excess)
}
…возвращается знаreturn nil
чение OverheatError
}

с превышением безопасной температуры.

func main() {
var err error = checkTemperature(121.379, 100.0)
if err != nil {
log.Fatal(err)
}
}

2018/04/02 19:27:44 Overheating by 21.38 degrees!

В:

часто

Задаваемые
вопросы

Как мы использовали тип интерфейса error во всех этих пакетах
без импорта? Его имя начинается
с буквы нижнего регистра. Разве это не
означает, что он не экспортируется из
пакета, в котором объявлен? И в каком
пакете объявляется error, если на
то пошло?

О:

Тип error является «предварительно объявленным идентификатором», как int или string. И как все
остальные предварительно объявленные
идентификаторы, он не является частью
никакого пакета. Он является частью
«универсального блока»; это означает, что
он доступен везде, независимо от того,
какой пакет является текущим.
Помните, что существуют блоки if
и for, которые могут содержаться
в блоках функций, которые в свою очередь
могут содержаться в блоках пакетов?
Так вот, универсальный блок содержит
все блоки пакетов. Это означает, что все
определенное в универсальном блоке
может использоваться в любом пакете без
его импорта. И к этой категории относятся
ошибки и все остальные предварительно
определенные идентификаторы.
дальше 4   375

интерфейс stringer

Интерфейс Stringer
Помните типы Gallons, Liters и Milliliters, созданные в главе 9 для того,
чтобы различать разные единицы измерения объема? Как выяснилось, различать
их не так уж просто. Двенадцать галлонов сильно отличаются от 12 литров или
12 миллилитров, но при выводе все значения выглядят одинаково. А если значение
содержит слишком много цифр в дробной части, то выглядит громоздко.
type Gallons float64
type Liters float64
type Milliliters float64

Создаем и выводим
значение Gallons.

func main() {
fmt.Println(Gallons(12.09248342))
fmt.Println(Liters(12.09248342))
Создаем и выводим значение Liters.
fmt.Println(Millilite
rs(12.09248342))
Создаем и выводим значение Milliliters.
}
Все три значения выглядят одинаково!

12.09248342
12.09248342
12.09248342

Можно воспользоваться функцией Printf для округления числа и вывода сокращенного обозначения единиц измерения. С другой стороны, если это придется
делать при каждом использовании этих типов, это быстро надоест.
Форматирование fmt.Printf("%0.2f gal\n", Gallons(12.09248342))
чисел и добавление fmt.Printf("%0.2f L\n", Liters(12.09248342))
сокращений. fmt.Printf("%0.2f mL\n", Milliliters(12.09248342))

12.09 gal
12.09 L
12.09 mL

Именно для этой цели пакет fmt определяет интерфейс fmt.Stringer: чтобы любой
тип мог решить, как он должен отображаться при выводе. Любой тип легко подготовить для поддержки Stringer; достаточно определить метод String(), который
возвращает строку. Определение интерфейса выглядит так:
type Stringer interface {
String() string
}

Любой тип поддерживает fmt.Stringer,
если он содержит метод String, который возвращает строку.

Например, здесь мы изменяем тип CoffeePot для поддержки Stringer:
type CoffeePot string
func (c CoffeePot) String() string {
return string(c) + " coffee pot"
}
func main() {
coffeePot := CoffeePot("LuxBrew")
fmt.Println(coffeePot.String())
}

376

глава 11

Поддерживает интерфейс Stringer.
Метод должен вернуть строку.

LuxBrew coffee pot

что можно сделать?

Интерфейс Stringer (продолжение)
Многие функции из пакета fmt проверяют, поддерживают ли переданные им
значения интерфейс Stringer, и если поддерживают — вызывают их методы
String. В частности, это относится к Print, Println и Printf, а также к
другим функциям. Теперь, когда CoffeePot поддерживает Stringer, мы
можем передавать значения CoffeePot непосредственно этим функциям,
и возвращаемое значение метода String значения CoffeePot будет использовано при выводе:
Создание значения CoffeePot.

coffeePot := CoffeePot("LuxBrew")
CoffeePot переда- fmt.Print(coffeePot, "\n")
ется различным fmt.Println(coffeePot)
функциям fmt. fmt.Printf("%s", coffeePot)

Возвращаемое значение String используется в выводе.

LuxBrew coffee pot
LuxBrew coffee pot
LuxBrew coffee pot

Перейдем к примеру более серьезного использования типа интерфейса.
Сделаем так, чтобы наши типы Gallons, Liters и Milliliters поддерживали Stringer. Мы переместим свой код форматирования этих значений
в методы String, связанные с каждым типом. Вместо Printf метод будет
вызывать функцию Sprintf и возвращать полученное значение.

Чтобы тип Gallons подtype Gallons float64
func (g Gallons) String() string {
держивал Stringer.
return fmt.Sprintf("%0.2f gal", g)
}

Чтобы тип Liters поддерtype Liters float64
func (l Liters) String() string {
живал Stringer.
return fmt.Sprintf("%0.2f L", l)
}

type Milliliters float64
func (m Milliliters) String() string {
return fmt.Sprintf("%0.2f mL", m)
}

Чтобы тип Milliliters
поддерживал Stringer.

func main() {

Значения каждого fmt.Println(Gallons(12.09248342))
типа передаются fmt.Println(Liters(12.09248342))
Println. fmt.Println(Millilite

rs(12.09248342))
}

12.09 gal
12.09 L
12.09 mL

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

Теперь каждый раз, когда значения Gallons, Liters и Milliliters передаются Println (или большинству других функций fmt), будут вызываться их
методы String, а возвращаемые значения будут использоваться в выводе. Мы
определим удобный формат по умолчанию для вывода каждого из этих типов!
дальше 4   377

пустой интерфейс

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

…передаем число с плавающей
точкой...

Вызываем fmt.
Println...

…строку...

…и логическое
значение!
fmt.Println(3.1415, "A string", true)

3.1415 A string true

Хороший вопрос! Выполним команду go doc, чтобы вывести документацию fmt.Println, и посмотрим, с каким типом объявлены его параметры...
Просмотр документации
для функции «Println»
пакета «fmt».

я получает
«...» означает, что функци
ументов.
арг
о
ств
переменное количе
{}»?
ace
Но что такое «interf

File Edit Window Help

$ go doc fmt Println
func Println(a ...interface{}) (n int, err error)
Println formats using the default formats for its operands and writes to
standard output. Spaces are always added between operands and a newline...

Как было показано в главе 6, ... означает, что это функция с переменным количеством аргументов. Но что это за тип interface{}?
Помните: объявление interface определяет методы, которые должен содержать
тип для поддержки этого интерфейса. Например, наш интерфейс NoiseMaker
поддерживается любым типом, который содержит метод MakeSound.
type NoiseMaker interface {
MakeSound()
}

Но что произойдет, если мы объявим тип интерфейса, которому вообще не
требуются никакие методы? Он будет поддерживаться любым типом! Он будет
поддерживаться всеми типами!
type Anything interface {
}

378

глава 11

что можно сделать?

Пустой интерфейс (продолжение)
Тип interface{} называется пустым интерфейсом, и он используется для
передачи значений любого типа. Пустой интерфейс не содержит методов, необходимых для его поддержки,поэтому он поддерживается каждым типом.
Если вы объявите функцию, которая получает параметр с типом пустого интерфейса, то в ее аргументах могут передаваться значения любого типа:
Получает параметр с типом
пустого интерфейса.
func AcceptAnything(thing interface{}) {
}

func main() {
AcceptAnything(3.1415)
AcceptAnything("A string")
AcceptAnything(true)
AcceptAnything(Whistle("Toyco Canary"))
}

Все эти типы
могут передаваться нашей
функции!

Но не торопитесь и не начинайте использовать пустые интерфейсы для
всех параметров ваших функций! Если у вас имеется значение с типом
пустого интерфейса, сделать с ним можно не так уж много.
Многие функции в fmt получают значения с типами пустых интерфейсов,
поэтому им можно передать все эти значения:

Пустому интерфейсу
не требуются
никакие методы для
поддержки, поэтому
он поддерживается
всеми типами.

func AcceptAnything(thing interface{}) {
fmt.Println(thing)
}
func main() {
AcceptAnything(3.1415)
AcceptAnything(Whistle("Toyco Canary"))
}

3.1415
Toyco Canary

Не пытайтесь вызывать какие-либо методы для значения с типом пустого интерфейса! Помните: если у вас имеется значение с типом интерфейса, то вызвать
для него можно только те методы, которые входят в этот интерфейс. А пустой
интерфейс не содержит никаких методов. Это означает, что у значения с типом
пустого интерфейса нет методов, которые вы могли бы вызвать!
func AcceptAnything(thing interface{}) {
для значения
fmt.Println(thing)
Пытаемся вызвать метод
а...
ейс
thing.MakeSound()
ерф
с типом пустого инт
}
Ошибка.

thing.MakeSound undefined (type interface {} is interface with no methods)
дальше 4   379

пустой интерфейс

Пустой интерфейс (продолжение)
Чтобы вызывать методы для значения с типом пустого интерфейса, необходимо воспользоваться утверждением типа для получения значения
конкретного типа.
func AcceptAnything(thing interface{}) {
Использование утверждения
.
fmt.Println(thing)
типа для получения Whistle
whistle, ok := thing.(Whistle)
if ok {
whistle.MakeSound()
Вызов метода для Whistle.
}
}
func main() {
AcceptAnything(3.1415)
AcceptAnything(Whistle("Toyco Canary"))
}

3.1415
Toyco Canary
Tweet!

А теперь сравните с функцией, которая получает только этот
конкретный тип.
func AcceptWhistle(whistle Whistle) {
Получает Whistle.
fmt.Println(whistle)
Вызов метода. Никакие преwhistle.MakeSound()
образования типа не нужны.
}

Итак, полезность пустых интерфейсов при определении собственных функций весьма ограниченна. При этом вы будете постоянно использовать пустые
интерфейсы с функциями из пакета fmt и в других местах тоже. И когда вы
в следующий раз увидите параметр interface{} в документации функции,
вы будете совершенно точно знать, что он означает!
При определении переменных или параметров функций часто бывает известно, с чем вы работаете. В таких случаях можно использовать конкретный
тип — такой, как Pen, Car или Whistle. В других случаях вас интересует лишь
то, что может делать значение. И тогда, возможно, стоит определить тип
интерфейса — такой, как WritingInstrument, Vehicle или NoiseMaker.
Методы, которые должны вызываться для значения, определяются как часть
типа интерфейса. И тогда вы можете выполнять присваивание или вызывать
свои функции, не особенно беспокоясь о конкретном типе значений. Если
значение содержит нужные методы, вы сможете им пользоваться!

380

глава 11

что можно сделать?

Ваш инструментарий Go

s
arcrkaaygse
l
P
peescia
A
s
ofntas
e
isfs vaaslu
c
ee
liapsctosrkspayscp
SM
e
s
n
m
at
ole
tf ipew
wyeis
oim
rrfourian
ddlsaoTDliaettlirfcisioottcnio
uhtereth
G
eta
n
n
o
TAhS
e
’s
t
k
o
a
D
o
e
u
a
li
t
a
n
hw
looievlln.agluйeсellt,ы
uunsat lieke a
cyp
etcaoetcprhis
sntu
tyeoju
a
li
od
ud
bn
isp
y
a
AirM
on
r etticheer
yd
a io
е
ad
e
cn
ype
is
E
ф
esm
b
s
t
r
is
t
n
u
р
la
r
c
m
dpA
е
u
cto
u
la
io
o
.adtдоitв,
argeit
E
it
r
ic
u
t
ruw
tp
itin
arndea
in
in
gdp
нdoarт
tcfico
inrjo
arstp
fo
elu
eea
A
ew
in
ed.bsety
ftoh
a
h
t
is
d
le
eG
r
b
e
h .оa
d
o
еrtт
is
is
o
y
il
r
h
n
f
ofTyaИ
a
y
o
t
r
dtio astvra орepм
s ve

sy
sf uous ьr .nн, аeбxcp
m
o
e ee—
toetla
ldcnh
yeom
p
ceou
tpy
yrnis
u
heocaA
atatin
ryrn
r,p
h
ls
fatoo
tn
syctttsay
w
io
сkita
in
т
orAa
tsn
й
rn
pm
aentegn.ea
ва
ep
k

ф
in
esеfago
.a
if
aecit
atgfcseo
d
in
d
еneрd
enrиeo
enrИ
рtoж
p
h
f
е
rcE
a
o
a
e
c
.
a
д
aca
t
io
n
н
lyhin
д
s
t
m
rnnit
e
p
d
E
о
e
d
a
n
in
e
u
п
m
r
s
n
e
h
s
r eahrnidvsin
lu
g
t
a
u
t
a
le
oftu
n
a
a
ы
m
o
e
p
b
e
a
e
н
n
g
w
le
c
r
p
y
a
is
e
ж
e
y
s
in
a
a
d
trы
te dfodrem
e cdreselia
оteл
in
cb
pcyn
oruoa
yiv
cotvaeр
еrdeдcem
un
Ytooo
erm
ar e
rfctsк
ям
sh
ba
и
eиre. s
aчtteea
bin
fa
еn
ddeaоsesm
rm
оaт
ruaosyvtsa
boeeиds eзtm
lu
u
tнeo
orafrid
o
cco
нhа
h
tin
a
lu
d
in
hm
aaptecdan
d
a
ic
t
ia
t
r
E
c
if
m
y
a
a
m
g
м
e
c
e
e
s
a
n
h
c
s
e
a
ы
t
r
t
r
s
r
a
a
p
a
e
р
,
is
g
a
p
s
p
r
s
e
о
h
aкin
paew
rohfeield
. a
oy
т
pW
ekxsesesfieldsly
seld
оo
ta
ld
g
’t
cnodrm
in
нply
oеn
яsered
Tli
eth
aisresm
rrta
е. р
rrreeй
eсtrsin
.euoe

rsd
ehuehrcosk
cna
сecо
doa
eyoд
.nsobw
.sy
tdh
usep
eytaegrrtin
ceekiv
Sir
tm
o
n
r
o
c
A
e
a
t
р
cad
c
f
s
e
e
о
r
s
eth
li
e
s
m
e
e
т
r
e
unT
p
dnttyotpyep
d
r
о
h
a
o
h
A
y
e. е
e
o
t
к
s
t
n
.
s
f
s
u
,
ucstevelv
e
;
m
y
e
o
s
п
e
lu
n
terom
t
r
d
и
e
shh
b
a
a
a
a
o
n
eе,чtи
fnreрd
)yp
n
сpл
aetенны
oad
dп
a
nsetт

m
w
cm
ooeй
tyh
о
le
bt,sem
б
s
h
n
n
tou
lm
t
m
n
ю
e
d
f
io
in
a
e
o
m
o
d
t
y
(
Л
a
h
a
a
t
n
.
e
n
la
n
it
e
n
le
a
a
n
u hh
ы
hseu
pe.оeдc sw
ptosold
seailrеay
одtu
oef F
t udbtaeetrtйhсty
n
naecin
sdo
am
EM
tdtвotseсy
ali

v
tale
еep.aeм
а
s kscoт
m
ie
v,haпlu
fm
in
r
b
s
w
e
y
g
t
y
h
е
h
n
il
ie
u
e
c
e
t
t
ф
a
a
v
o
a
.
in
и
v
р
h
o
y
E
d
a
n
uesttro
a
е
’s
a ee
.t a нepт
lt es ymw
aagp
d
in
a
foro
totgm
pspe.arж
aey
m
cin
fileD
rtnrеeb
o
a
a
tly
ythdee,ovaif
an
rdс. . e
alu
иrgиrcatиll
eaф
frn
нin
ocдa
heр

ssie

й
p
л
ll
е
s
е
t
t
A
ly
e
o
s
e
е
r
ll
g
e
d
a
ls
e
a
n
u
a
d
d
m
п
d
т
d
o
о
n
e
is
н
ly
h
n
u
ll
euleam
в
и
n
t
a
a
a
e
o
s
n
t
c
,
y
a
a
ed d
от
sme tycpaen
beт
mtm

theepykeoe. u won’t
. osэtтcbou
yp
utвrla
em
sa
saE

Tnhtcеeha
ep
uetesrurty
ttychpaeen,ob
stsaи
taж
e
e еaрs- e
uhcр
нpт
m
n
soetfrд
иeй ctиy
tefe
sses th
saщtm
ю
ingeed. Yoou рe
e
а
b cdh
e
в
c
h
и
a
t
an
ж
оенd
пeоhдteдorе bcode tha ь присвw
e
’tипh,av
dobnТ
быoт
is allo рамет
e
е
reak o,tsм
d
ж
о
c
а
.
o
п
n
e
с
и
й
e
lu
ил
фvеa
aus
thdea
ta, becпеременной
бъявле
орые о
любой
т
о
к
,
и
а
to.
ейс .
ункци
нтерф
и
тру ф
о
г
о
т
ипом э
ны с т

ƒƒ Конкретный тип указывает не только то, что могут делать значения (какие методы для них можно вызывать),
но и то, чем они являются: он задает базовый тип,
в котором хранятся данные значения.
ƒƒ Тип интерфейса — абстрактный тип. Интерфейсы
не описывают, чем является значение: они ничего не
говорят о том, какой базовый тип используется или как
хранятся его данные. Они описывают только то, что
значение может делать: какие методы оно содержит.
ƒƒ Определение интерфейса должно содержать список
имен методов со всеми параметрами или возвращаемыми значениями, которые должны иметь такие методы.
ƒƒ Чтобы поддерживать интерфейс, тип должен содержать
все методы, заданные в интерфейсе. Имена методов,
типы параметров (если они есть) и типы возвращаемых
значений (если они есть) должны совпадать с теми,
которые определены в интерфейсе.
ƒƒ Тип может содержать методы помимо тех, которые перечислены в интерфейсе, но никакие из обязательных
методов не могут отсутствовать; в противном случае
тип не поддерживает интерфейс.
ƒƒ Тип может поддерживать сразу несколько интерфейсов,
и интерфейс может поддерживаться сразу несколькими
типами.
ƒƒ Поддержка интерфейсов достигается автоматически.
В Go не нужно специально объявлять, что конкретный
тип поддерживает интерфейс.
ƒƒ Для переменной с типом интерфейса можно вызывать
только те методы, которые определены в интерфейсе.
ƒƒ Если значение конкретного типа присвоено переменной с типом интерфейса, вы можете воспользоваться
утверждением типа для получения значения конкретного типа. Только после этого вы сможете вызывать
методы, определенные для конкретного типа (но не
для интерфейса).
ƒƒ Утверждения типов возвращают второе логическое значение, которое сообщает, успешно ли было выполнено
утверждение.
car, ok := vehicle.(Car)

ГЛАВА 11

Глава 11 осталась позади!
В ней ваш инструментарий
пополнился интерфейсами.

КЛЮЧЕВЫЕ
МОМЕНТЫ

дальше 4   381

решние упражнений

У бассейна. Решение
type Car string
func (c Car) Accelerate() {
fmt.Println("Speeding up")
}
func (c Car) Brake() {
fmt.Println("Stopping")
}
func (c Car) Steer(direction string) {
fmt.Println("Turning", direction)
}
type Truck string
func (t Truck) Accelerate() {
fmt.Println("Speeding up")
}
func (t Truck) Brake() {
fmt.Println("Stopping")
}
func (t Truck) Steer(direction string) {
fmt.Println("Turning", direction)
}
func (t Truck) LoadCargo(cargo string) {
fmt.Println("Loading", cargo)
}
type Vehicle interface {

}

Accelerate()
Brake()
Steer(string)

Не забудьте указать,
что Steer принимает
параметр!

func main() {
var vehicle Vehicle = Car("Toyoda Yarvic")
vehicle.Accelerate()
vehicle.Steer("left")

}

382

vehicle = Truck("Fnord F180")
vehicle.Brake()
vehicle.Steer("right") Speeding up

Turning left
Stopping
Turning right

глава 11

type Truck string
func (t Truck) Accelerate() {
fmt.Println("Speeding up")
}
func (t Truck) Brake() {
fmt.Println("Stopping")
}
func (t Truck) Steer(direction string) {
fmt.Println("Turning", direction)
}
func (t Truck) LoadCargo(cargo string) {
fmt.Println("Loading", cargo)
}
type Vehicle interface {
Accelerate()
Brake()
Steer(string)
}
func TryVehicle(vehicle Vehicle ) {
vehicle. Accelerate()
vehicle.Steer("left")
vehicle.Steer("right")
vehicle.Brake()
truck, ok := vehicle. (Truck)
if ok {
Утверждение типа успешно?
truck .LoadCargo("test cargo")
}
Содержит Truck, а не (просто)
}
Vehicle, поэтому мы можем вызвать LoadCargo.
func main() {
TryVehicle(Truck("Fnord F180"))
}

Speeding up
Turning left
Turning right
Stopping
Loading test cargo

12 Снова на ногах

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

Любая программа сталкивается с ошибками. Учтите их при
планировании. Иногда обработка ошибки сводится к простому выводу
сообщения и завершению программы. Другие ошибки требуют дополнительных
действий: например, закрытия открытых файлов или сетевых подключений или
иного освобождения ресурсов, чтобы после вашей программы оставался порядок. В этой главе мы покажем, как отложить завершающие действия, чтобы
они выполнились даже в случае ошибки. Также вы узнаете, как поднять панику
в тех (редких) ситуациях, когда это уместно, и как восстановиться после нее.

вызов важных функций

Снова о чтении чисел из файла
В книге было немало сказано об обработке ошибок.
Но способы, о которых говорилось ранее, подходят
не для каждой ситуации. Рассмотрим один из таких
сценариев. Напишем программу sum.go, которая
читает значения float64 из текстового файла, суммирует их и выводит сумму.
В главе 6 была создана функция
GetFloats, которая открывает текстовый файл, преобразует каждую
строку файла в значение float64 package main
и возвращает эти значения в виде
import (
сегмента.
Здесь мы переместили функцию
GetFloats в пакет main и обновили
ее, чтобы для открытия и закрытия
текстового файла в ней использовались две новые функции, OpenFile )
и CloseFile.
Открывает файл и возвращает указатель на него, а также
значение обнаруженной ошибки.
Закрывает файл.

Вместо прямого вызова os.Open
вызывается функция OpenFile.

Вместо прямого вызова file.Close
вызывается CloseFile.

384

глава 12

"bufio"
"fmt"
"log"
"os"
"strconv"

Shell Edit View Window Help

2.12
4.0
3.5
data.txt

$ go run sum.go data.txt
Opening data.txt
Closing file
Sum: 9.62

Весь этот код был перемещен в пакет «main»
в исходном файле sum.go.

func OpenFile(fileName string) (*os.File, error) {
fmt.Println("Opening", fileName)
return os.Open(fileName)
}
func CloseFile(file *os.File) {
fmt.Println("Closing file")
file.Close()
}
func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
number, err := strconv.ParseFloat(scanner.Text(), 64)
if err != nil {

return nil, err
}
numbers = append(numbers, number)
}
CloseFile(file)
if scanner.Err() != nil {
return nil, scanner.Err()
}
return numbers, nil
}

снова на ногах

Снова о чтении чисел из файла (продолжение)
Имя файла, из которого будут читаться данные, передается в аргументе командной строки.
В главе 6 уже упоминался os.Args — сегмент строковых значений, содержащий все аргументы,
заданные при запуске программы.
В функции main мы получаем имя открываемого файла из первого аргумента командной строки os.Args[1]. (Напомним, что элемент os.Args[0] содержит имя запускаемой программы;
фактические аргументы появляются в os.Args[1] и последующих элементах.)
Затем имя файла передается GetFloats для чтения данных из файла, и программа получает
сегмент значений float64.
Если при этом будут обнаружены какие-либо ошибки, они будут возвращены функцией GetFloats
и сохранены в переменной err. Если значение err не равно nil, значит, произошла ошибка,
поэтому программа просто выводит ее и завершается.
В противном случае это означает, что файл был прочитан успешно, поэтому используется цикл
for для суммирования всех значений в сегменте, после чего выводится полученная сумма.
Сохраняется сегмент чисел, прочитанных
из файла, со значением ошибки..

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

func main() {
numbers, err := GetFloats(os.Args[1])
Если произошла ошибка, программа if err != nil {
log.Fatal(err)
выводит сообщение и завершается.
}
var sum float64 = 0
Суммируем все числа for _, number := range numbers {
sum += number
в сегменте.
}
fmt.Printf("Sum: %0.2f\n", sum)
Выводится сумма.
}

Сохраните весь этот код в файле с именем sum.go. Затем создайте
обычный текстовый файл с числами, по одному числу в строке.
Сохраните файл под именем data.txt в одном каталоге с sum.go.
Запустите программу командой go run sum.go data.txt.
Строка "data.txt" будет первым аргументом программы sum.go,
поэтому это имя файла будет передано GetFloats.
Мы видим, где вызываются функции O p e n F i l e и
CloseFile, потому что обе
функции включают вызовы
fmt.Println. И в конце вывода мы видим сумму всех
чисел в data.txt. Похоже, все
работает!

data.txt передается в аргументе командной строки..

Здесь вызывается функция OpenFile.
Здесь вызывается функция CloseFile.
Сумма всех чисел в файле.

20.25
5.0
10.5
15.0
data.txt

Shell Edit View Window Help

$ go run sum.go data.txt
Opening data.txt
Closing file
Sum: 50.75

дальше 4

385

команды defer

Любая ошибка помешает закрытию файла!
Если программа sum.go получит неправильно отформатированный файл, то возникнут проблемы. Например, файл со
строкой, которая не может быть преобразована в float64,
приводит к возникновению ошибки.
Программа запускается для файла
с некорректными данными.

Здесь вызывается функция OpenFile.
При чтении файла происходит ошибка...
Функция CloseFile не будет вызвана!

Не преобразуется в float64!

20.25
hello
10.5
bad-data.txt

Shell Edit View Window Help

$ go run sum.go bad-data.txt
Opening data.txt
2018/04/07 21:18:09 strconv.ParseFloat:
parsing "hello": invalid syntax
exit status 1

Само по себе это не страшно; любая программа время от времени получает недействительные данные. Но функция GetFloats должна вызвать функцию CloseFile после
завершения работы. Мы не видим в выводе программы сообщения «Closing file»,
а это наводит на мысль, что функция CloseFile вообще не вызывается!
Проблема в том, что при вызове strconv.ParseFloat для строки, которая не может
быть преобразована в float64, возвращается ошибка. Наш код устроен так, что в этот
момент происходит выход из функции GetFloats.
Но возврат происходит до вызова
CloseFile, что означает, что файл
никогда не будет закрыт!

func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName)
if err != nil {
return nil, err
}
Функция ParseFloat возвращает ошибку,
scanner := bufio.NewScanner(file)
если ей не удается преобразовать текfor scanner.Scan() {
number, err := strconv.ParseFloat(scanner.Text(), 64)
стовую строку в float64...
if err != nil {
return nil, err
...это приводит к тому, что GetFloats
}
возвращает ошибку...
numbers = append(numbers, number)
}
...а это означает, что функция
CloseFile(file)
CloseFile не будет вызвана!
if scanner.Err() != nil {
return nil, scanner.Err()
}
return numbers, nil
}

386

глава 12

снова на ногах

Отложенные вызовы функций
На первый взгляд в незакрытом файле нет ничего страшного. И наверное, для простой программы, которая просто открывает один файл, это правда. Но каждый файл, который остается открытым, продолжает поглощать ресурсы операционной системы. Со временем незакрытые файлы могут накапливаться
и приводить к сбою программы или даже снижать производительность всей системы. Очень важно
завести привычку закрывать файлы тогда, когда программа завершила работу с ними.
Но как это сделать? Функция GetFloats устроена так, что она немедленно завершает работу при ошибках во время чтения файла, даже если еще не была вызвана функция CloseFile!
Если у вас есть вызов функции, который должен быть гарантированно выполнен в любом случае, используйте команду defer. Ключевое слово defer может располагаться перед любым вызовом обычной
функции или метода, и Go отложит вызов этой функции вплоть до выхода из текущей функции.
Обычно вызовы функций выполняются сразу же после их обнаружения в программе. В этом коде вызов
fmt.Println("Goodbye!") располагается перед двумя другими вызовами fmt.Println.
package main
import "fmt"
func Socialize() {
fmt.Println("Goodbye!")
fmt.Println("Hello!")
fmt.Println("Nice weather, eh?")
}
func main() {
Socialize()
}

Goodbye!
Hello!
Nice weather, eh?

Но если добавить ключевое слово defer перед вызовом fmt.Println("Goodbye!"),
этот вызов будет выполнен только после того, как отработает весь остальной код
в функции Socialize, а функция Socialize завершится.
package main
import "fmt"
func Socialize() {
Перед вызовом
defer fmt.Println("Goodbye!")
fmt.Println("Hello!")
функции добавfmt.Println("Nice weather, eh?")
ляется ключевое
}
слово «defer».
Первый вызов
функции откладыfunc main() {
Socialize()
вается до завер}
шения Socialize!

Hello!
Nice weather, eh?
Goodbye!

дальше 4

387

использование defer

Восстановление после ошибок
Здорово, конечно, но вы что-то говорили
об использовании defer для вызовов
функций, которые должны выполняться
«в любом случае». Объясните?

Ключевое слово defer гарантирует, что вызов функции
будет выполнен даже в том случае, если вызывающая
функция завершится преждевременно — например, из-за
ключевого слова return.
Ниже приведена обновленная версия функции
S o c i a l i z e , которая возвращает ошибку. Выход
из S o c i a l i z e происходит перед вызовом f m t .
Println("Nice weather, eh?"). Но из-за того, что
перед вызовом.Println("Goodbye!") располагается
ключевое слово defer, Socialize всегда будет выводить
сообщение «Goodbye!» перед завершением диалога.
package main
import (
"fmt"
"log"
)

Вывод сообщения «Goodbye!»
откладывается.

func Socialize() error {
defer fmt.Println("Goodbye!")
fmt.Println("Hello!")
return fmt.Errorf("I don't want to talk.")
Этот код не будет fmt.Println("Nice weather, eh?")
выполнен! return nil
}
func main() {
err := Socialize()
if err != nil {
log.Fatal(err)
}
}

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

388

глава 12

Ключевое слово
«defer» гарантирует,
что вызов функции
будет выполнен
даже в том случае,
если вызывающая
функция завершится
преждевременно.

Возвращает ошибку.

Hello!
Goodbye!
2018/04/08 19:24:48 I don't want to talk.

снова на ногах

Использование отложенного вызова для гарантированного закрытия файлов
Так как ключевое слово defer может гарантировать, что вызов функции будет выполнен «в любом случае», обычно оно используется для кода, который
должен быть выполнен даже в случае ошибки. Один из типичных примеров
такого рода — гарантированное закрытие файлов, открытых в программе.
Именно это и должно происходить в функции GetFloats программы sum.go. После вызова функции OpenFile программа должна
вызвать CloseFile даже в том
случае, если при разборе содержимого файла произойдет ошибка.

func OpenFile(fileName string) (*os.File, error) {
fmt.Println("Opening", fileName)
return os.Open(fileName)
}
func CloseFile(file *os.File) {
fmt.Println("Closing file")
file.Close()
}

Этой цели можно достичь
очень просто: переместите вызов C l o s e F i l e после вызова
OpenFile (и сопутствующего кода
обработки ошибок) и поставьте
перед ним ключевое слово defer.

func GetFloats(fileName string) ([]float64, error) {
var numbers []float64
file, err := OpenFile(fileName)
if err != nil {
Перемещается после вызова
return nil, err
OpenFile (и сопутствующего
}
кода обработки ошибок).
defer CloseFile(file)
для
Ключевое слово «defer» нужно
scanner := bufio.NewScanner(file)
того, чтобы функция не выполняfor scanner.Scan() {
лась до выхода из GetFloats.
number, err := strconv.ParseFloat(scanner.Text(), 64)
Даже если здесь будет получена
Ключевое слово defer гарантирует, if err != nil {
ошибка, функция CloseFile все
что функция CloseFile будет вы- return nil, err
равно будет вызвана!
}
звана при выходе из GetFloats —

numbers
=
append(numbers,
number)
как в случае нормального завер}
шения, так и при возникновении
Функция CloseFile будет выif scanner.Err() != nil {
звана и в том случае, если
ошибок в ходе разбора файла.
return nil, scanner.Err()
здесь была возвращена ошибка!
}
Даже если программа sum.go полуИ
конечно,
функция CloseFile
return numbers, nil
чит файл с некорректными данвызывается и в том случае, если
}

ными, она все равно закроет файл
перед завершением!

GetFloats завершится нормально!

Shell Edit View Window Help

Выполняется
отложенный вызов
CloseFile!

20.25
5.0
10.5
15.0

Shell Edit View Window Help

data.txt
Файл содержит ошибку.

20.25
hello
10.5

$ go run sum.go data.txt
Opening data.txt
Closing file
Sum: 50.75

Выполняется
отложенный
вызов CloseFile!

bad-data.txt

$ go run sum.go bad-data.txt
Opening data.txt
Closing file
2018/04/09 21:30:42 strconv.ParseFloat:
parsing "hello": invalid syntax
exit status 1
дальше 4

389

как работает defer

Развлечения с магнитами

Эта программа определяет тип Refrigerator , который моделирует холодильник.
Refrigerator использует в качестве базового типа сегмент строк с названиями продуктов,
лежащих в холодильнике. Тип содержит метод Open, который моделирует открытие двери,
и парный метод Close для ее закрытия (никому не захочется понапрасну расходовать энергию).
Метод FindFood вызывает Open для открытия двери, вызывает функцию find, написанную для
поиска конкретного продукта в базовом сегменте, а затем вызывает Close для закрытия двери.
Но с методом FindFood возникает проблема. Он возвращает значение ошибки, если искомый
продукт не найден. Но когда это происходит, метод возвращает управление до вызова Close,
и дверь виртуального холодильника остается открытой!
func find(item string, slice []string) bool {
(продолжение на следующей странице...)
for _, sliceItem := range slice {

if item == sliceItem {
Возвращает true, если строка
return true
найдена в сегменте...
}
}

}
return false

...или false, если строка не найдена.

type Refrigerator []string

м
сегменте строк, в которо
Тип Refrigerator основан на
.
ика
льн
в из холоди
хранятся названия продукто

func main() {
fridge := Refrigerator{"Milk", "Pizza", "Salsa"}
for _, food := range []string{"Milk", "Bananas"} {

err := fridge.FindFood(food)
Opening refrigerator

if err != nil {
Found Milk
log.Fatal(err)
Closing refrigerator
}
Opening refrigerator
}
Дверь холодильника открыва2018/04/09 22:12:37 Bananas not found
}
ется, но не будет закрыта!

390

глава 12

Ответ на с. 411.

func (r Refrigerator) Open() {
Моделирует открытие двери холодильника.
fmt.Println("Opening refrigerator")
}
func (r Refrigerator) Close() {
Моделирует закрытие двери холодильника.
fmt.Println("Closing refrigerator")
}
func (r Refrigerator) FindFood(food string) error {
r.Open()
Если Refrigerator содержит нужный продукт...
if find(food, r) {
fmt.Println("Found", food)
...выводится сообщение о том, что продукт найден.
} else {

return fmt.Errorf("%s not found", food)
В противном случае возвра}
щается сообщение об ошибке.
Но если будет возвращена ошибка,
r.Close()
этот метод не будет вызван!
return nil
}

снова на ногах

Развлечения с магнитами (продолжение)

Используйте магниты для создания обновленной версии метода FindFood. Она должна
откладывать вызов метода Close, чтобы он выполнялся при выходе из FindFood (независимо от того, был найден продукт или нет).
Метод Close типа Refrigerator должен вызываться в том случае, если продукт найден.
Метод Close также должен вызываться
и в том случае, если продукт не найден.

defer

Opening refrigerator
Found Milk
Closing refrigerator
Opening refrigerator
Closing refrigerator
2018/04/09 22:12:37 Bananas not found

if find(food, r) {
fmt.Println("Found", food)
} else {
return fmt.Errorf("%s not found", food)
}

func (r Refrigerator) FindFood(food string) error {

}

r.Open()
r.Close()

return nil

часто

В:

Задаваемые
вопросы

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

О:

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

дальше 4

391

чтение каталогов

Получение списка файлов в каталоге
В Go есть и другие средства обработки ошибок. Вскоре мы покажем вам программу,
в которой они продемонстрированы. Но в этой программе также используется пара
новых приемов, которые нужно объяснить перед рассмотрением программы. Для
начала стоит рассказать о том, как прочитать список содержимого каталога.
Создайте каталог с именем my_directory, в котором находятся два файла и подкаталог,
как показано справа. Программа, приведенная внизу, выводит содержимое my_directory
с указанием имени каждого найденного элемента и признака того, является ли он
файлом или подкаталогом.
Пакет io/ioutil включает функцию ReadDir, предназначенную для чтения содержимого каталога. Функция ReadDir получает имя каталога и возвращает сегмент значений, по одному для каждого файла или подкаталога в каталоге (а также возможные
значения ошибок). Каждое значение в сегменте поддерживает интерфейс FileInfo,
который включает метод Name, возвращающий имя файла, и метод IsDir, возвращающий значение true для каталогов.
Наша программа вызывает функцию ReadDir, передавая имя my_directory в аргументе.
Затем она перебирает значения в полученном сегменте. Если IsDir возвращает для
значения true, то программа выводит "Directory:" и имя файла, а если false —
"File:" и имя файла.

Тест-драйв

my_directory
a.txt
subdir
z.txt

package main

files.go

import (
"fmt"
"io/ioutil"
"log"
)

тами,
Получает сегмент с элемен
е
имо
ерж
сод
представляющими
«my_directory».

func main() {
files, err := ioutil.ReadDir("my_directory")
if err != nil {
log.Fatal(err)
е...
Для каждого файла в сегмент
}
Если файл является for _, file := range files {
каталогом...
if file.IsDir() {

fmt.Println("Directory:",
file.Name())
…то выводится
«Directory»
и имя файла. } else {

fmt.Println("File:",
file.Name())
А если нет — выводится
«File:»
и имя файла.
}
}

}

Сохраните приведенный выше код в файле files.go в одном каталоге с my_directory.
В терминале перейдите в этот каталог и введите команду go run files.go.
Программа выполняется и выводит список файлов и каталогов, находящихся
в my_directory.
392

глава 12

Shell Edit View Window Help

$ cd work
$ go run files.go
File: a.txt
Directory: subdir
File: z.txt

снова на ногах

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

go
src
geo

Обычно такая программа получается достаточно сложной. Общая
схема выглядит примерно так:

coordinates.go

I. Получить список файлов в каталоге.

landmark.go

A. Получить следующий файл.
B. Файл является каталогом?
1. Да: получить список файлов в каталоге.
a. Получить следующий файл.
b. Файл является каталогом?

Тест-драйв

locked

И кто знает,
сколько еще
будет уровней!

secret.go
vehicle

01. Да: получить список файлов в каталоге...

car.go

2. Нет: просто вывести имя файла.
Голова идет кругом, верно? Писать такой код никому не захочется!
Нет ли более простого решения? Логика примерно такая:
I. Получить список файлов в каталоге.
A. Получить следующий файл.

Получить список файлов
в каталоге

B. Файл является каталогом?
1. Да: повторить с шага I для этого каталога.

Получить следующий файл

2. Нет: просто вывести имя файла.
Но пока неясно, как реализовать этап
«повторить с шага I для этого каталога».
Для этого нам понадобится новая концепция программирования...

Файл является
каталогом?
Нет

Да

Как повторить
процесс для
подкаталога?

Вывести имя файла

дальше 4

393

рекурсивные функции

Рекурсивные вызовы функций

Тест-драйв

И тут мы подходим ко второму (и последнему) приему, который мы должны показать
вам перед тем, как вернуться к основной теме обработки ошибок.
Go — один из многих языков программирования с поддержкой механизма рекурсии,
который позволяет функции вызывать саму себя.
Но если действовать неосторожно, то возникнет бесконечный цикл, в котором
функция будет вызывать себя снова и снова:
package main
import "fmt"
func recurses() {
fmt.Println("Oh, no, I'm stuck!")
recurses()
Функция recurses вызывает сама себя!
}
Функция recurses
вызывается
в первый раз.

func main() {
recurses()
}

Oh,
Oh,
Oh,
Oh,

no,
no,
no,
no,

I'm stuck!
I'm stuck!
I'm stuck!
^Csignal: interrupt

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

Тот, кто запускает программу, должен нажать
Ctrl+C, чтобы прервать
бесконечный цикл!

package main
import "fmt"
func count(start int, end int) {
fmt.Println(start)
Вывести текущее начальное число.
if start < end {
Если конечное число еще не достигнуто...
count(start+1, end)
…то функция count вызывает
}
сама себя с начальным числом
}
на 1 больше текущего.
func main() {
Вызываем count в первый раз
count(1, 3)
}
и указываем, что отсчет
1
должен вестись от 1 до 3.

2
3

394

глава 12

снова на ногах

Рекурсивные вызовы функций (продолжение)

Тест-драйв

Последовательность действий в программе:
1.

main вызывает count с параметром start, равным 1, и параметром end, равным 3.

2.

count выводит параметр start: 1.

3.

start (1) меньше end (3), поэтому функция count вызывает саму себя с параметром start,
равным 2, и параметром end, равным 3.

4.

Второй вызов count выводит свой новый параметр start: 2.

5.

start (2) меньше end (3), поэтому функция count вызывает саму себя с параметром start,
равным 3, и параметром end, равным 3.

6.

Третий вызов count выводит свой новый параметр start: 3.

7.

start (3) не меньше end (3), поэтому count не вызывает себя снова, а просто возвращает
управление.

8.

Предыдущие два вызова count также возвращают управление, и программа завершается.

Если добавить вызовы Printf, обозначающие каждый вызов count и каждый
выход из функции, эта последовательность станет чуть более очевидной:
package main
import "fmt"
func count(start int, end int) {
fmt.Printf("count(%d, %d) called\n", start, end)
fmt.Println(start)
if start < end {
count(start+1, end)
}
fmt.Printf("Returning from count(%d, %d) call\n", start, end)
}
func main() {
count(1, 3)
}

count(1, 3) called
1
count(2, 3) called
2
count(3, 3) called
3
Returning from count(3, 3) call
Returning from count(2, 3) call
Returning from count(1, 3) call

Так работает простая рекурсивная функция. Попробуем применить рекурсию в программе files.go и посмотрим, как она
поможет с выводом содержимого подкаталогов.
дальше 4

395

использование рекурсивных функций

Рекурсивный вывод содержимого каталога

go

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

Тест-драйв

src
geo

I. Получить список файлв в каталоге.

coordinates.go

A. Получить следующий файл.
landmark.go

B. Файл является каталогом?
1. Да: начать с шага I для этого каталога.

locked

2. Нет: просто вывести имя файла.
Из функции main был исключен код чтения содержимого каталога; main теперь
просто вызывает рекурсивную функцию
scanDirectory. Функция scanDirectory
получает путь к сканируемому каталогу, поэтому ей передается путь к подкаталогу "go".

scanDirectory прежде всего выводит текущий путь, чтобы мы знали, какой каталог
обрабатывается в настоящий момент. Затем
она вызывает ioutil.ReadDir для этого
пути, чтобы получить содержимое каталога.
Функция перебирает сегмент значений
FileInfo, возвращенных ReadDir, последовательно обрабатывая каждое значение.
Она вызывает функцию filepath.Join
для объединения текущего пути и текущего
имени файла со слешами / (таким образом,
"go" и "src" объединяются в "go/src").

Если текущий файл не является каталогом,
scanDirectory просто выводит полный
путь и переходит к следующему файлу (если
в текущем каталоге еще остались необработанные элементы).

Но если текущий файл является каталогом, рекурсия вступает в силу: функция
scanDirectory вызывает сама себя с путем подкаталога. Если этот подкаталог содержит свои подкаталоги, scanDirectory
вызовет себя для каждого из этих подкаталогов и так далее по всему дереву файла.
396

глава 12

secret.go

package main
import (
"fmt"
"io/ioutil"
"log"
"path/filepath"
)
поРекурсивная функция, которая
лучает путь для обработки.

vehicle
car.go
Возвращает все обнаруженные ошибки.

func scanDirectory(path string) error {
fmt.Println(path)
Выводит текущий каталог.
files, err := ioutil.ReadDir(path)
if err != nil {
Получает сегмент
return err
с содержимым каталога.
}
Соединяет
путь каталога и имя файла
через символ /.

for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
Если это подкаталог...
err := scanDirectory(filePath)
if err != nil {
…рекурсивно вызывает
scanDirectory, но на этот
return err
раз с путем подкаталога.
}
} else {
fmt.Println(filePath)
}
Если это обычный файл, про}
сто вывести его имя с путем.
return nil
}
Запускаем процесс вызовом
scanDirectory для каталога

func main() {
верхнего уровня.
err := scanDirectory("go")
if err != nil {
log.Fatal(err)
}
}

снова на ногах

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

$ cd /Users/jay
$ go run files.go
go
go/src
go/src/geo
go/src/geo/coordinates.go
go/src/geo/landmark.go
go/src/locked
go/src/locked/secret.go
go/src/vehicle
go/src/vehicle/car.go

И когда вы увидите, как работает функция scanDirectory, вы оцените всю элегантность рекурсии. Процесс выглядит примерно так:
1.

main вызывает scanDirectory с путем "go".

2.

scanDirectory выводит переданный путь "go" — текущий
каталог, с которым работает функция.

3.

Функция вызывает ioutil.ReadDir для пути "go".

4.
5.
6.
7.
8.
9.

Тест-драйв

Shell Edit View Window Help

Возвращенный сегмент содержит всего один элемент: "src".

Вызов filepath.Join для текущего каталога "go" и имени файла "src" дает новый путь "go/src".
src является подкаталогом, поэтому scanDirectory вызывается снова — на этот раз для пути "go/
src".
Рекурсия!
scanDirectory выводит новый путь: "go/src".

Функция вызывает ioutil.ReadDir для пути "go/src".

Первым элементом возвращенного сегмента является "geo".

10. Вызов filepath.Join для текущего каталога "go/src" и имени файла "geo" дает новый путь
"go/src/geo".

11. geo является подкаталогом, поэтому scanDirectory вызывается снова — на этот раз для пути "go/
src/geo".
Рекурсия!
12. scanDirectory выводит новый путь: "go/src/geo".

13. Функция вызывает ioutil.ReadDir для пути "go/src/geo".

14. Первым элементом возвращенного сегмента является "coordinates.go".
15. coordinates.go не является каталогом, поэтому функция просто выводит имя файла.
16. И так далее.
Рекурсивные функции в основном сложнее
обычных нерекурсивных функций и часто
требуют больше вычислительных ресурсов.
Но иногда рекурсивные функции справляются
с задачами, которые было бы очень трудно
решить другими способами.
Итак, программы files.go готовы, и наше отступление можно считать завершенным. А теперь
вернемся к обсуждению средств обработки
ошибок в Go.

go

scanDirectory("go")
src

scanDirectory("go/src")
geo

scanDirectory("go/src/geo")
coordinates.go

Тест-драйв

(И так далее...)

landmark.go

дальше 4

397

использование паники

Обработка ошибок в рекурсивной функции
Если функция scanDirectory обнаруживает ошибку во время
сканирования любого подкаталога (например, если у пользователя отсутствуют разрешения для обращения к каталогу), она
возвращает ошибку. И это ожидаемое поведение; программа
не контролирует файловую систему, и очень важно сообщать
об ошибках, когда они неизбежно возникают в программе.
Но если добавить пару команд Printf для вывода возвращаемых ошибок, мы видим, что способ обработки ошибок не
идеален:

Shell Edit View Window Help

$ go run files.go
go
go/src
go/src/geo
go/src/geo/coordinates.go
go/src/geo/landmark.go
go/src/locked
2018/04/09 19:09:21 open
go/src/locked: permission denied
exit status 1

func scanDirectory(path string) error {
fmt.Println(path)
files, err := ioutil.ReadDir(path)
if err != nil {
fmt.Printf("Returning error from scanDirectory(\"%s\") call\n", path)
return err
Вывод отладочной информации
}
для ошибок в вызове ReadDir.
for _, file := range files {
filePath := filepath.Join(path, file.Name())
if file.IsDir() {
err := scanDirectory(filePath)
if err != nil {
fmt.Printf("Returning error from scanDirectory(\"%s\") call\n", path)
return err
Вывод отладочной информации для
}
ошибок
} else {
в рекурсивном вызове scanDirectory.
fmt.Println(filePath)
}
}
return nil
}
func main() {
err := scanDirectory("go")
Shell Edit View Window Help
if err != nil {
$ go run files.go
log.Fatal(err)
go
}
go/src
}

Если ошибка происходит в одном из рекурсивных вызовов
scanDirectory, эта ошибка должна быть возвращена
вверх по всей цепочке, пока
не достигнет функции main!
398

глава 12

go/src/geo
go/src/geo/coordinates.go
go/src/geo/landmark.go
go/src/locked
Returning error from scanDirectory("go/src/locked") call
Returning error from scanDirectory("go/src") call
Returning error from scanDirectory("go") call
2018/06/11 11:01:28 open go/src/locked: permission denied
exit status 1

снова на ногах

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

Также паника встречалась
нам при ошибках утверждений типов (если не использовать необязательное
логическое значение ok):

"la", "ti"}
notes := [7]string{"do", "re", "mi", "fa", "so",

ачение, коНаибольшее зн стигнуто
до
торое будет
», равно 7!
переменной «i

for i := 0; i