Эффективный Go
Введение¶
Go — это новый язык программирования. Хотя он заимствует идеи из существующих языков, у него есть необычные свойства, которые делают эффективные программы на Go отличными по характеру от программ, написанных на его родственниках. Прямой перевод программы на C++ или Java на Go вряд ли даст удовлетворительный результат — программы на Java написаны на Java, а не на Go. С другой стороны, рассмотрение задачи с точки зрения Go может привести к успешной, но совершенно другой программе. Другими словами, чтобы хорошо писать на Go, важно понимать его свойства и идиомы. Также важно знать установленные соглашения при программировании на Go, такие как именование, форматирование, структура программы и т.д., чтобы ваши программы было легко понять другим программистам Go.
Этот документ дает советы по написанию ясного, идиоматичного кода на Go. Он дополняет спецификацию языка, Тур по Go, и Как писать код на Go, которые следует прочитать в первую очередь.
Примечание, добавлено в январе 2022 года: Этот документ был написан для выпуска Go в 2009 году и с тех пор не обновлялся значительно. Хотя он является хорошим руководством для понимания самого языка, благодаря стабильности языка, он мало говорит о библиотеках и ничего о значительных изменениях в экосистеме Go с момента написания, таких как система сборки, тестирование, модули и полиморфизм. Планов по его обновлению нет, поскольку произошло так много изменений, и существует большое и растущее количество документов, блогов и книг, хорошо описывающих современное использование Go. «Эффективный Go» по-прежнему полезно, но читатель должен понимать, что это далеко не полное руководство. См. issue 28782 для контекста.
Примеры¶
Исходники пакетов Go предназначены не только для служения основной библиотеке, но и как примеры того, как использовать язык. Более того, многие пакеты содержат рабочие, автономные исполняемые примеры, которые вы можете запускать напрямую с веб-сайта go.dev, например, этот (при необходимости кликните на слово "Example", чтобы открыть пример). Если у вас есть вопрос о том, как подойти к задаче или как что-то может быть реализовано, документация, код и примеры в библиотеке могут дать ответы, идеи и контекст.
Форматирование¶
Вопросы форматирования — самые спорные, но наименее критичные. Люди могут адаптироваться к различным стилям форматирования, но лучше, если этого не требуется, и меньше времени тратится на эту тему, если все придерживаются одного стиля. Проблема в том, как достичь этой утопии без длинного предписывающего руководства по стилю.
В Go мы применяем необычный подход и позволяем машине
заботиться о большинстве вопросов форматирования.
Программа gofmt
(также доступная как go fmt, которая
работает на уровне пакета, а не отдельного файла исходного кода)
читает программу на Go
и выводит исходный код в стандартном стиле с отступами
и вертикальным выравниванием, сохраняя и при необходимости
перестраивая комментарии.
Если вы хотите узнать, как обработать новую ситуацию с макетом,
запустите gofmt; если результат кажется неправильным,
измените программу (или сообщите о баге в gofmt),
не пытайтесь обходить проблему самостоятельно.
Например, нет необходимости тратить время на выравнивание
комментариев в полях структуры.
gofmt сделает это за вас. Для объявления
type T struct {
name string // имя объекта
value int // его значение
}
gofmt выровняет колонки:
type T struct {
name string // имя объекта
value int // его значение
}
Весь код Go в стандартных пакетах был отформатирован с помощью gofmt.
Некоторые детали форматирования остаются. Кратко:
- Отступы
- Используем табуляцию для отступов, и
gofmtвыводит их по умолчанию. Используйте пробелы только если необходимо. - Длина строки
- В Go нет ограничения на длину строки. Не беспокойтесь о переполнении перфокарты. Если строка кажется слишком длинной, перенесите её на следующую и добавьте дополнительный табулятор.
- Скобки
-
Go требует меньше скобок, чем C и Java: управляющие конструкции (
if,for,switch) не имеют скобок в синтаксисе. Кроме того, иерархия приоритета операторов короче и яснее, поэтомуx<<8 + y<<16
означает именно то, что подразумевает пробел, в отличие от других языков.
Комментарии¶
В Go доступны комментарии в стиле C /* */
и в стиле C++ // для строк.
Комментарии в строке являются нормой;
блочные комментарии в основном используются для описания пакета, но
могут быть полезны внутри выражения или для временного отключения больших фрагментов кода.
Комментарии, расположенные перед объявлениями верхнего уровня без промежуточных пустых строк, считаются документирующими само объявление. Эти «документирующие комментарии» являются основным источником документации для данного пакета или команды Go. Подробнее о документирующих комментариях см. «Документирующие комментарии Go».
Имена¶
Имена в Go так же важны, как и в любом другом языке. Они даже имеют семантический эффект: видимость имени за пределами пакета определяется тем, является ли его первый символ заглавным. Поэтому стоит уделить немного времени обсуждению соглашений по именованию в Go-программах.
Имена пакетов¶
Когда пакет импортируется, его имя становится способом доступа к содержимому. Например:
import "bytes"
импортирующий пакет может использовать bytes.Buffer.
Полезно, чтобы все использующие пакет могли обращаться к его содержимому одним и тем же именем,
поэтому имя пакета должно быть хорошим: коротким, лаконичным, ёмким. По соглашению,
имена пакетов пишутся в нижнем регистре, одним словом; не должно быть необходимости в подчеркиваниях или mixedCaps.
Старайтесь к краткости, так как каждый, кто использует ваш пакет, будет набирать это имя.
Не беспокойтесь о возможных конфликтах заранее: имя пакета — это только имя по умолчанию для импорта;
оно не обязательно уникально во всём исходном коде. В редких случаях при конфликте импортирующий пакет
может выбрать другое локальное имя. В любом случае путаница редка, так как имя файла в импорте
определяет, какой пакет используется.
Ещё одно соглашение: имя пакета — это базовое имя его исходного каталога;
пакет в src/encoding/base64 импортируется как "encoding/base64"
и называется base64, а не encoding_base64 или encodingBase64.
Импортер пакета будет использовать имя для обращения к его содержимому,
поэтому экспортируемые имена могут использовать это для избегания повторов.
(Не используйте import ., который упрощает тесты вне пакета, но в остальных случаях лучше избегать.)
Например, тип буферизованного чтения в пакете bufio называется Reader,
а не BufReader, потому что пользователи видят его как bufio.Reader,
что ясно и лаконично. Более того, так как импортированные сущности всегда адресуются через имя пакета,
bufio.Reader не конфликтует с io.Reader. Аналогично, функция для создания
новых экземпляров ring.Ring обычно называется NewRing, но так как
Ring — единственный экспортируемый тип пакета ring, она просто называется New,
видимая клиентами как ring.New. Используйте структуру пакета для выбора хороших имён.
Ещё один короткий пример — once.Do; once.Do(setup) читается хорошо и не улучшится
при написании once.DoOrWaitUntilDone(setup). Длинные имена не делают код автоматически более читаемым.
Полезный комментарий часто ценнее, чем чрезмерно длинное имя.
Геттеры¶
Go не предоставляет автоматическую поддержку геттеров и сеттеров.
Ничего плохого нет в том, чтобы предоставлять их самому, и часто это уместно,
но не идиоматично и не обязательно включать Get в имя геттера.
Если у вас есть поле owner (с маленькой буквы, неэкспортируемое), геттер должен называться
Owner (с большой буквы, экспортируемое), а не GetOwner.
Использование заглавных букв для экспорта позволяет различать поле и метод.
Сеттер, при необходимости, вероятно, будет называться SetOwner.
Оба имени хорошо читаются на практике:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Имена интерфейсов¶
По соглашению, интерфейсы с одним методом именуются по имени метода с суффиксом -er или подобным образом,
образуя существительное-агент: Reader, Writer, Formatter,
CloseNotifier и т.д.
Существует множество таких имён, и полезно уважать их и соответствующие имена функций.
Read, Write, Close, Flush, String и т.д.
имеют канонические сигнатуры и значения. Чтобы избежать путаницы, не давайте вашему методу одно из этих имён,
если оно не имеет той же сигнатуры и смысла. Наоборот, если ваш тип реализует метод с таким же значением,
как метод известного типа, используйте то же имя и сигнатуру; назовите метод преобразования в строку String, а не ToString.
Смешанный регистр (MixedCaps)¶
Наконец, в Go принято использовать MixedCaps или mixedCaps, а не подчеркивания для написания
многословных имён.
Точки с запятой¶
Как и в C, формальная грамматика Go использует точки с запятой для завершения операторов, но в отличие от C, эти точки с запятой не появляются в исходном коде. Вместо этого лексер применяет простое правило для автоматической вставки точек с запятой при разборе, поэтому в тексте их почти нет.
Правило таково. Если последний токен перед новой строкой — это идентификатор
(включая слова вроде int и float64),
базовая литеральная константа, такая как число или строка, или один из токенов:
break continue fallthrough return ++ -- ) }
лексер всегда вставляет точку с запятой после токена. Это можно резюмировать так: «если новая строка идет после токена, который может завершать оператор, вставьте точку с запятой».
Точку с запятой также можно опустить сразу перед закрывающей скобкой, поэтому оператор, например:
go func() { for { dst <- <-src } }()
не требует точек с запятой.
Идиоматические Go-программы используют точки с запятой только в местах вроде
клаузы for, чтобы отделять инициализацию, условие и шаг цикла.
Они также нужны для разделения нескольких операторов на одной строке, если вы пишете код таким образом.
Одно следствие правил вставки точек с запятой заключается в том, что нельзя ставить
открывающую скобку структуры управления (if, for, switch или select)
на следующей строке. Если это сделать, перед скобкой будет вставлена точка с запятой,
что может вызвать нежелательные эффекты. Пишите так:
if i < f() {
g()
}
а не так:
if i < f() // неправильно!
{ // неправильно!
g()
}
Структуры управления¶
Структуры управления в Go похожи на C, но имеют важные отличия.
Циклов do или while нет, есть только
слегка обобщённый for;
switch более гибкий;
if и switch могут включать необязательную
инструкцию инициализации, как в for;
операторы break и continue могут принимать
необязательную метку для указания, что именно прерывать или продолжать;
есть новые структуры управления, включая type switch и многопутевой
мультиплексор коммуникаций select.
Синтаксис также немного отличается:
скобки не используются,
а тело всегда ограничено фигурными скобками.
If¶
В Go простой if выглядит так:
if x > 0 {
return y
}
Обязательные фигурные скобки стимулируют писать простые if операторы
на нескольких строках. Это хороший стиль, особенно когда тело содержит
управляющую инструкцию, такую как return или break.
Поскольку if и switch могут включать инструкцию инициализации,
часто используют её для объявления локальной переменной.
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
В стандартных библиотеках Go часто можно увидеть, что
если тело if не продолжается в следующий оператор — то есть
заканчивается break, continue,
goto или return — лишний else опускается.
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
Это пример типичной ситуации, когда код должен обрабатывать последовательность
ошибок. Код читается легко, если успешный поток управления идёт вниз по странице,
исключая ошибки по мере их появления. Поскольку ошибки обычно завершаются
return, else не требуется.
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
Повторное объявление и переназначение¶
Важно: последний пример из предыдущего раздела демонстрирует деталь работы
короткой формы объявления :=.
Объявление, которое вызывает os.Open, выглядит так:
f, err := os.Open(name)
Эта инструкция объявляет две переменные, f и err.
Через несколько строк вызов f.Stat выглядит так:
d, err := f.Stat()
На первый взгляд, это объявляет d и err.
Обратите внимание, что err встречается в обоих выражениях.
Это допустимо: err объявлена в первом выражении, а во втором
только переназначается.
Это означает, что вызов f.Stat использует существующую
переменную err, объявленную выше, и просто присваивает ей новое значение.
В объявлении с := переменная v может появляться даже
если она уже объявлена, при условии:
- объявление находится в той же области видимости, что и существующее объявление
v(еслиvуже объявлена во внешней области, будет создана новая переменная §), - соответствующее значение инициализации можно присвоить
v, и - существует как минимум одна другая переменная, которая создаётся этим объявлением.
Это необычное свойство продиктовано прагматизмом,
позволяя, например, удобно использовать одну переменную err
в длинной цепочке if-else.
Вы будете часто видеть такое использование.
§ Следует отметить, что в Go область видимости параметров функции и возвращаемых значений совпадает с телом функции, даже если они лексически расположены за пределами фигурных скобок, которые ограничивают тело.
For¶
Цикл for в Go похож на цикл в C, но не идентичен.
Он объединяет возможности for и while, при этом do-while отсутствует.
Существует три формы, только одна из которых использует точки с запятой.
// Как в C: for
for init; condition; post { }
// Как в C: while
for condition { }
// Как в C: for(;;)
for { }
Короткие объявления позволяют легко объявлять индексную переменную прямо в цикле.
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
Если вы проходите по массиву, срезу, строке или карте, или читаете из канала, можно использовать конструкцию range.
for key, value := range oldMap {
newMap[key] = value
}
Если нужен только первый элемент диапазона (ключ или индекс), можно опустить второй:
for key := range m {
if key.expired() {
delete(m, key)
}
}
Если нужен только второй элемент диапазона (значение), используйте пустой идентификатор – подчеркивание – чтобы отбросить первый:
sum := 0
for _, value := range array {
sum += value
}
Пустой идентификатор имеет много применений, подробнее о них в следующем разделе.
Для строк range выполняет дополнительную работу: разбивает строку на отдельные Unicode-коды, анализируя UTF-8.
Ошибочные кодировки занимают один байт и заменяются символом U+FFFD.
(Термин rune обозначает один Unicode-код в Go. Подробнее см. спецификацию языка.)
Пример цикла:
for pos, char := range "日本\x80語" { // \x80 – недопустимая UTF-8 кодировка
fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}
Вывод:
character U+65E5 '日' starts at byte position 0 character U+672C '本' starts at byte position 3 character U+FFFD '�' starts at byte position 6 character U+8A9E '語' starts at byte position 7
Наконец, в Go отсутствует оператор запятая, а ++ и -- – это отдельные инструкции, а не выражения.
Поэтому, если нужно работать с несколькими переменными в for, используйте параллельное присваивание (хотя это исключает ++ и --).
// Разворот массива a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
Switch¶
В Go switch более универсален, чем в C.
Выражения не обязательно должны быть константами или целыми числами,
кейсы проверяются сверху вниз до нахождения совпадения,
а если switch не имеет выражения, он проверяет true.
Таким образом, можно и идиоматично писать цепочки if-else if-else с помощью switch.
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
Автоматического «проваливания» (fall through) нет, но кейсы можно перечислять через запятую.
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
Хотя в Go break встречается реже, чем в других языках семейства C, его можно использовать для преждевременного выхода из switch.
Иногда необходимо выйти из окружающего цикла, а не из switch. Для этого можно использовать метку на цикле и делать break с этой меткой.
Пример демонстрирует оба варианта.
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
Конечно, continue также принимает необязательную метку, но применяется только к циклам.
Для завершения раздела приведён пример функции для сравнения срезов байт, использующий два switch:
// Compare возвращает целое число, сравнивая два среза байт лексикографически.
// Результат: 0 если a == b, -1 если a < b, +1 если a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
Type switch¶
switch также может использоваться для определения динамического типа переменной интерфейса. Такой type switch использует синтаксис утверждения типа с ключевым словом type в скобках.
Если switch объявляет переменную в выражении, эта переменная будет иметь соответствующий тип в каждом кейсе.
Идиоматично повторно использовать имя переменной в таких случаях, фактически объявляя новую переменную с тем же именем, но другим типом в каждом кейсе.
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T\n", t) // %T выводит фактический тип t
case bool:
fmt.Printf("boolean %t\n", t) // t имеет тип bool
case int:
fmt.Printf("integer %d\n", t) // t имеет тип int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t имеет тип *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t имеет тип *int
}
Функции¶
Несколько возвращаемых значений¶
Одна из необычных особенностей Go — функции и методы могут возвращать несколько значений. Этот подход позволяет улучшить несколько неудобных идиом из C: возвращение ошибок через код (например, -1 для EOF) и изменение аргумента по указателю.
В C ошибка записи сигнализируется отрицательным числом, а код ошибки хранится в скрытом месте. В Go Write может возвращать количество записанных байт и ошибку: «Да, вы записали часть байт, но не все, потому что устройство заполнилось». Подпись метода Write в пакете os:
func (file *File) Write(b []byte) (n int, err error)
Как указано в документации, метод возвращает количество записанных байт и ненулевую ошибку, если n != len(b). Это распространённый стиль; см. раздел об обработке ошибок для других примеров.
Аналогичный подход исключает необходимость передавать указатель для имитации параметра по ссылке. Пример простой функции для получения числа из позиции в срезе байт, возвращающей число и следующую позицию:
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
Использовать её для обхода чисел во входном срезе b можно так:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
Именованные возвращаемые параметры¶
Возвращаемые значения функции Go могут иметь имена и использоваться как обычные переменные, точно так же, как входные параметры.
При именовании они инициализируются нулевыми значениями своего типа при начале выполнения функции; если функция выполняет return без аргументов, используются текущие значения именованных результатов.
Имена не обязательны, но могут сделать код короче и понятнее: они служат своего рода документацией.
Если назвать результаты функции nextInt, сразу становится ясно, какой int за что отвечает.
func nextInt(b []byte, pos int) (value, nextPos int) {
Так как именованные результаты инициализируются и связаны с обычным return, это упрощает код и делает его более читаемым. Вот пример функции io.ReadFull, использующей именованные результаты:
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
Defer¶
Оператор defer в Go откладывает выполнение вызова функции (так называемой отложенной функции) до момента, когда функция, содержащая defer, завершит выполнение. Это необычный, но эффективный способ работать с ресурсами, которые должны быть освобождены независимо от пути возврата функции. Классические примеры — разблокировка мьютекса или закрытие файла.
// Contents возвращает содержимое файла в виде строки.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close выполнится, когда функция завершится.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append будет обсужден позже.
if err != nil {
if err == io.EOF {
break
}
return "", err // f будет закрыт при возврате.
}
}
return string(result), nil // f будет закрыт при возврате.
}
Отложенный вызов функции, например Close, имеет два преимущества. Во-первых, это гарантирует, что вы никогда не забудете закрыть файл — ошибка, которую легко допустить при добавлении новых путей возврата. Во-вторых, закрытие находится рядом с открытием, что гораздо нагляднее, чем размещение его в конце функции.
Аргументы для отложенной функции (включая получателя, если функция является методом) вычисляются в момент выполнения defer, а не при самом вызове. Это позволяет использовать один оператор defer для нескольких вызовов. Пример:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Отложенные функции выполняются в порядке LIFO, поэтому будет выведено 4 3 2 1 0 при завершении функции. Более полезный пример — простое отслеживание выполнения функций:
func trace(s string) { fmt.Println("entering:", s) }
func untrace(s string) { fmt.Println("leaving:", s) }
// Использование:
func a() {
trace("a")
defer untrace("a")
// выполняем что-то...
}
Можно улучшить этот подход, используя факт, что аргументы отложенных функций вычисляются при выполнении defer:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
Вывод:
entering: b in b entering: a in a leaving: a leaving: b
Для программистов, привыкших к управлению ресурсами на уровне блоков, defer может показаться странным, но его сила и интересные применения как раз в том, что он основан на функции, а не на блоке. В разделе о panic и recover будет показан еще один пример.
Данные¶
Выделение памяти с помощью new¶
В Go есть два примитива для выделения памяти — встроенные функции
new и make.
Они делают разные вещи и применяются к разным типам, что может запутать,
но правила просты.
Начнем с new.
Это встроенная функция, которая выделяет память, но в отличие от одноименных функций в некоторых других языках, она не инициализирует память,
а только обнуляет её.
То есть new(T) выделяет нулевую память для нового элемента типа T и возвращает его адрес, значение типа *T.
В терминологии Go это указатель на вновь выделенное значение типа T, заполненное нулями.
Поскольку память, возвращаемая new, обнулена, полезно проектировать структуры данных так, чтобы
нулевое значение каждого типа было готово к использованию без дополнительной инициализации. Это позволяет пользователю
создавать структуру с помощью new и сразу работать с ней.
Например, документация для bytes.Buffer утверждает, что
"нулевое значение для Buffer — это пустой буфер, готовый к использованию".
Аналогично, sync.Mutex не имеет явного конструктора или метода Init.
Нулевое значение sync.Mutex определяется как разблокированный мьютекс.
Свойство "нулевое значение готово к использованию" распространяется транзитивно. Рассмотрим такое объявление типа:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
Значения типа SyncedBuffer также готовы к использованию сразу после выделения памяти или простой декларации. В следующем примере как p, так и v будут работать корректно без дополнительной подготовки.
p := new(SyncedBuffer) // тип *SyncedBuffer var v SyncedBuffer // тип SyncedBuffer
Конструкторы и составные литералы¶
Иногда нулевого значения недостаточно, и требуется конструктор для инициализации,
как в этом примере из пакета os.
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
Здесь много шаблонного кода. Мы можем упростить его с помощью составного литерала, который представляет собой выражение, создающее новый экземпляр при каждом вычислении.
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
Обратите внимание, что в отличие от C, возвращать адрес локальной переменной совершенно нормально; память, связанная с переменной, сохраняется после возврата функции. На самом деле, взятие адреса составного литерала выделяет новый экземпляр каждый раз при вычислении, так что можно объединить последние две строки.
return &File{fd, name, nil, 0}
Поля составного литерала расположены по порядку и все должны присутствовать.
Однако, если явно обозначить элементы как пары поле:значение,
инициализаторы могут быть в любом порядке, а отсутствующие поля будут иметь свои нулевые значения. Так можно написать:
return &File{fd: fd, name: name}
В крайнем случае, если составной литерал не содержит полей, он создаёт
нулевое значение типа. Выражения new(File) и &File{} эквивалентны.
Составные литералы также могут быть созданы для массивов, срезов и карт,
при этом метками полей выступают индексы или ключи карты соответственно.
В этих примерах инициализация работает независимо от значений Enone,
Eio и Einval, при условии, что они различны.
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
Выделение памяти с помощью make¶
Возвращаемся к выделению памяти.
Встроенная функция make(T, args) служит
другой цели по сравнению с new(T).
Она создаёт только срезы, карты и каналы и возвращает инициализированное
(не обнулённое)
значение типа T (не *T).
Причина различия в том, что эти три типа представляют, под капотом, ссылки на структуры данных, которые
должны быть инициализированы перед использованием.
Например, срез — это трёхэлементный дескриптор,
содержащий указатель на данные (внутри массива), длину и
вместимость, и до инициализации этих элементов срез равен nil.
Для срезов, карт и каналов
make инициализирует внутреннюю структуру данных и готовит
значение к использованию.
Например:
make([]int, 10, 100)
выделяет массив из 100 целых чисел и создаёт структуру среза с длиной 10 и вместимостью 100,
указывающую на первые 10 элементов массива.
(При создании среза вместимость можно опустить; см. раздел о срезах для дополнительной информации.)
В отличие от этого, new([]int) возвращает указатель на только что выделенную, обнулённую структуру среза,
то есть указатель на значение среза nil.
Эти примеры иллюстрируют разницу между new и make.
var p *[]int = new([]int) // выделяет структуру среза; *p == nil; редко используется var v []int = make([]int, 100) // срез v теперь ссылается на новый массив из 100 целых чисел // Сложно и излишне: var p *[]int = new([]int) *p = make([]int, 100, 100) // Идиоматично: v := make([]int, 100)
Помните, что make применяется только к картам, срезам и каналам
и не возвращает указатель.
Чтобы получить явный указатель, используйте new или возьмите адрес
переменной явно.
Массивы¶
Массивы полезны при планировании детальной организации памяти и иногда помогают избежать выделения памяти, но в основном они служат строительным блоком для срезов, о которых пойдёт речь в следующем разделе. Чтобы подготовить основу для этой темы, несколько слов о массивах.
Существуют существенные различия между работой массивов в Go и C. В Go:
- Массивы являются значениями. Присваивание одного массива другому копирует все элементы.
- В частности, если передать массив в функцию, она получит копию массива, а не указатель на него.
-
Размер массива является частью его типа. Типы
[10]intи[20]intразличны.
Свойство значений может быть полезным, но также может быть затратным; если вы хотите поведение и эффективность, похожие на C, можно передавать указатель на массив.
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Обратите внимание на явный оператор взятия адреса
Но даже такой стиль не является идиоматичным для Go. Используйте вместо этого срезы.
Срезы¶
Срезы оборачивают массивы, предоставляя более общий, мощный и удобный интерфейс для работы с последовательностями данных. За исключением объектов с явным размером, таких как матрицы преобразований, большая часть работы с массивами в Go выполняется с помощью срезов, а не простых массивов.
Срезы хранят ссылки на базовый массив, и если присвоить один
срез другому, оба будут ссылаться на один и тот же массив.
Если функция принимает срез в качестве аргумента, изменения,
внесённые в элементы среза, будут видны вызывающему коду, что
аналогично передаче указателя на базовый массив. Следовательно,
функция Read может принимать аргумент-срез вместо
указателя и счётчика; длина среза задаёт верхнюю границу того,
сколько данных можно прочитать. Вот сигнатура метода
Read типа File из пакета os:
func (f *File) Read(buf []byte) (n int, err error)
Метод возвращает количество прочитанных байт и значение ошибки, если оно есть.
Чтобы прочитать первые 32 байта из большого буфера buf,
"срезаем" буфер (slice используется здесь как глагол).
n, err := f.Read(buf[0:32])
Такое использование срезов удобно и эффективно. На самом деле, даже если на время отбросить соображения эффективности, следующий код также прочитал бы первые 32 байта:
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Чтение одного байта.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
Длину среза можно изменять, пока она остаётся в пределах базового массива; просто присвойте ему срез самого себя.
Вместимость среза, доступная через встроенную функцию cap, показывает максимальную длину, которую срез может принять.
Ниже пример функции добавления данных в срез. Если данные превышают вместимость, срез перераспределяется.
Полученный срез возвращается. Функция использует тот факт, что len и cap допустимы для nil-среза и возвращают 0.
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // перераспределение
// Выделяем в два раза больше, чем нужно, для будущего роста.
newSlice := make([]byte, (l+len(data))*2)
// Функция copy предопределена и работает для любого типа среза.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
Необходимо вернуть срез после выполнения функции, потому что хотя Append
может изменять элементы среза, сам срез (структура времени выполнения, содержащая указатель, длину и вместимость) передаётся по значению.
Идея добавления элементов к срезу настолько полезна, что реализована во встроенной функции append.
Чтобы полностью понять её конструкцию, нужно рассмотреть ещё несколько деталей, к которым мы вернёмся позже.
Двумерные срезы¶
Массивы и срезы в Go одномерные. Чтобы создать эквивалент двумерного массива или среза, необходимо определить массив массивов или срез срезов, например:
type Transform [3][3]float64 // 3x3 массив, на самом деле массив массивов. type LinesOfText [][]byte // Срез срезов байтов.
Так как срезы имеют переменную длину, каждая вложенная
последовательность может иметь разную длину.
Это часто встречается, как в нашем примере с LinesOfText:
каждая строка имеет независимую длину.
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
Иногда необходимо выделить память для двумерного среза, например, при обработке строк сканированных пикселей. Существует два способа это сделать. Один — выделять каждый срез отдельно; другой — выделить один большой массив и распределить отдельные срезы по нему. Какой способ выбрать, зависит от задачи. Если срезы могут увеличиваться или уменьшаться, их лучше выделять отдельно, чтобы избежать перезаписи следующей строки; если нет, может быть более эффективно построить объект одним выделением памяти. Ниже приведены примеры обоих методов. Сначала — строка за строкой:
// Выделяем верхнеуровневый срез.
picture := make([][]uint8, YSize) // По одной строке на единицу y.
// Проходим по строкам, выделяя срез для каждой строки.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
А теперь одним выделением, разделяя на строки:
// Выделяем верхнеуровневый срез, как и раньше.
picture := make([][]uint8, YSize) // По одной строке на единицу y.
// Выделяем один большой срез для всех пикселей.
pixels := make([]uint8, XSize*YSize) // Тип []uint8, хотя picture — [][]uint8.
// Проходим по строкам, срезая каждую строку с начала оставшегося среза pixels.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Карты (Maps)¶
Карты — это удобная и мощная встроенная структура данных, которая сопоставляет значения одного типа (ключ, key) со значениями другого типа (элемент, value). Ключ может быть любого типа, для которого определён оператор равенства, например, целые числа, числа с плавающей точкой и комплексные числа, строки, указатели, интерфейсы (если динамический тип поддерживает сравнение), структуры и массивы. Срезы нельзя использовать в качестве ключей, потому что для них не определено равенство. Как и срезы, карты хранят ссылки на внутреннюю структуру данных. Если вы передадите карту в функцию, которая изменяет её содержимое, изменения будут видны вызывающему коду.
Карты можно создавать с помощью обычного синтаксиса составных литералов с парами ключ:значение, что облегчает инициализацию.
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
Присваивание и получение значений карты выглядит синтаксически так же, как для массивов и срезов, за исключением того, что индекс не обязательно должен быть числом.
offset := timeZone["EST"]
Попытка получить значение по ключу, которого нет в карте, вернёт нулевое значение
типа элементов карты. Например, если карта содержит целые числа, обращение к отсутствующему
ключу вернёт 0.
Множество можно реализовать как карту с типом значения bool.
Присвойте true ключу, чтобы добавить его в множество, затем проверяйте наличие
через индекс.
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // будет false, если person нет в карте
fmt.Println(person, "was at the meeting")
}
Иногда нужно различать отсутствие ключа и нулевое значение. Существует ли запись для "UTC"
или это 0 потому, что ключа нет? Это можно проверить с помощью множественного присваивания.
var seconds int var ok bool seconds, ok = timeZone[tz]
По понятным причинам это называется идиомой «comma ok».
В примере, если tz присутствует, seconds получит соответствующее значение, а ok будет true;
если нет — seconds будет 0, а ok — false.
Вот функция, которая объединяет это с удобной обработкой ошибки:
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
Чтобы проверить наличие ключа в карте, не заботясь о значении, можно использовать
пустой идентификатор (_) вместо обычной переменной для значения.
_, present := timeZone[tz]
Чтобы удалить запись из карты, используйте встроенную функцию delete,
аргументы которой — карта и ключ для удаления.
Это безопасно даже если ключ уже отсутствует.
delete(timeZone, "PDT") // Теперь стандартное время
Вывод (Printing)¶
Форматированный вывод в Go использует стиль, похожий на семейство printf из C,
но более богатый и универсальный. Функции находятся в пакете fmt и имеют
имена с заглавной буквы: fmt.Printf, fmt.Fprintf,
fmt.Sprintf и так далее. Строковые функции (Sprintf и т.п.)
возвращают строку, вместо того чтобы заполнять предоставленный буфер.
Форматная строка не обязательна. Для каждой из функций Printf,
Fprintf и Sprintf существует ещё пара функций без форматной строки,
например Print и Println. Эти функции не принимают
форматную строку, а используют стандартное форматирование для каждого аргумента.
Версии Println вставляют пробел между аргументами и добавляют
перевод строки, тогда как Print добавляет пробелы только если операнды
с обеих сторон не являются строками. В этом примере каждая строка выводит одинаковый результат.
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
Форматированные функции вывода, такие как fmt.Fprint, принимают первым
аргументом любой объект, реализующий интерфейс io.Writer;
знакомые экземпляры — это os.Stdout и os.Stderr.
Здесь Go начинает отличаться от C. Во-первых, числовые форматы вроде %d
не используют флаги для знака или размера; функции вывода определяют эти свойства
по типу аргумента.
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
выводит:
18446744073709551615 ffffffffffffffff; -1 -1
Если нужен просто стандартный вывод, например десятичный для целых чисел,
можно использовать универсальный формат %v (“value”); результат
будет таким же, как у Print и Println. Этот формат
может выводить любые значения, включая массивы, срезы, структуры и карты. Например:
fmt.Printf("%v\n", timeZone) // или просто fmt.Println(timeZone)
Результат:
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
Для карт функции вроде Printf сортируют вывод лексикографически по ключу.
Для структур модифицированный формат %+v выводит имена полей структуры вместе с их значениями,
а альтернативный формат %#v выводит значение в полном синтаксисе Go.
type T struct {
a int
b float64
c string
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
выводит:
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
(Обратите внимание на амперсанды.) Формат с кавычками доступен также через %q,
если применяется к типу string или []byte.
Альтернативный формат %#q использует обратные кавычки, если возможно.
(%q также применяется к целым числам и рунам, формируя одинарно-кавычную константу руны.)
Формат %x работает со строками, массивами байтов и срезами байтов, а также с целыми числами,
создавая длинную шестнадцатеричную строку; с пробелом в формате (% x) между байтами вставляются пробелы.
Ещё один удобный формат — %T, который выводит тип значения.
fmt.Printf("%T\n", timeZone)
выводит:
map[string]int
Чтобы управлять стандартным форматом вывода для пользовательского типа, нужно определить
метод с сигнатурой String() string для этого типа. Для нашего типа T это может выглядеть так:
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
выведет:
7/-2.35/"abc\tdef"
(Если нужно выводить значения типа T, а не только указатели на T,
приёмник метода String должен быть типом значения. Здесь использован указатель — это более эффективно и идиоматично для структур.
См. раздел pointers vs. value receivers для подробностей.)
Метод String может вызывать Sprintf, поскольку функции печати полностью реентерабельны и могут быть обёрнуты таким образом.
Важно понимать одну деталь: не стройте метод String, вызывая Sprintf так, чтобы он бесконечно рекурсировал в сам себя. Это произойдёт, если Sprintf попытается вывести приёмник напрямую как строку, что снова вызовет метод.
Пример ошибки:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Ошибка: бесконечная рекурсия
}
Исправление простое: преобразовать аргумент в базовый тип string, который не имеет метода String:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: обратите внимание на приведение
}
В разделе инициализация мы увидим ещё одну технику, которая позволяет избежать рекурсии.
Ещё одна техника печати заключается в передаче аргументов одной функции печати напрямую другой функции. Сигнатура Printf использует тип ...interface{} для последнего аргумента, чтобы указать, что после формата может идти произвольное количество параметров (любого типа).
func Printf(format string, v ...interface{}) (n int, err error) {
Внутри функции Printf v ведёт себя как переменная типа []interface{}, но если она передаётся другой вариативной функции, она интерпретируется как обычный список аргументов. Вот реализация функции log.Println, которую мы использовали ранее. Она напрямую передаёт свои аргументы fmt.Sprintln для фактического форматирования.
// Println печатает в стандартный логгер, как fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output принимает параметры (int, string)
}
Мы пишем ... после v при вызове Sprintln, чтобы компилятор трактовал v как список аргументов; иначе он передаст v как один срез.
С печатью связано ещё больше, чем мы рассмотрели здесь. Смотрите документацию godoc для пакета fmt для деталей.
Кстати, параметр ... может быть определённого типа, например ...int для функции Min, которая выбирает наименьшее значение из списка целых чисел:
func Min(a ...int) int {
min := int(^uint(0) >> 1) // наибольшее значение int
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append¶
Теперь у нас есть недостающий кусок, который нужен для объяснения конструкции встроенной функции append. Сигнатура append отличается от нашей пользовательской функции Append, приведённой выше. Схематично она выглядит так:
func append(slice []T, elements ...T) []T
где T — это заглушка для любого типа. На самом деле в Go нельзя написать функцию, где тип T определяется вызывающей стороной. Поэтому append встроена: она требует поддержки со стороны компилятора.
Что делает append: добавляет элементы в конец среза и возвращает результат. Результат нужно возвращать, потому что, как и в нашей самописной Append, базовый массив может измениться. Этот простой пример:
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
выводит [1 2 3 4 5 6]. Таким образом, append работает немного как Printf, собирая произвольное количество аргументов.
А если мы хотим сделать то, что делает наша Append, и добавить один срез к другому? Легко: используем ... при вызове, так же как мы делали в вызове Output выше. Этот фрагмент даёт такой же результат, как и предыдущий.
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
Без ... это не скомпилировалось бы, потому что типы были бы неправильными; y не является типом int.
Инициализация¶
Хотя на первый взгляд она не сильно отличается от инициализации в C или C++, инициализация в Go более мощная. Сложные структуры можно создавать во время инициализации, а вопросы порядка инициализации объектов, даже между разными пакетами, обрабатываются корректно.
Константы¶
Константы в Go — это именно константы. Они создаются на этапе компиляции, даже если определены локально внутри функций, и могут быть только числами, символами (runes), строками или булевыми значениями. Из-за ограничения на этапе компиляции выражения, их задающие, должны быть вычисляемыми компилятором. Например, 1<<3 — константное выражение, а math.Sin(math.Pi/4) — нет, потому что вызов функции math.Sin выполняется во время работы программы.
В Go перечисляемые константы создаются с помощью перечислителя iota. Поскольку iota может быть частью выражения, а выражения могут повторяться неявно, легко строить сложные наборы значений.
type ByteSize float64
const (
_ = iota // игнорируем первое значение, присваивая его пустому идентификатору
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
Возможность добавлять методы, такие как String, к любому пользовательскому типу позволяет произвольным значениям автоматически форматироваться для печати. Хотя чаще всего это применяется к структурам, этот приём также полезен для скалярных типов, таких как числа с плавающей точкой, например ByteSize.
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
Выражение YB выводится как 1.00YB, а ByteSize(1e13) выводится как 9.09TB.
Использование Sprintf для реализации метода String типа ByteSize безопасно (избегает бесконечной рекурсии) не из-за преобразования, а потому что вызывается с %f, который ожидает число с плавающей точкой. Sprintf вызовет метод String только если нужно получить строку, а %f требует число с плавающей точкой.
Переменные¶
Переменные могут инициализироваться так же, как константы, но инициализатор может быть любым выражением, вычисляемым во время выполнения.
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
Функция init¶
Наконец, каждый исходный файл может определять собственную функцию init без аргументов для настройки необходимого состояния. (На самом деле в одном файле может быть несколько функций init.)
И «наконец» означает, что init вызывается после того, как все объявления переменных в пакете вычислили свои инициализаторы, а те, в свою очередь, вычисляются только после того, как будут инициализированы все импортированные пакеты.
Помимо инициализаций, которые невозможно выразить декларациями, обычное применение функций init — проверка или исправление корректности состояния программы перед началом реального выполнения.
func init() {
if user == "" {
log.Fatal("$USER не задан")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath может быть переопределён флагом --gopath при запуске программы.
flag.StringVar(&gopath, "gopath", gopath, "переопределить стандартный GOPATH")
}
Методы¶
Указатели против значений¶
Как мы видели на примере ByteSize, методы могут быть определены для любого именованного типа (кроме указателя или интерфейса); приёмник не обязательно должен быть структурой.
В обсуждении срезов выше мы писали функцию Append. Её можно определить как метод для срезов. Для этого сначала объявляем именованный тип, к которому можно привязать метод, а затем делаем приёмником метода значение этого типа.
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Тело точно такое же, как в функции Append выше.
}
Это всё ещё требует, чтобы метод возвращал обновлённый срез. Можно избавиться от этого неудобства, определив метод с указателем на ByteSlice как приёмник, чтобы метод мог изменять срез вызывающего.
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Тело как выше, без return.
*p = slice
}
На самом деле можно сделать ещё лучше. Если изменить функцию так, чтобы она выглядела как стандартный метод Write, например:
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Снова как выше.
*p = slice
return len(data), nil
}
Тогда тип *ByteSlice удовлетворяет стандартному интерфейсу io.Writer, что удобно. Например, мы можем печатать в него:
var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7)
Мы передаём адрес ByteSlice, потому что только *ByteSlice удовлетворяет io.Writer.
Правило для методов с указателями и значениями: методы со значением могут вызываться как на указателях, так и на значениях, но методы с указателем — только на указателях.
Это правило существует потому, что методы с указателем могут изменять приёмник; вызов их на значении передал бы копию, и изменения были бы потеряны. Язык запрещает такую ошибку.
Есть удобное исключение: если значение адресуемо, компилятор автоматически вставляет оператор адреса, когда вызывается метод с указателем на значение. В нашем примере переменная b адресуема, поэтому можно вызвать b.Write, а компилятор перепишет это как (&b).Write.
Кстати, идея использования Write для среза байтов лежит в основе реализации bytes.Buffer.
Интерфейсы и другие типы¶
Интерфейсы¶
Интерфейсы в Go предоставляют способ указать поведение объекта: если что-то умеет делать это, значит оно может использоваться здесь. Мы уже видели несколько простых примеров: пользовательские принтеры могут быть реализованы через метод String, а Fprintf может выводить данные в любой объект с методом Write. Интерфейсы с одним или двумя методами встречаются часто и обычно получают имя, производное от метода, например io.Writer для объектов, реализующих Write.
Тип может реализовывать несколько интерфейсов.
Например, коллекция может быть отсортирована с помощью функций из пакета sort, если она реализует sort.Interface, содержащий методы Len(), Less(i, j int) bool и Swap(i, j int). При этом она также может иметь собственный форматтер. В этом искусственном примере Sequence удовлетворяет обоим требованиям.
type Sequence []int // Методы, необходимые для sort.Interface. func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Copy возвращает копию Sequence. func (s Sequence) Copy() Sequence { copy := make(Sequence, 0, len(s)) return append(copy, s...) } // Метод для печати - сортирует элементы перед выводом. func (s Sequence) String() string { s = s.Copy() // Создаём копию; не изменяем оригинал. sort.Sort(s) str := "[" for i, elem := range s { // Цикл O(N²); исправим в следующем примере. if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
Преобразования¶
Метод String для Sequence повторяет работу, которую Sprint уже выполняет для срезов. (Кроме того, его сложность O(N²), что плохо.) Мы можем разделить работу (и ускорить её), если преобразуем Sequence в обычный []int перед вызовом Sprint.
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
Этот метод является ещё одним примером техники преобразования типа для безопасного вызова Sprintf из метода String.
Поскольку два типа (Sequence и []int) идентичны по данным, игнорируя имя типа, такое преобразование легально. Преобразование не создаёт нового значения, оно лишь временно рассматривает существующее значение как другой тип.
(Существуют и другие легальные преобразования, например, из целого числа в число с плавающей точкой, которые создают новое значение.)
В Go обычной практикой является преобразование типа выражения для доступа к другому набору методов. Например, мы можем использовать существующий тип sort.IntSlice, чтобы упростить весь пример:
type Sequence []int
// Метод для печати - сортирует элементы перед выводом
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
Теперь вместо того, чтобы заставлять Sequence реализовывать несколько интерфейсов (сортировку и печать), мы используем возможность преобразования одного объекта к разным типам (Sequence, sort.IntSlice и []int), каждый из которых выполняет свою часть работы. На практике это встречается реже, но может быть эффективно.
Преобразования интерфейсов и утверждения типов¶
Type switches (типовые переключатели) — это форма преобразования: они берут интерфейс и для каждого case в switch, по сути, преобразуют его к типу этого case. Вот упрощённая версия того, как код под fmt.Printf превращает значение в строку с помощью типового переключателя.
Если это уже строка, мы хотим получить реальное значение строки из интерфейса, а если есть метод String, мы вызываем его.
type Stringer interface {
String() string
}
var value interface{} // Значение, предоставленное вызывающим.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
Первый case находит конкретное значение; второй преобразует интерфейс в другой интерфейс. Так смешивать типы вполне нормально.
Что если нас интересует только один тип? Если мы знаем, что значение содержит string и хотим просто извлечь его, можно использовать одно-случайный type switch, но можно и утверждение типа (type assertion).
Утверждение типа извлекает из интерфейсного значения значение указанного типа. Синтаксис похож на открывающий case type switch, но с явным указанием типа вместо ключевого слова type:
value.(typeName)
Результат — новое значение со статическим типом typeName. Этот тип должен либо совпадать с конкретным типом, хранящимся в интерфейсе, либо быть другим интерфейсом, к которому значение можно преобразовать. Чтобы извлечь строку, которую мы знаем, что содержит значение, пишем:
str := value.(string)
Но если окажется, что значение не содержит строку, программа аварийно завершится с ошибкой времени выполнения. Чтобы этого избежать, используйте идиому "comma, ok" для безопасной проверки:
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
Если утверждение типа не удалось, str всё равно существует и имеет тип string, но будет равен нулевому значению — пустой строке.
Для иллюстрации возможностей вот if-else конструкция, эквивалентная type switch, открывающему этот раздел:
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
Обобщённость¶
Если тип существует только для реализации интерфейса и никогда не будет иметь экспортируемых методов за пределами этого интерфейса, нет необходимости экспортировать сам тип. Экспортируя только интерфейс, становится ясно, что значение не обладает интересным поведением кроме того, что описано в интерфейсе. Это также избавляет от необходимости повторять документацию для каждого случая общего метода.
В таких случаях конструктор должен возвращать значение интерфейса, а не реализующий тип.
Например, в библиотеках хеширования функции crc32.NewIEEE и adler32.New возвращают тип интерфейса hash.Hash32.
Замена алгоритма CRC-32 на Adler-32 в программе на Go требует лишь изменения вызова конструктора; остальной код не зависит от алгоритма.
Аналогичный подход позволяет алгоритмам потокового шифрования в различных пакетах crypto быть отделёнными от блочных шифров, с которыми они работают.
Интерфейс Block в пакете crypto/cipher определяет поведение блочного шифра, который шифрует один блок данных.
По аналогии с пакетом bufio, пакеты шифрования, реализующие этот интерфейс, могут использоваться для построения потоковых шифров, представленных интерфейсом Stream, без знания деталей блочного шифрования.
Интерфейсы пакета crypto/cipher выглядят так:
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
Вот определение режима счётчика (CTR) для потокового шифра, который превращает блочный шифр в потоковый; обратите внимание, что детали блочного шифра абстрагированы:
// NewCTR возвращает Stream, который шифрует/расшифровывает с использованием данного Block в // режиме счётчика. Длина iv должна совпадать с размером блока Block. func NewCTR(block Block, iv []byte) Stream
NewCTR применяется не только к одному конкретному алгоритму шифрования и источнику данных, но к любой реализации интерфейса Block и любому Stream.
Поскольку функции-конструкторы возвращают значения интерфейсов, замена шифрования CTR на другой режим — локальное изменение. Вызовы конструктора нужно отредактировать, но окружающий код будет работать только с Stream и не заметит разницы.
Интерфейсы и методы¶
Поскольку почти любому объекту можно прикрепить методы, почти любой объект может удовлетворять интерфейсу.
Иллюстративный пример можно найти в пакете http, который определяет интерфейс Handler. Любой объект, реализующий Handler, может обрабатывать HTTP-запросы.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter сам по себе является интерфейсом и предоставляет методы для отправки ответа клиенту.
Эти методы включают стандартный метод Write, поэтому http.ResponseWriter можно использовать там, где требуется io.Writer.
Request — это структура, содержащая разобранное представление запроса от клиента.
Для простоты будем считать, что HTTP-запросы всегда GET; это не влияет на способ настройки обработчиков. Вот тривиальная реализация обработчика, который считает количество посещений страницы.
// Простой сервер-счётчик.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
(Заметим, что Fprintf может выводить в http.ResponseWriter.)
В реальном сервере доступ к ctr.n нужно защищать от одновременного доступа.
Смотрите пакеты sync и atomic для примеров.
Для справки: вот как подключить такой сервер к узлу URL-дерева.
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
Но зачем делать Counter структурой? На самом деле нужен только целочисленный счётчик.
(Приёмник должен быть указателем, чтобы инкремент был виден вызывающей стороне.)
// Проще сервер-счётчик.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
Если в вашей программе есть внутреннее состояние, которое должно получать уведомление о посещении страницы, привяжите канал к веб-странице.
// Канал, который отправляет уведомление при каждом посещении.
// (Вероятно, канал стоит сделать с буфером.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
Наконец, предположим, что мы хотим показать на /args аргументы,
с которыми был вызван бинарный файл сервера.
Написать функцию для печати аргументов очень просто.
func ArgServer() {
fmt.Println(os.Args)
}
Как превратить это в HTTP-сервер? Мы могли бы сделать ArgServer методом какого-либо типа, значение которого нам не важно, но есть более элегантный способ.
Поскольку метод можно определить для любого типа, кроме указателей и интерфейсов, мы можем написать метод для функции.
Пакет http содержит следующий код:
// Тип HandlerFunc — адаптер, позволяющий использовать
// обычные функции как HTTP-обработчики. Если f — функция
// с соответствующей сигнатурой, HandlerFunc(f) является
// объектом Handler, который вызывает f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP вызывает f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
HandlerFunc — это тип с методом ServeHTTP, поэтому значения этого типа могут обрабатывать HTTP-запросы.
Обратите внимание: приёмник метода — это функция f, а метод просто вызывает f.
Это может показаться необычным, но похоже на случай, когда приёмником является канал, а метод отправляет в него данные.
Чтобы сделать из ArgServer HTTP-обработчик, сначала изменим её сигнатуру:
// Сервер аргументов.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
Теперь ArgServer имеет такую же сигнатуру, как HandlerFunc,
поэтому её можно привести к этому типу, чтобы получить доступ к методам,
так же, как мы преобразовывали Sequence в IntSlice, чтобы вызвать Sort.
Код настройки получается лаконичным:
http.Handle("/args", http.HandlerFunc(ArgServer))
Когда кто-то посещает страницу /args,
установленный обработчик имеет значение ArgServer и тип HandlerFunc.
HTTP-сервер вызовет метод ServeHTTP этого типа, где приёмником будет ArgServer,
что, в свою очередь, вызовет ArgServer (через f(w, req) внутри HandlerFunc.ServeHTTP).
Аргументы будут отображены.
В этом разделе мы создали HTTP-сервер из структуры, целого числа, канала и функции — всё потому, что интерфейсы — это просто наборы методов, которые можно определить почти для любого типа.
Пустой идентификатор¶
Мы уже упоминали пустой идентификатор несколько раз, в контексте
for range циклов
и карт (maps).
Пустому идентификатору можно присвоить или объявить любое значение любого типа — значение просто игнорируется.
Это немного похоже на запись в Unix-файл /dev/null:
он представляет собой "только для записи" идентификатор, используемый как заполнитель,
когда переменная нужна, но фактическое значение не важно.
У него есть и другие применения, кроме тех, что мы уже рассмотрели.
Пустой идентификатор в множественном присваивании¶
Использование пустого идентификатора в for range цикле — это
частный случай более общей ситуации: множественного присваивания.
Если присваивание требует несколько значений слева, но одно из значений не будет использоваться программой, пустой идентификатор слева позволяет избежать создания фиктивной переменной и ясно показывает, что это значение следует отбросить. Например, при вызове функции, которая возвращает значение и ошибку, но важна только ошибка, используйте пустой идентификатор, чтобы отбросить неважное значение.
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s не существует\n", path)
}
Иногда можно встретить код, который игнорирует возвращаемое значение ошибки, чтобы просто не обрабатывать её; это крайне плохая практика. Всегда проверяйте ошибки — они возвращаются не просто так.
// Плохо! Этот код сломается, если путь не существует.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s это директория\n", path)
}
Неиспользуемые импорты и переменные¶
Ошибкой считается импорт пакета или объявление переменной без её использования. Неиспользуемые импорты раздувают программу и замедляют компиляцию, а переменная, которая инициализирована, но не используется, по крайней мере, тратит ресурсы и может быть признаком более серьёзной ошибки. Однако во время активной разработки часто возникают неиспользуемые импорты и переменные, и бывает неприятно удалять их только для того, чтобы компиляция прошла, а потом снова понадобятся. Пустой идентификатор предоставляет обходной путь.
Этот недописанный пример программы имеет два неиспользуемых импорта
(fmt и io) и неиспользуемую переменную (fd),
поэтому он не скомпилируется, но было бы полезно проверить корректность текущего кода.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: использовать fd.
}
Чтобы подавить сообщения о неиспользуемых импортированных пакетах,
можно использовать пустой идентификатор для обращения к символу из пакета.
Аналогично, присваивание неиспользуемой переменной fd пустому идентификатору
подавит ошибку о неиспользуемой переменной.
Эта версия программы компилируется.
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // Для отладки; удалить после завершения.
var _ io.Reader // Для отладки; удалить после завершения.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: использовать fd.
_ = fd
}
По соглашению, глобальные объявления для подавления ошибок импорта должны располагаться сразу после импортов и содержать комментарии, чтобы их было легко найти и помнить о необходимости удалить их позже.
Импорт ради побочного эффекта¶
Неиспользуемый импорт вроде fmt или io в предыдущем примере
в конечном итоге должен быть использован или удалён:
пустые присваивания указывают на то, что код находится в разработке.
Но иногда полезно импортировать пакет только ради его побочных эффектов,
без явного использования.
Например, во время выполнения функции init пакет
net/http/pprof регистрирует HTTP-обработчики,
которые предоставляют отладочную информацию. У пакета есть экспортируемый API,
но большинству клиентов нужны только регистрация обработчиков,
а доступ к данным осуществляется через веб-страницу.
Чтобы импортировать пакет только ради побочных эффектов,
переименуйте пакет в пустой идентификатор:
import _ "net/http/pprof"
Такая форма импорта ясно показывает, что пакет импортируется ради побочных эффектов, поскольку иного использования пакета нет: в этом файле он не имеет имени. (Если бы имя было, а мы его не использовали, компилятор отклонил бы программу.)
Проверка интерфейсов¶
Как мы видели в обсуждении интерфейсов выше,
тип не обязан явно объявлять, что он реализует интерфейс.
Тип реализует интерфейс просто реализуя его методы.
На практике большинство преобразований интерфейсов статические и проверяются во время компиляции.
Например, передача *os.File в функцию, ожидающую io.Reader,
не скомпилируется, если *os.File не реализует интерфейс io.Reader.
Однако некоторые проверки интерфейсов происходят во время выполнения.
Один из примеров — пакет encoding/json,
который определяет интерфейс Marshaler.
Когда JSON-кодировщик получает значение, реализующее этот интерфейс,
он вызывает метод маршализации этого значения для преобразования в JSON вместо стандартного преобразования.
Кодировщик проверяет это свойство во время выполнения с помощью type assertion, например:
m, ok := val.(json.Marshaler)
Если нужно лишь проверить, реализует ли тип интерфейс, не используя сам интерфейс (например, для проверки ошибок), можно использовать пустой идентификатор, чтобы проигнорировать значение после приведения типа:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
Одна из ситуаций, когда это применяется, — гарантия внутри пакета, что тип
действительно удовлетворяет интерфейсу.
Например, если тип json.RawMessage
должен иметь собственное JSON-представление, он должен реализовать
json.Marshaler, но нет статических преобразований, которые заставили бы компилятор проверить это автоматически.
Если тип случайно не реализует интерфейс, JSON-кодировщик всё равно будет работать,
но не будет использовать кастомную реализацию.
Чтобы гарантировать корректность реализации, можно использовать глобальное объявление с пустым идентификатором:
var _ json.Marshaler = (*RawMessage)(nil)
В этом объявлении приведение *RawMessage к Marshaler
требует, чтобы *RawMessage реализовывал Marshaler,
и это свойство будет проверено во время компиляции.
Если интерфейс json.Marshaler изменится, пакет больше не скомпилируется,
и мы будем знать, что его нужно обновить.
Использование пустого идентификатора в этой конструкции показывает, что объявление существует только для проверки типов, а не для создания переменной. Не делайте это для каждого типа, реализующего интерфейс. По соглашению, такие объявления применяются только тогда, когда статических преобразований в коде нет, что встречается редко.
Встраивание (Embedding)¶
Go не предоставляет типичное понятие наследования, основанного на типах, но позволяет «заимствовать» части реализации через встраивание типов в структуру или интерфейс.
Встраивание интерфейсов очень простое.
Мы уже упоминали интерфейсы io.Reader и io.Writer;
вот их определения:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Пакет io также экспортирует несколько других интерфейсов,
описывающих объекты, которые могут реализовать несколько таких методов.
Например, существует io.ReadWriter — интерфейс, содержащий и Read, и Write.
Мы могли бы явно перечислить оба метода, но проще и нагляднее встроить два интерфейса для создания нового:
// ReadWriter — интерфейс, объединяющий Reader и Writer.
type ReadWriter interface {
Reader
Writer
}
Это означает ровно то, что видно: ReadWriter может делать то, что делает Reader, и то, что делает Writer;
это объединение встроенных интерфейсов.
В интерфейсы можно встраивать только другие интерфейсы.
Та же идея применима и к структурам, но с более широкими последствиями.
Пакет bufio содержит два типа структур: bufio.Reader и bufio.Writer,
каждая из которых реализует соответствующий интерфейс из пакета io.
Пакет bufio также реализует буферизированный ридер/райтер,
объединяя ридер и райтер в одну структуру с помощью встраивания:
типы перечислены внутри структуры без указания имен полей.
// ReadWriter хранит указатели на Reader и Writer.
// Реализует io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Встроенные элементы — это указатели на структуры, и они должны быть инициализированы корректными структурами перед использованием.
Структура ReadWriter могла бы быть записана как:
type ReadWriter struct {
reader *Reader
writer *Writer
}
Но тогда, чтобы «продвинуть» методы полей и удовлетворить интерфейсы io,
надо было бы написать методы-перенаправители:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
Встраивая структуры напрямую, мы избегаем дополнительного «обслуживания» методов.
Методы встроенных типов автоматически становятся методами внешнего типа, что означает, что bufio.ReadWriter
имеет не только методы bufio.Reader и bufio.Writer, но и удовлетворяет всем трём интерфейсам:
io.Reader, io.Writer и io.ReadWriter.
Существует важное отличие встраивания от наследования. Когда мы встраиваем тип,
его методы становятся методами внешнего типа, но при вызове receiver метода остаётся внутренним типом, а не внешним.
В нашем примере вызов метода Read у bufio.ReadWriter
работает точно так же, как если бы мы писали метод-перенаправитель;
receiver — это поле reader структуры ReadWriter, а не сама структура ReadWriter.
Встраивание также может быть простым удобством. Пример показывает встроенное поле вместе с обычным именованным полем.
type Job struct {
Command string
*log.Logger
}
Теперь тип Job имеет методы Print, Printf, Println
и другие методы *log.Logger.
Мы могли бы дать Logger имя поля, но это не обязательно.
После инициализации можно вести лог прямо через Job:
job.Println("starting now...")
Logger является обычным полем структуры Job,
поэтому его можно инициализировать привычным образом в конструкторе Job, например:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
Или с помощью составного литерала:
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
Если нужно обратиться к встроенному полю напрямую, используется имя типа поля без квалификатора пакета,
как это делалось в методе Read структуры ReadWriter.
Здесь, чтобы обратиться к *log.Logger переменной job, пишем job.Logger,
что полезно, если хотим расширить методы Logger.
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
Встраивание типов создаёт потенциальные конфликты имён, но правила их разрешения просты.
Во-первых, поле или метод X скрывает любые другие элементы X более глубоко вложенных типов.
Если бы log.Logger содержал поле или метод Command,
поле Command структуры Job имело бы приоритет.
Во-вторых, если одинаковое имя встречается на одном уровне вложенности, это обычно ошибка;
например, было бы ошибкой встраивать log.Logger, если структура Job уже имела поле или метод Logger.
Однако если дублирующее имя никогда не используется вне определения типа, это допустимо.
Такое правило защищает от конфликтов при изменениях внешних типов:
если добавляется поле с именем, совпадающим с полем в другом встроенном типе, это не проблема, если ни одно из полей не используется.
Конкурентность¶
Делитесь через коммуникацию¶
Параллельное программирование — обширная тема, и здесь приведены только некоторые особенности Go.
В многих средах параллельное программирование осложняется необходимостью правильно управлять доступом к общим переменным. Go предлагает иной подход: общие значения передаются через каналы и фактически никогда не разделяются между отдельными потоками исполнения. В каждый момент времени только одна горутина имеет доступ к значению. Гонки данных не могут возникнуть по замыслу. Чтобы подчеркнуть этот подход, была сформулирована краткая заповедь:
Не общайтесь, разделяя память; вместо этого разделяйте память через общение.
Иногда этот подход может быть чрезмерным. Например, подсчёт ссылок лучше реализовать, обернув целочисленную переменную мьютексом. Но как высокоуровневый метод, использование каналов для контроля доступа упрощает написание ясных и корректных программ.
Можно мысленно представить себе обычную однопоточную программу, работающую на одном CPU. Ей не нужны средства синхронизации. Запустите ещё один такой экземпляр — он тоже не нуждается в синхронизации. Теперь пусть два экземпляра обмениваются данными; если коммуникация выполняет роль синхронизатора, другие механизмы синхронизации не нужны. Например, Unix-конвейеры идеально подходят под эту модель. Хотя подход Go к параллельности берёт начало в Communicating Sequential Processes (CSP) Хоара, его также можно рассматривать как типобезопасное обобщение Unix-пайпов.
Горутины¶
Их называют горутінами, потому что существующие термины — потоки (threads), корутины (coroutines), процессы и т.д. — дают неверное представление. Горутина имеет простую модель: это функция, выполняющаяся параллельно с другими горутинами в том же адресном пространстве. Она лёгкая, требует немного ресурсов — почти только для выделения стека. Стэки стартуют маленькими, поэтому они дешёвые, и растут по мере необходимости, используя память из кучи.
Горутины мультиплексируются на несколько потоков ОС, поэтому если одна блокируется, например, ожидая ввода-вывода, остальные продолжают работать. Их дизайн скрывает многие сложности создания и управления потоками.
Чтобы запустить вызов функции или метода в новой горутине, используйте ключевое слово go.
Когда вызов завершится, горутина тихо завершает работу.
(Эффект похож на запись & в Unix shell для запуска команды в фоне.)
go list.Sort() // запустить list.Sort параллельно; не ждать завершения.
Анонимные функции удобно использовать для запуска в горутине.
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Обратите внимание на скобки — нужно вызвать функцию.
}
В Go анонимные функции являются замыканиями (closures): реализация гарантирует, что переменные, на которые ссылается функция, живут, пока они активны.
Эти примеры не слишком практичны, потому что функции не могут сигнализировать о завершении. Для этого нужны каналы.
Каналы¶
Подобно картам (maps), каналы создаются с помощью make, а полученное значение выступает ссылкой на внутреннюю структуру данных.
Если передан необязательный целочисленный параметр, он задаёт размер буфера канала.
По умолчанию он равен нулю — для неблокирующего (синхронного) канала.
ci := make(chan int) // неблокирующий канал для целых чисел cj := make(chan int, 0) // неблокирующий канал для целых чисел cs := make(chan *os.File, 100) // буферизованный канал для указателей на файлы
Небуферизованные каналы объединяют передачу данных — обмен значениями — с синхронизацией, гарантируя, что две вычисления (горутины) находятся в известном состоянии.
Существует много удобных идиом с использованием каналов. Например, мы запускали сортировку в фоне. Канал позволяет запускать горутину и ждать завершения сортировки.
c := make(chan int) // Создать канал
// Запустить сортировку в горутине; по завершении сигнализировать через канал.
go func() {
list.Sort()
c <- 1 // Отправить сигнал; значение не важно
}()
doSomethingForAWhile()
<-c // Ждём завершения сортировки; отбросить отправленное значение
Получатели всегда блокируются до тех пор, пока данные не станут доступны. Если канал неблокирующий, отправитель блокируется, пока получатель не примет значение. Если канал буферизованный, отправитель блокируется только до тех пор, пока значение не скопировано в буфер; если буфер полон, ждём, пока получатель заберёт значение.
Буферизованный канал можно использовать как семафор для ограничения параллелизма.
В примере входящие запросы передаются функции handle, которая отправляет значение в канал, обрабатывает запрос, затем получает значение из канала, освобождая «семафор» для следующего запроса.
Ёмкость буфера канала ограничивает число одновременных вызовов process.
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Ждём, пока активная очередь освободится
process(r) // Может выполняться долго
<-sem // Готово; разрешаем следующий запрос
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Не ждём завершения handle
}
}
Когда MaxOutstanding обработчиков выполняют process, все последующие блокируются при попытке отправки в заполненный буфер, пока один из существующих обработчиков не завершится.
Однако такая конструкция имеет проблему: Serve создаёт новую горутину для каждого запроса, хотя одновременно может выполняться только MaxOutstanding.
Если запросы приходят слишком быстро, программа может расходовать неограниченные ресурсы.
Исправление — ограничить создание горутин:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
(Примечание: в Go до версии 1.22 этот код имеет баг — переменная цикла общая для всех горутин. См. Go wiki для деталей.)
Другой способ эффективного управления ресурсами — запустить фиксированное число горутин handle, все читают из канала запросов.
Количество горутин ограничивает число одновременных вызовов process.
Эта версия Serve также принимает канал для завершения; после запуска горутин она блокируется, ожидая сигнала на этом канале.
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Запуск обработчиков
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Ждём сигнала завершения
}
Каналы каналов¶
Одним из важнейших свойств Go является то, что канал — это полноценное значение первого класса, которое можно создавать и передавать как любое другое. Частое применение этого свойства — безопасный параллельный демультиплексинг.
В примере из предыдущего раздела handle был упрощённым обработчиком запроса, но тип обрабатываемого значения не был определён.
Если этот тип содержит канал для ответа, каждый клиент может предоставить свой собственный путь для получения результата.
Вот схематичное определение типа Request:
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
Клиент предоставляет функцию и её аргументы, а также канал внутри объекта запроса для получения ответа.
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Отправляем запрос
clientRequests <- request
// Ждём ответа
fmt.Printf("answer: %d\n", <-request.resultChan)
На стороне сервера меняется только функция обработчика.
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
Разумеется, для полноценной реализации нужно сделать гораздо больше, но этот код представляет собой каркас системы параллельного, неблокирующего RPC с ограничением скорости, без единого mutex.
Параллелизация¶
Ещё одно применение этих идей — распараллеливание вычислений на несколько ядер процессора. Если вычисление можно разбить на отдельные части, которые могут выполняться независимо, их можно запускать параллельно, используя канал для сигнала о завершении каждой части.
Предположим, у нас есть ресурсоёмкая операция над вектором элементов, и результат операции для каждого элемента независим, как в этом идеализированном примере.
type Vector []float64
// Применяет операцию к элементам v[i], v[i+1] ... до v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // сигнал о завершении этой части
}
Мы запускаем части независимо в цикле, по одной на каждое CPU. Они могут завершаться в любом порядке, но это не имеет значения; мы просто подсчитываем сигналы о завершении, читая канал после запуска всех горутин.
const numCPU = 4 // количество ядер CPU
func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // Буферизация необязательна, но разумна.
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// Считываем сигналы из канала.
for i := 0; i < numCPU; i++ {
<-c // ждём завершения одной задачи
}
// Всё готово.
}
Вместо того чтобы задавать константное значение для numCPU, можно узнать, какое значение подходит, с помощью runtime.
Функция runtime.NumCPU возвращает количество физических ядер в машине:
var numCPU = runtime.NumCPU()
Существует также функция runtime.GOMAXPROCS, которая сообщает (или задаёт) количество ядер, используемых Go-программой одновременно.
По умолчанию она равна runtime.NumCPU, но может быть изменена через переменную окружения или вызовом функции с положительным числом. Вызов с 0 просто возвращает текущее значение.
Чтобы учитывать пожелания пользователя по ресурсам, стоит писать:
var numCPU = runtime.GOMAXPROCS(0)
Важно не путать понятия concurrency — структурирование программы как набора независимых выполняющихся компонентов — и parallelism — одновременное выполнение вычислений на нескольких CPU для повышения эффективности. Хотя возможности Go по concurrency облегчают организацию некоторых задач как параллельных вычислений, Go остаётся языком для конкурентного программирования, а не для параллельного, и не все задачи распараллеливания подходят под модель Go. Для обсуждения различий см. этот блог-пост.
“Протекающий” буфер¶
Инструменты конкурентного программирования могут даже облегчить выражение идей, не связанных с конкуренцией. Вот пример, абстрагированный из RPC-пакета. Гороутина клиента циклично получает данные из какого-либо источника, например, сети. Чтобы не выделять и освобождать буферы постоянно, используется свободный список буферов, представленный через буферизованный канал. Если канал пуст, выделяется новый буфер. Как только буфер сообщения готов, он отправляется серверу через serverChan.
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
var b *Buffer
// Берём буфер из свободного списка или создаём новый.
select {
case b = <-freeList:
// Буфер получен; больше ничего делать не нужно.
default:
// Нет свободных буферов, создаём новый.
b = new(Buffer)
}
load(b) // Чтение следующего сообщения из сети.
serverChan <- b // Отправка серверу.
}
}
Цикл сервера получает каждое сообщение от клиента, обрабатывает его и возвращает буфер в свободный список.
func server() {
for {
b := <-serverChan // Ждём работу.
process(b)
// Возвращаем буфер в свободный список, если есть место.
select {
case freeList <- b:
// Буфер добавлен в список; больше ничего не нужно.
default:
// Список полон, продолжаем без возврата.
}
}
}
Клиент пытается получить буфер из freeList; если доступных буферов нет, создаётся новый.
Отправка сервера в freeList возвращает буфер обратно в список, если есть место; иначе буфер “выбрасывается” и будет собран сборщиком мусора.
(default в select выполняется, если ни один другой case не готов, так что select никогда не блокируются.)
Эта реализация создаёт “протекающий” свободный список всего в несколько строк, полагаясь на буферизованный канал и сборщик мусора для управления памятью.
Ошибки¶
Библиотечные функции часто должны возвращать какой-либо индикатор ошибки вызывающему коду.
Как уже упоминалось, множественные значения возврата в Go позволяют легко возвращать подробное описание ошибки вместе с обычным значением.
Хороший стиль — использовать эту возможность для предоставления детальной информации об ошибке.
Например, os.Open не возвращает просто nil при сбое, но также возвращает значение ошибки, описывающее, что произошло.
По соглашению, ошибки имеют тип error, простой встроенный интерфейс.
type error interface {
Error() string
}
Автор библиотеки может реализовать этот интерфейс более сложным образом, чтобы не только видеть ошибку, но и предоставлять контекст.
Как упоминалось, вместе с обычным значением *os.File os.Open возвращает также значение ошибки.
Если файл открыт успешно, ошибка будет nil, а при проблеме — это будет os.PathError:
// PathError хранит информацию об ошибке, операции и
// пути к файлу, вызвавших её.
type PathError struct {
Op string // "open", "unlink" и т.д.
Path string // Файл, к которому относится ошибка.
Err error // Ошибка, возвращённая системным вызовом.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
Метод Error у PathError формирует строку вроде:
open /etc/passwx: no such file or directory
Такая ошибка, содержащая имя проблемного файла, операцию и системную ошибку, полезна даже при выводе далеко от места вызова; она гораздо информативнее простой строки "no such file or directory".
По возможности строки ошибок должны указывать своё происхождение, например, иметь префикс с названием операции или пакета.
В пакете image строковое представление ошибки декодирования из-за неизвестного формата выглядит так: "image: unknown format".
Вызывающие функции, которым важны точные детали ошибки, могут использовать type switch или утверждение типа для поиска конкретных ошибок и извлечения деталей.
Для PathError это может включать проверку внутреннего поля Err для обработки восстановимых ошибок.
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Освободить место.
continue
}
return
}
Второе выражение if здесь — ещё одно утверждение типа.
Если оно не удаётся, ok будет false, а e — nil.
Если удаётся, ok истинно, значит ошибка имеет тип *os.PathError, и тогда мы можем исследовать e для получения дополнительной информации.
Panic¶
Обычный способ сообщить об ошибке вызывающему коду — вернуть error как дополнительное значение возврата.
Классический пример — метод Read, который возвращает количество байт и error.
Но что если ошибка неисправима? Иногда программа просто не может продолжать работу.
Для этого существует встроенная функция panic, которая фактически создаёт ошибку времени выполнения и остановит программу (см. следующий раздел).
Функция принимает один аргумент любого типа — чаще всего строку, — который будет напечатан при завершении программы.
Также это способ указать на невозможное событие, например, выход из бесконечного цикла.
// Простейшая реализация вычисления кубического корня методом Ньютона.
func CubeRoot(x float64) float64 {
z := x/3 // Произвольное начальное значение
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z - x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// Миллион итераций не дал сходимости; что-то не так.
panic(fmt.Sprintf("CubeRoot(%g) не сошёлся", x))
}
Это всего лишь пример, но реальные функции библиотек должны избегать panic.
Если проблему можно скрыть или обойти, всегда лучше позволить программе продолжать работу, а не останавливать её полностью.
Возможный контрпример — инициализация: если библиотека действительно не может корректно настроиться, допустимо использовать panic.
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("нет значения для $USER")
}
}
Recover¶
Когда вызывается panic, включая неявные ошибки времени выполнения, такие как выход за пределы среза или неудачное приведение типа, выполнение текущей функции немедленно останавливается, и начинается "разворачивание" стека горутины с выполнением всех отложенных функций (defer) по пути.
Если разворачивание достигнет вершины стека горутины, программа завершится.
Однако с помощью встроенной функции recover можно вернуть управление горутине и продолжить нормальное выполнение.
Вызов recover останавливает разворачивание и возвращает аргумент, переданный panic.
Поскольку во время разворачивания выполняется только код внутри отложенных функций, recover полезен только в defer.
Пример применения recover — безопасное завершение падающей горутины внутри сервера без остановки других выполняющихся горутин.
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("работа завершилась с ошибкой:", err)
}
}()
do(work)
}
В этом примере, если do(work) вызовет panic, результат будет залогирован, а горутина завершится корректно, не нарушая работу других.
Внутри отложенной функции больше ничего делать не нужно; вызов recover полностью обрабатывает ситуацию.
Поскольку recover всегда возвращает nil, если не вызвано прямо из defer, отложенный код может вызывать библиотечные функции, которые сами используют panic и recover, без сбоев.
С этим паттерном восстановления функция do (и всё, что она вызывает) может безопасно выходить из любых проблемных ситуаций через panic.
Это упрощает обработку ошибок в сложном ПО. Ниже приведён идеализированный пример пакета regexp, который сообщает об ошибках парсинга через panic с локальным типом ошибки.
// Error — тип ошибки парсинга; реализует интерфейс error.
type Error string
func (e Error) Error() string {
return string(e)
}
// error — метод *Regexp, который сообщает об ошибках парсинга через panic с Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile возвращает разобранное представление регулярного выражения.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse вызовет panic, если есть ошибка парсинга.
defer func() {
if e := recover(); e != nil {
regexp = nil // Очистка возвращаемого значения
err = e.(Error) // Вызовет повторный panic, если не ошибка парсинга
}
}()
return regexp.doParse(str), nil
}
Если doParse вызывает panic, блок восстановления устанавливает возвращаемое значение в nil и проверяет, что ошибка действительно типа Error.
Если это не так, приведение типа вызовет ошибку времени выполнения и продолжит разворачивание стека, как будто никакого recover не было.
С обработкой ошибок метод error позволяет легко сообщать об ошибках парсинга без ручного разворачивания стека:
if pos == 0 {
re.error("'*' недопустимо в начале выражения")
}
Этот паттерн полезен, но его следует использовать только внутри пакета.
Функция Parse превращает внутренние вызовы panic в значения error, не раскрывая паники клиенту — это хорошее правило.
Примечание: идиома повторного panic изменяет значение panic, если произошла настоящая ошибка. Однако оба — исходная и новая ошибка — будут отображены в отчёте о сбое, поэтому причина остаётся видимой. Если требуется показать только исходную ошибку, можно добавить фильтрацию и повторный panic с оригинальной ошибкой.
Веб-сервер¶
Закончим полным примером программы на Go — веб-сервером.
На самом деле это своего рода веб-посредник.
Google предоставляет сервис на chart.apis.google.com, который автоматически форматирует данные в графики и диаграммы.
Однако использовать его интерактивно неудобно, потому что данные нужно помещать в URL в виде запроса.
Эта программа предлагает удобный интерфейс для одного вида данных: при подаче короткого текста она вызывает сервер графиков для генерации QR-кода — матрицы квадратиков, кодирующих текст.
Эту картинку можно сфотографировать камерой телефона и интерпретировать, например, как URL, экономя ввод его вручную на маленькой клавиатуре телефона.
Вот полный код программы. Объяснение следует далее.
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var templ = template.Must(template.New("qr").Parse(templateStr))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}
const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl=map[pageData:{docs} title:Эффективный Go - язык Golang на русском]" />
<br>
map[pageData:{docs} title:Эффективный Go - язык Golang на русском]
<br>
<br>
<form action="/" name=f method="GET">
<input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
<input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`
Части кода до функции main должны быть простыми для понимания.
Флаг задаёт порт HTTP по умолчанию для сервера. Переменная шаблона templ — это место, где начинается интересное: она строит HTML-шаблон, который сервер будет исполнять для отображения страницы. Подробнее об этом чуть позже.
Функция main парсит флаги и, используя описанный выше механизм, привязывает функцию QR к корневому пути сервера. Затем вызывается http.ListenAndServe, чтобы запустить сервер; этот вызов блокирует выполнение, пока сервер работает.
Функция QR просто получает запрос с данными формы и выполняет шаблон на данных из значения формы с именем s.
Пакет шаблонов html/template мощный; эта программа лишь слегка касается его возможностей.
По сути, он динамически переписывает HTML, подставляя элементы, полученные из данных, переданных в templ.Execute, в данном случае — значение формы.
Внутри текста шаблона (templateStr) конструкции в двойных фигурных скобках обозначают действия шаблона.
Фрагмент от до выполняется только если значение текущего элемента данных, обозначаемого как . (dot), не пустое. То есть, когда строка пуста, этот фрагмент шаблона подавляется.
Два фрагмента map[pageData:{docs} title:Эффективный Go - язык Golang на русском] означают показать данные, переданные шаблону — строку запроса — на веб-странице.
Пакет шаблонов автоматически выполняет безопасное экранирование текста для HTML.
Остальная часть строки шаблона — это просто HTML, который отображается при загрузке страницы. Если этого объяснения недостаточно, см. документацию по пакету шаблонов для более подробного изучения.
И вот оно: полезный веб-сервер в нескольких строках кода плюс HTML с данными. Go достаточно мощный, чтобы многое сделать всего за несколько строк.