Skip to content

kveselkov/Effective_Go_RU

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 

Repository files navigation

Effective Go (RU) (Эффективный Go)

Оригинал смотри: https://golang.org/doc/effective_go.html

go version go1.7.4

Введение

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

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

Примеры

Go пакеты исходных кодов предназначены не только в качестве основных библиотек, но и в качестве примеров использования языка. Кроме того, многие пакеты имеют работающие, автономные исполняемые примеры и Вы можете запустить напрямую с помощью страницы golang.org, такие как этот (если необходимо, нажмите на слово "Примеры" чтобы открыть их). Если у Вас есть вопрос о том как решить какую-либо проблему или как что-то реализовать, то документация, исходные коды и примеры в библиотеке могут дать ответ, идею или объяснение.

Форматирование

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

В Go мы используем нетипичный подход и передаем машине заботу о форматировании. Программа gofmt (также доступна, как go fmt, которая производит действия на уровне пакета, а не на уровне файлов) читает код на Go и выпускает исходный код со стандарным стилем отступов и вертикальным выравниванием, сохраняет, и при необходимости, переформатирует комментарии. Если Вы хотите знать, как по-новому структурировать код, запустите gofmt; если структура неверна, gofmt поправит Вашу программу (или файл сообщит об ошибке gofmt), не работайте в обход форматирования программой gofmt.

К примеру, нет необходимости тратить время на выравнивание комментариев для полей структур, т.к. gofmt сделает это за Вас. Для данного фрагмента кода

type T struct {
    name string // name of the object
    value int // its value
}

gofmt произведет выравнивание по колонкам:

type T struct {
    name    string // name of the object
    value   int    // its value
}

Все стандартные пакеты Go отформатированны с помощью gofmt.

Очень коротко о некоторых деталях форматирования:

Абзац

Мы используем табуляцию для абзацев и gofmt делает это по умолчанию. Используйте пробелы только при острой необходимости.

Длина строки

Go не имеет предела длины строки. Не беспокойтесь о длинных строках. Если строка кажется слишком длинной, прервите ее и добавьте дополнительный отступ (символ табуляции) на новой строке.

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

Go нуждается в меньшем количестве круглых скобок, чем C и Java: структуры ветвления, цикла ( if , for , switch) не имеют круглых скобок в своём синтаксисе. Также, иерархия операторов стала проще и короче. К примеру, выражение

x<<8 + y<<16

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

Комментарии

Go использует C-стиль /* */ для блока комментариев и C++-стиль // для однострочных комментариев. Как правило, используются однострочные комментарии. Блок комментариев, в основном, используется при комментировании пакетов, но также для выразительности или отключения большого участка кода.

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

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

/*
Package regexp implements a simple library for regular expressions.

The syntax of the regular expressions accepted is:

    regexp:
        concatenation { '|' concatenation }
    concatenation:
        { closure }
    closure:
        term [ '*' | '+' | '?' ]
    term:
        '^'
        '$'
        '.'
        character
        '[' [ '^' ] character-ranges ']'
        '(' regexp ')'
*/
package regexp

Если пакет простой, то комментарий может быть кратким.

// Package path implements utility routines for
// manipulating slash-separated filename paths.

Дополнительное формативание, к примеру баннер из * (звездочек), не требуется. Шрифт для сформированного результата не обязательно будет моноширинный, поэтому не полагайтесь на пробелы при выравнивании, godoc, также как gofmt, позаботяться об этом. Комментарии интерпретируются как простой текст, поэтому HTML и другие аннотации такие как _эта_ поспроизводятся дословно и поэтому не должны использоваться. Единственное исключение, которое делает godoc, это выделение моноширинным шрифтом участков кода с отступами. Хорошим примером такого исключения является комментарий к пакету fmt.

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

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

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

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

Если комментарий начинается с имени, то godoc может с использоваться совместно с grep. Представьте, что Вы не можете вспомнить имя "Compile", но Вы ищите the parsing function для регулярных выражений и тогда Вы можете выполнить команду:

$ godoc regexp | grep -i parse

Если все коментарии в пакете начанаются с "This function...", grep не сможет помочь с поиском имени. Если же комментарии начинаются с имени, Вы можете увидеть что-то вроде следующего результата, который напомнит Вам о том, что Вы искали.

$ godoc regexp | grep parse
    Compile parses a regular expression and returns, if successful, a Regexp
    parsed. It simplifies safe initialization of global variables holding
    cannot be parsed. It simplifies safe initialization of global variables
$

Синтаксис Go допускает групповое объявление. Для каждой группы констант или переменных может быть представлен один общий комментарий. Однако такое объявление выглядит небрежно.

// Error codes returned by failures to parse an expression.
var (
    ErrInternal      = errors.New("regexp: internal error")
    ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
    ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
    ...
)

Группировка также может показать взаимосвязи между элементами, к примеру, группа переменных защищенных mutex:

var (
    countLock   sync.Mutex
    inputCount  uint32
    outputCount uint32
    errorCount  uint32
)

Именование

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

Именование пакетов

Когда пакет импортируется, имя пакета используется для доступа к его содержимому. После того, как пакет импортирован,

import "bytes"

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

Согласно другому соглашению, имя пакета является базовым именем его исходного каталога; пакет src/encoding/base64 импортируется как "encoding/base64" и имеет название base64, а не encoding_base64 и не encodingBase64.

Импортирующий пакет будет использовать имя пакета для обозначения его содержимого, поэтому при экспорте может учитываться этот факт, чтобы избежать повторения. (Не используйте import ., это, конечно, может упростить запуск тестов вне пакета, но в других случаях использоваться не должно). Например, тип reader для буферного чтения описанный в пакете bufio называется Reader, а не BufReader, т.к пользователи его видят как bufio.Reader, имя которого кратко и понятно.

Более того, т.к. импортируемые объекты адресуются по имени пакета, следовательно bufio.Reader не будет конфликтовать с io.Reader. Аналогично, функция для создания нового экземпляра объекта ring.Ring, которая объявлена как конструктор в Go, может называться NewRing, но т.к. Ring - это экспортируемый тип из пакета ring, функция-конструктор может называться просто New, которую, можно будет вызвать как ring.New. Используйте структуру пакетов при выборе имен.

Другой короткий пример функция once.Do; once.Do(setup) читается хорошо, и при этом луше не станет, если ее переименовать в once.DoOrWaitUntilDone(setup). Длинные имена не делают названия более читабельными. В то время как комментарии могут быть более ценным, чем длинные имена.

Геттеры

Go не предоставляет автоматическую поддержку геттеров и сеттеров. Но не будет ошибкой создание геттеров и сеттеров самостоятельно, и если это необходимо, то делайте так, но идиоматически нет необходимости добавлять Get в имя геттера. Если у Вас есть поле с именем owner (с маленькой буквы, неэкспортируемое), то геттер может называться Owner (с большой буквы, экспортируемый), а не GetOwner. Использование имен, начинающихся с заглавной буквы, позволяет отделить экспортируемые методы от неэкспортируемых полей. Cеттер, при необходимости, может быть назван 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, а не подчеркивание для имен из нескольких слов.

Точка с запятой

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

Правило такое. Если последний токен(лексема) перед символом новой строки - идентификатор (который включает такие слова, как int и float64), базовый литерал, такой как, число или строковая константа, или один из нижеперечисленных токенов

break continue fallthrough return ++ -- ) }

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

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

     go func() { for { dst <- <-src } }()

точка с запятой не требуется.

Как следствие из правила, вы не можете перенести открывающую скобку управляющих структур (if, for, switch или select) на новую строку. Если перенесете, точка с запятой будет вставлена перед скобкой, которая может стать причиной нежелательных эффектов. Пишите так,

if i < f() {
    g()
}

но не так

if i < f()  // ошибка!
{           // ошибка!
    g()
}

Управляющие струтуры

Управляющие структуры в Go аналогичны тем же струтурам в C, но имеют ряд важных отличий. Во-первых нет циклов do и while, есть лишь обобщенный for. Во-вторых, switch более гибкий. В-третьих if и switch имеют опциональную инициализацию переменных, как и в for. В-четвертых, break и continue опционально принимают метку, к которой необходимо перейти. В-пятых, есть новые операторы, такие как типизированный 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

f, err := os.Open(name)

Несколькими строками ниже вызывается f.Stat,

d, err := f.Stat()

который выглядит как объявления двух переменных d и err. Хотя err присутствует в обоих объявлениях. Это дублирование вполне законно: err объявляется в первом случае, и лишь переприсваивается во втором. Это означает, что f.Stat использует уже существующую переменную err, определенную выше, и просто присваивает ей новое значение.

В объявлении := переменная v может присутствовать, даже если она уже объявлена, при условии:

  • если объявление в той же самой области видимости, что и существующее объявление переменной v (если v уже объявлена за пределами видимости, то объявление создаст новую переменную §)
  • соответствующее значение, при инициализации, может быть присвоено v
  • существует хотя бы одна новая переменная в объявлении, которая будет создана заново

Это необычное свойство - чистая практичность, которая служит для упрощения использования одной переменной err, к примеру, в длинных цепочках if-else. Вы увидите, это используется часто.

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

Оператор For

В Go цикл for очень похож, но не такой же как в C. Он унифицирует for и while, при это отсутсвует do-while цикл. Существует 3 различных формы и только в одной из них используется точка с запятой.

// C-подобный for
for init; condition; post { }

// C-подобный while
for condition { }

// C-подобный for(;;)
for { }

Краткая запись позволяет легко объявить начальные условия прямо в цикле:

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

Если Вы итерируетесь по массиву, срезу, строке или map'у, или читаете из канала, то для управления можно использовать 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. При ошибочном использование кодировки, по-байтово заменяет рунами(rune) U+FFFD. (rune (и одноименный встроенный тип) в терминологии Go используеться для работы с символами Unicode. Смотрите детальную информацию в Спецификации языка).

Данный цикл:

for pos, char := range "日本\x80語" { // \x80 is an illegal UTF-8 encoding
    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, то Вы можете использовать параллельное определение переменных (без использования ++ и --).

// Reverse 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
}

Автоматический пропуск условий отсутствует, но, при этом, условия могут быть записаны через запятую:

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

Несмотря на то, что они не столь распространены в Go, как в некоторых других C-подобных языках, break может быть использован для досрочного прерывания switch. Хотя, иногда, надо прервать внешний (по отношению к switch) цикл, а не сам switch, и в Go это может быть достигнуто путём добавления метки перед циклом, и переходом к этой метке в случае вызова 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 returns an integer comparing the two byte slices,
// lexicographically.
// The result will be 0 if a == b, -1 if a < b, and +1 if 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 может быть использован для определения динамических типов интерфейсных переменных. Так, типизированный switch использует синтаксис приведения типов, с ключевым словом type внутри скобок. Если switch объявляет переменную в выражении, то переменная будет иметь соответствующий тип в каждом пункте. Также, идиоматически верно переиспользовать имена переменных для объявления новых пременных с тем же именем, но другим типом в каждом случае:

var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
    fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
case bool:
    fmt.Printf("boolean %t\n", t)             // t has type bool
case int:
    fmt.Printf("integer %d\n", t)             // t has type int
case *bool:
    fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
    fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}

Функции и методы(Functions, методы)

Множественное возвращение результатов

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

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

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

func (file *File) Write(b []byte) (n int, err error)

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

TODO if the function executes a return statement with no arguments, the current values of the result parameters are used as the returned values. -

Именование не обязательное, но оно может сделать код короче и чище, самодокументируемым. Если имя результата будет nextInt, то очевидно что тип результата int.

func nextInt(b []byte, pos int) (value, nextPos int) {

На примере 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)

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

// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
        return "", err
    }
    defer f.Close()  // f.Close will run when we're finished.

    var result []byte
    buf := make([]byte, 100)
    for {
        n, err := f.Read(buf[0:])
        result = append(result, buf[0:n]...) // append is discussed later.
        if err != nil {
            if err == io.EOF {
                break
            }
            return "", err  // f will be closed if we return here.
        }
    }
    return string(result), nil // f will be closed if we return here.
}

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

Аргументы отложенной функции выполняются когда выполняется 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) }

// Use them like this:
func a() {
    trace("a")
    defer untrace("a")
    // do something....
}

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

TODO Since the memory returned by new is zeroed, it's helpful to arrange when designing your data structures that the zero value of each type can be used without further initialization. This means a user of the data structure can create one with new and get right to work. For example, the documentation for bytes.Buffer states that "the zero value for Buffer is an empty buffer ready to use." Similarly, sync.Mutex does not have an explicit constructor or Init method. Instead, the zero value for a sync.Mutex is defined to be an unlocked mutex. The zero-value-is-useful property works transitively. Consider this type declaration. -

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

TODO Values of type SyncedBuffer are also ready to use immediately upon allocation or just declaration. In the next snippet, both p and v will work correctly without further arrangement. -

p := new(SyncedBuffer)  // type *SyncedBuffer
var v SyncedBuffer      // type  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
}

Обратите внимание на то, что в отличии от языка С, это нормально, возвращать адрес локальных переменных, так как переменная уже существует после возвращения из функции. На самом деле, возвращение адресов сложных литералов создает новую сущность каждый раз, как он вычисляется. Итак мы можем объединить последние две строки:

    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 значений типа int и затем создает структуру среза длинной 10 и емкостью 100 со ссылкой только на первые 10 элементов. (Когда создается слайс, его емкость задавать не обязательно, смотрите раздел посвящённый срезам.) В противоположность, new([]int) возвращает указатель на новый, созданный, заполненный нулями срез, это указатель на значение nil среза.

Эти примеры показывают различие между new и make.

var p *[]int = new([]int)       // allocates slice structure; *p == nil; rarely useful
var v  []int = make([]int, 100) // the slice v now refers to a new array of 100 ints

// Unnecessarily complex:
var p *[]int = new([]int)
*p = make([]int, 100, 100)

// Idiomatic:
v := make([]int, 100)

Помните что make используется только для карт, срезов и каналов и не возвращают указатель. Для получения указателя в явном виде используйте new или возьмите указатель в явном виде.

Массивы

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

Какие основные отличия между обращением с массивами между языками Go и C:

  • Массивы значений. Присвоение одно массива другому копирует все элементы.
  • Если вы передаёте массив в функцию, то передаётся копия массива, а не указатель на него.
  • Размер массива является частью массива. Типы [10]int и [20]int разные.

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

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)  // Note the explicit address-of operator

Но данный стиль не подходит Go. Используйте срезы вместо массивов.

Срезы(Slices, слайсы)

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

Срез хранит ссылку на массив и поэтому если приравнять срез к другому срезу, то будет тот же массив. Если срез является аргументом функции, то изменения элементов в срезе будут видны вызывающему данному функцию, это аналогично передаче указателя на базовый массив. В функция Read может принимать в качестве аргумента срез, что равнозначно указателю на массив и длины массива; длина среза указывает верхний предел количество данных которые необходимо прочитать. В данном случае тип File пакета os имеет следующую сигнатуру метода Read:

func (f * File) Read(buf []byte) (n int, err error)

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

    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])  // Read one byte.
        if nbytes == 0 || e != nil {
            err = e
            break
        }
        n += nbytes
    }

Длина среза может меняться, пока не исчерпает размер внутреннего массива. С помощью встроенной функции cap можно узнать емкость среза, представляющий максимальную длину среза. В следующем примере рассматривается функция для добавления данных в срез. Если данные превышают ёмкость среза, то срез необходимо переопределить. Функция Append возвращает результирующий срез. Функция использует тот факт что использование len и cap допустимо, даже если у нас имеется нулевой срез nil - при этом возвращая 0.

func Append(slice, data []byte) []byte {
    l := len(slice)
    if l + len(data) > cap(slice) {  // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))* 2)
        // The copy function is predeclared and works for any slice type.
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:l+len(data)]
    for i, c := range data {
        slice[l+i] = c
    }
    return slice
}

TODO We must return the slice afterwards because, although Append can modify the elements of slice, the slice itself (the run-time data structure holding the pointer, length, and capacity) is passed by value. -

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

Двухмерные срезы

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

type Transform [3][3]float64  // A 3x3 array, really an array of arrays.
type LinesOfText [][]byte     // A slice of byte slices.

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

text := LinesOfText{
	[]byte("Now is the time"),
	[]byte("for all good gophers"),
	[]byte("to bring some fun to the party."),
}

Иногда необходимо создавать двухмерные срезы, к примеру при обработки пикселей. Есть 2 способа для этого:

  • Первый, создание каждого среза независимо
  • Второй, создание простого массива срезов. Наилучший способ выбирается в зависимости от программы. Если срез можно увеличивать или уменьшать, они должны быть независимы, для того чтобы избежать перезаписи новых строк. Если не требуется изменять размер, то наиболее эффективным был бы способ с создание одним их аллоцированием(инициализацией). Рассмотрим оба способа.
// Allocate the top-level slice.
picture := make([][]uint8, YSize) // One row per unit of y.
// Loop over the rows, allocating the slice for each row.
for i := range picture {
	picture[i] = make([]uint8, XSize)
}

с одним созданием:

// Allocate the top-level slice, the same as before.
picture := make([][]uint8, YSize) // One row per unit of y.
// Allocate one large slice to hold all the pixels.
pixels := make([]uint8, XSize*YSize) // Has type []uint8 even though picture is [][]uint8.
// Loop over the rows, slicing each row from the front of the remaining pixels slice.
for i := range picture {
	picture[i], pixels = pixels[:XSize], pixels[XSize:]
}

Карты(Maps)

Карты - это удобная и мощная встроенная структура данных, связывающая значение одного типа(ключ (key)) со значением другого типа (элемент (element) или значение (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] { // will be false if person is not in the map
    fmt.Println(person, "was at the meeting")
}

Иногда необходимо отличать отсутствие записи от нулевого значения. К примеру, есть ли запись для "UTC" или это пустая строка потому что отсутствует значение в карте? Для того чтобы отличить - Вы можете использовать множественное присвоение.

var seconds int
var ok bool
seconds, ok = timeZone[tz]

Очевидная причина называть данную идиому "запятая ок". В данном примере, если tz существует, то seconds будет иметь необходимое значение и ok будет true, но если не существует, то seconds будет иметь нулевое значение а 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")  // Now on Standard Time

Печать(Printing)

Форматированная печать в Go подобна стилю в языке C printf, но более богаче и более обобщенное. Необходимые функции расположены в пакете fmt и имеют назавания с большой буквы: fmt.Printf, fmt.Fprintf, fmt.Sprintf и так далее. Функции (Sprintf и другие) возвращают строку, а не заполняют предоставленный буфер.

Вам нет необходимости в создании форматировании строк, так как для каждой Printf, Fprintf and 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 знакомы.

Следующее расходится с реализацией на языке С. Первое, числовые форматы %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 (для "значений"); и результат будет одинаков как для Print так и для Println.

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

fmt.Printf("%v\n", timeZone)  // or just fmt.Println(timeZone)

который печатает следующий результат

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

Ключи карт могут быть напечатаны в любом порядке. При печати структуры, с аннотацией %+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, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

(На заметку: обратите внимание на амперсанды)

Для ссылок на строки подходит %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 должен иметь значение типа; этот пример использует указатель, т.к. они более эффективны и идиоматичны типу структуры.)

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

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // Error: will recur forever.
}

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

type MyString string
func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion.
}

Другой способ печати это допустить печать функции аргументов напрямую в другую функцию. Сигнатура Printf используется для типов ...interface{}, что допускает произвольное число аргументов, которые добавляются после формата format.

func Printf(format string, v ...interface{}) (n int, err error) {

TODO Within the function Printf, v acts like a variable of type []interface{} but if it is passed to another variadic function, it acts like a regular list of arguments. Here is the implementation of the function log.Println we used above. It passes its arguments directly to fmt.Sprintln for the actual formatting. -

// Println prints to the standard logger in the manner of fmt.Println.
func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))  // Output takes parameters (int, string)
}

Запись ... после v при вызове функции Sprintln объявляет компилятору о том что v является списком аргументов; с другой стороны v воспринимается как простой срез аргументов.

Если Вам необходимо большее количество информации, то смотрите документацию godoc в пакетеfmt.

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

func Min(a ...int) int {
    min := int(^uint(0) >> 1)  // largest 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.

Инициализация(Initialization)

Инициализация в языке Go более мощный инструмент нежели в языках С или С++. Даже сложные структуры можно инициализировать. Упорядочивание между инициализируемыми объектами разных пакетов, обрабатывается корректно.

Константы(Constants)

Константы в Go это просто константы. Они создаются во время компиляции даже если она определена в локальной функции и могут быть цифры, символы(руны), строки или булевый тип. Из-за ограничения времени компиляции, компилятор должен определять какие выражения могут быть константами. К примеру, выражение 1<<3 это константное выражение, в то время как выражение math.Sin(math.Pi/4) не является константой, так как вызывает функцию math.Sin требующую выполнения по время выполнения.

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

//{{code "/doc/progs/eff_bytesize.go" `/^type ByteSize/` `/^\)/`}}
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import "fmt"

type ByteSize float64

const (
	_           = iota // ignore first value by assigning to blank identifier
	KB ByteSize = 1 << (10 * iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

Использование функции String к пользовательским типам производить печать необходимым образом. TODO Although you'll see it most often applied to structs, this technique is also useful for scalar types such as floating-point types like ByteSize. -

//{{code "/doc/progs/eff_bytesize.go" `/^func.*ByteSize.*String/` `/^}/`}}

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 безопасна(не вызывается рекурсивно), не потому что происходит конвертирование, а потому что вызывается функция Sprintf с %f, который не строковый формат: Sprintf будет вызывать функцию String, функцию которой необходима строка и %f число с плавающей точкой.

Переменные(Variables)

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

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 not set")
    }
    if home == "" {
        home = "/home/" + user
    }
    if gopath == "" {
        gopath = home + "/go"
    }
    // gopath may be overridden by --gopath flag on command line.
    flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}

Методы(Methods)

Указатели или Значения

Как мы видели в примеры с ByteSize, функции может иметь имя типа (кроме указателей или интерфейсов) и приемник не обязательно должен иметь структуры.

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

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte {
    // Body exactly the same as the Append function defined above.
}

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

func (p *ByteSlice) Append(data []byte) {
    slice := *p
    // Body as above, without the return.
    *p = slice
}

На самом деле, мы можем сделать это ещё лучше. Если мы изменим функцию, то она будет выглядить как стандартная функция Write, то есть вот так,

func (p *ByteSlice) Write(data []byte) (n int, err error) {
    slice := *p
    // Again as above.
    *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 адресованная, поэтому мы можем вызвать функцию Write просто вызвав b.Write. Компилятор сам допишет (&b).Write за нас.

Кстати, идея использования Write на срезах байт наиважнейшая для реализации bytes.Buffer.

Интерфейсы и другие типы

Интерфейсы

Интерфейсы в Go позволяют создать особое поведения для объектов: Если нечто может делать это , то это можно использовать здесь. Мы уже это встречали в простых примерах, когда реализовывали функцию String для печати, в то время как Fprintf может выдавать на печать другое с методом Write. Интерфейсы с одним или двумя функциями свойственны в языке Go, как io.Writer реализующий Write.

Любой тип может реализовывать множество интерфейсов. К примеру, коллекции могут быть отсортированы с помощью функций из пакета sort, если она реализует sort.Interface, который состоит из Len(), Less(i, j int) bool, и Swap(i, j int) и это может задать собственный формат. Рассмотрим пример Sequence

//{{code "/doc/progs/eff_sequence.go" `/^type/` "$"}}
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"fmt"
	"sort"
)

func main() {
	seq := Sequence{6, 2, -1, 44, 16}
	sort.Sort(seq)
	fmt.Println(seq)
}

type Sequence []int

// Methods required by 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]
}

// Method for printing - sorts the elements before printing.
func (s Sequence) String() string {
	sort.Sort(s)
	str := "["
	for i, elem := range s {
		if i > 0 {
			str += " "
		}
		str += fmt.Sprint(elem)
	}
	return str + "]"
}

Преобразование (Conversions)

Функция String работает с Sequence и Sprint уже работает со срезами. Мы может распространить данный эффект, если конвертируем Sequence на []int до вызова Sprint.

func (s Sequence) String() string {
    sort.Sort(s)
    return fmt.Sprint([]int(s))
}

Это функция другой пример техники конвертирования для вызова Sprintf безопасно для функции String. Так как два типа (Sequence и []int) одинаковы, то мы можем игнорировать имя типа, это допустимое конвертирование между ними. При конвертации не происходит создание нового значения, это временная замена существующего значения на новый тип. (При других допустимых конвертациях, к примеру из целого числа в число с плавающей точкой, происходит создание нового значения.)

Это идиоматично в программе Go - конвертация типа позволяет получить доступ к другим функциям. К примеру, мы можем использовать существующий тип sort.IntSlice:

type Sequence []int

// Method for printing - sorts the elements before printing
func (s Sequence) String() string {
    sort.IntSlice(s).Sort()
    return fmt.Sprint([]int(s))
}

Теперь, наш Sequence реализует множество интерфейсов (сортировка и печать), мы можем использовать множество типов (Sequence, sort.IntSlice and []int), которые выполняют определенную часть работ. Это не типично в использовании, но эффективно.

Конвертация интерфейсов и привязка типов

Переключатель типов(Type switches) является одной из форм конвертации: на основе интерфейса и переключателя для каждого элемента, в некотором смысле преобразует тип в элемент переключателя. Это простой вариант как в коде fmt.Printf конвертирует значение в строку, используя переключатель типа. И если это уже строка, мы хотим чтобы фактическое значение происходило по его интерфейсу, но в случаи если она имеет функцию String, то хотим чтобы в результате вызывалась именно она.

type Stringer interface {
    String() string
}

var value interface{} // Value provided by caller.
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

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

Что если, мы будет беспокоиться лишь об одном типе? Если мы знаем что значение имеет тип string и мы хотим вытащить только его? Можно сделать переключатель только с одним типом, но это будет type assertion. И type assertion берет значение интерфейса и переводит из его значения в его тип. Заимствование типа из открытия type switch, но переводит тип с помощью ключевого слова type:

value.(typeName)

и в результате у нас значение со статическим типом typeName. Этот тип должен быть конкретным типом имеющим интерфейс, или второй тип интерфейса - это тип в который может быть сконвертирован. Если мы знаем что это строка в значении, то мы можем записать:

str := value.(string)

Но если выясниться, что значение хранит не строку, то программа будет обрушена во время работы a run-time error. Для защиты от этого используется идиома запятая, ок "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 будет всё ещё существовать и будет типом строка, но будет иметь нулевое значение - пустую строку.

Для иллюстрации, используем условие if-else как эквивалент переключателя типов type switch в начале этого раздела.

if str, ok := value.(string); ok {
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

Общее(Generality)

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

В таких случаях, конструктор может возвращать значение интерфейса, что лучше чем реализованный тип. Для примера, в библиотеках хэш hash оба конструктора 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(src, dst []byte)
    Decrypt(src, dst []byte)
}

type Stream interface {
    XORKeyStream(dst, src []byte)
}

Определение режима счётчика потока counter mode (CTR) stream, который превращает блоки шифрования в поток шифрования, обратите внимание, что шифрование блоков абстрагировано:

// NewCTR returns a Stream that encrypts/decrypts using the given Block in
// counter mode. The length of iv must be the same as the Block's block size.
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(Запрос) это структура хранящая информацию о запросе от клиента.

Для упрощения, давайте игнорировать POSTs и предположим что HTTP запросы всегда используют GETs; Это упрощение не влияет на способ настройки обработчика handlers. К примеру следующий код показывает полный обработчик для подсчета количества раз показа данной страницы.

// Simple counter server.
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.) Для справки, следующий код показывает как присоединить сервер к узлу в URL tree.

import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)

Но зачем использовать структуру для Counter? Все что нам необходимо - это целое число. (Для получателя receiver необходим указатель, тогда инкремент будет виден для вызывающего)

// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    *ctr++
    fmt.Fprintf(w, "counter = %d\n", *ctr)
}

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

// A channel that sends a notification on each visit.
// (Probably want the channel to be buffered.)
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 есть следующий код:

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers.  If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(c, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
    f(w, req)
}

Это тип HandlerFunc с методом ServeHTTP, поэтому значения данного типа может служит для запросов HTTP. Посмотрим на реализацию метода: receiver это функция, f, и метод называется f. Это может показаться странным, но это ничем не отличается от работы с каналами и метод бы отсылал на канал.

Для создания ArgServer как HTTP сервера, вначале мы изменим корректную сигнатуру.

// Argument server.
func ArgServer(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}

Сейчас, ArgServer имеет ту же сигнатуру как HandlerFunc, поэтому его можно конвертировать в этот тип для доступа к его методам, просто как сконвертировать Sequence в IntSlice для доступа к IntSlice.Sort. Код для настройки лаконичен:

http.Handle("/args", http.HandlerFunc(ArgServer))

Когда кто-то посещает страницу /args, обработчик handler устанавливает страницу со значением ArgServer и типом HandlerFunc. Сервер HTTP будет вызывать метод ServeHTTP данного типа с получателем ArgServer, который будет вызывать ArgServer через вызов f(c, req) внутри HandlerFunc.ServeHTTP. Вследствии этого аргументы будут отображены.

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

Пустой идентификатор (The blank identifier _)

Мы уже упоминали пустой идентификатор пару раз, в разделах о циклах for range и картах maps. Пустой идентификатор может быть назначен или объявлен для любого типа, значение при этом отбрасывается. Это чем то похоже на запись в Unix файл в /dev/null: Это значение только на запись, где переменна необходима, но значение не важно. Есть дополнительные способы использования.

Пустой дентификатор в множественном присваении (_)

Использование пустого идентификатора в цикле for range является лишь одним случаем применения в общей картине множественного присваения.

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

if _, err := os.Stat(path); os.IsNotExist(err) {
	fmt.Printf("%s does not exist\n", path)
}

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

// Bad! This code will crash if path does not exist.
fi, _ := os.Stat(path)
if fi.IsDir() {
    fmt.Printf("%s is a directory\n", path)
}

Неиспользуемое импортирование и значения

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

Это полунаписанная программа имеет два неиспользуемых импорта (fmt и io) и не используемую переменную (fd), и она не проходит компиляцию, но было бы хорошо если бы можно было увидеть, что код корректен.

///{{code "/doc/progs/eff_unused1.go" `/package/` `$`}}
package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func main() {
	fd, err := os.Open("test.go")
	if err != nil {
		log.Fatal(err)
	}
	// TODO: use fd.
}

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

//{{code "/doc/progs/eff_unused2.go" `/package/` `$`}}
package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

var _ = fmt.Printf // For debugging; delete when done.
var _ io.Reader    // For debugging; delete when done.

func main() {
	fd, err := os.Open("test.go")
	if err != nil {
		log.Fatal(err)
	}
	// TODO: use fd.
	_ = fd
}

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

Импортирование для побочного эффекта (Import for side effect)

Неиспользуемые импорты, как например fmt и io в предедущем примере, в конечном счете должны быть удалены: пустое задание должно определять что код в процессе разработки. Но иногда, используется импортирование пакета только для создания побочного влияния, без какого либо явного использования. К примеру, для функции init в пакете net/http/pprof регистрирует HTTP обработчики для обеспечения отладочной информацией. Он имеет экспортированный API, но большинству клиентов необходима только регистрация обработчиков и получение доступа к данным через веб-страницу. Только для импортирования пакета с этим побочным эффектом, переименовывают пакет в пустой идентификатор:

import _ "net/http/pprof"

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

Проверка интерфейса (Interface checks)

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

К примеру, передавая *os.File в функцию ожидающая io.Reader не будет скомпилировано, так как *os.File не реализует интерфейс io.Reader.

Хотя все же некоторые проверки интерфейсов происходят во время выполнения. Один из примеров в пакете encoding/json, который определяет интерфейс Marshaler. Когда JSON encoder принимает значение, которое реализует этот интерфейс, encoder вызывает функцию упаковщик значений для преобразования в JSON, в отличии от стандартного преобразования.

Encoder проверяет эти свойства во время работы:

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

var _ json.Marshaler = (*RawMessage)(nil)

в этой деклорации, присвоение с конвертацией *RawMessage к Marshaler требует чтобы, *RawMessage реализовывал Marshaler и данная проверка будет производиться во время компиляции. В случаи если интерфейс json.Marshaler, этот пакет не будет компилироваться и мы будем знать об обновлении.

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

Вложение (Embedding)

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

Вложение интерфейса необычно простое. Мы уже упомянали об интерфейсах io.Reader and 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. Мы может указать io.ReadWriter перечислением двух методов в явном виде, но проще и более запомяющим будет встраивание двух интерфейсов в одну новую форму, вот так:

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

Это выглядит следующим образом: ReadWriter может делать все что делает Reader и что делает Writer. Это объединение встраивания интерфейсов (которые не имеют пересечений в методах). Только интерфейсы могут встраивать интерфейсы.

Аналогичная идея используется для структур, но с большим количеством последствий. Пакет bufio имеет две структуры типов - bufio.Reader и bufio.Writer, каждая из которых реализует аналогичные интерфейсы как в пакете io. И bufio также реализует буфферизованное чтение/запись, которое объединяет чтение и запись в одну структуру с использованием вложения: этот список типов структур, но не давая имена полям.

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

Вложение указателей элементов в структуры и конечно должно быть инициализированно необходимой структурой до его использования. Структура ReadWriter может быть записана так:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

TODO but then to promote the methods of the fields and to satisfy the io interfaces, we would also need to provide forwarding methods, like this: -

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.

Это важное отличие вложения от подклассов. Когда мы вкладываем тип, методы этого типа становяться методами внешнего типа, но для получателя они вызываються как встроенные типы, а не внешние. В нашем примере, когда метод Read из bufio.ReadWriter вызывается, он и вызываються также как описано выше; получатель поля reader из ReadWriter, является самим ReadWriter.

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

type Job struct {
    Command string
    *log.Logger
}

Тип Job сейчас имеет Log, Logf и другие методы *log.Logger. Мы могли бы дать имя для Logger, конечно же, но в этом нет необходимости. И сейчас, мы можем логировать Job:

job.Log("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 в нашей структуре ReaderWriter. При этом нам необходим доступ к *log.Logger в Job переменной job, мы можем написать job.Logger, что полезно если мы хотим уточнить методы Logger.

func (job *Job) Logf(format string, args ...interface{}) {
    job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

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

Первое, поля или метод X скрывает любой иной элемент X в более глубокой части вложенного типа. Если log.Logger содержит поле или метод под названием Command, то поле Command в Job будет преобладать над ним.

Во-вторых, если есть одинаковые имена на том же уровне вложенности, это как правило ошибка и было бы ошибочно вставлять log.Logger, если структура Job имеет другое вложенное поле или метод с названием Logger. Однако, если дублированные имена никогда не встречается в программе вне определённого типа, то это нормально. Это защищает от изменения типов вложенности за его пределами; и это не проблема, если добавлено поле вступающее в конфликт с другим полем в другом подтипе, если ни одно из полей не используется.

Согласованность, параллельная обработка, параллельное выполнение (Concurrency)

Распределение памяти по сообщениям (Share by communicating)

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

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

В языке Go поддерживается другой подход, в котором общие переменные shared values передаються через каналы, по сути, никогда активно не распределяется по исполняемым потокам. Только одна го-рутина(goroutine) имеет доступ к переменной в любой момент. Перенос данных не происходит по конструкции языка. Для того чтобы способствовать данному стилю мышления используеться лозунг:

Do not communicate by sharing memory; instead, share memory by communicating.

Не общайтесь с распределением памяти; Вместо того чтобы распределять память по коммуникациям.

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

Один из способов думать об этой модели как для типичных однопоточных программ запущенных на одном процессоре CPU. И нет необходимости в синхронизации примитивов. Для запуска следующего экземпляра, нет необходимостив синхронизации. Сейчас рассмотрим два способа коммуникации; Если коммуникация синхронна, то все также не требуется в дополнительной синхронизациию. К примеру, Unix pipelines великолепно используют эту модель. Хотя подход языка Go для организации параллельных процессов берет начало в Hoare's Communicating Sequential Processes (CSP), он также может рассматриваться как обобщение безопасности типов Unix pipes.

Го-рутины (Goroutines)

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

Горутины распределяются на несколько покотов OS, и если один заблокируются, например из-за ожидания I/O, другие продолжат работу. Их дизайн скрывает много сложностей по создание потоков и их управлению.

Префикс go у функции или метода запускает новую горутину. Когда вызов закончен, горутина выходит, молча. (Этот эффект похож на команду Unix с нотацией & означающая запуск команды в фоновом режиме.)

go list.Sort()  // run list.Sort concurrently; don't wait for it.

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

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // Note the parentheses - must call the function.
}

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

Эти примеры не очень практичны, так как функции не имеют сигнализировать о своем завершении. Для этого у нас есть каналы.

Каналы (Channels)

Каналы, как и карты(map) выделяются в памяти с помощью make и полученное значение является ссылкой на изначальную структуру данных. Если задан необязательный целый параметр, то он указывает на размер буффера в канале. По умолчанию, значение нулевое, как для небуфферезованного или синхронного канала.

ci := make(chan int)            // unbuffered channel of integers
cj := make(chan int, 0)         // unbuffered channel of integers
cs := make(chan *os.File, 100)  // buffered channel of pointers to Files

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

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

c := make(chan int)  // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
    list.Sort()
    c <- 1  // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c   // Wait for sort to finish; discard sent value.

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

TODO A buffered channel can be used like a semaphore, for instance to limit throughput. In this example, incoming requests are passed to handle, which sends a value into the channel, processes the request, and then receives a value from the channel to ready the "semaphore" for the next consumer. The capacity of the channel buffer limits the number of simultaneous calls to process. -

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // Don't wait for handle to finish.
    }
}

TODO Once MaxOutstanding handlers are executing process, any more will block trying to send into the filled channel buffer, until one of the existing handlers finishes and receives from the buffer. -

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

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func() {
            process(req) // Buggy; see explanation below.
            <-sem
        }()
    }
}

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

func Serve(queue chan *Request) {
    for req := range queue {
        sem <- 1
        go func(req *Request) {
            process(req)
            <-sem
        }(req)
    }
}

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

func Serve(queue chan *Request) {
    for req := range queue {
        req := req // Create new instance of req for the goroutine.
        sem <- 1
        go func() {
            process(req)
            <-sem
        }()
    }
}

Может кажется странным, писать:

req := req

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

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

Функция Serve также принимает канал, на который посылается об окончании; после запуска горутины блокируют получающих в этот канал.

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

Канал каналов (Channels of channels)

Одно из важных свойств 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)}
// Send request
clientRequests <- request
// Wait for response.
fmt.Printf("answer: %d\n", <-request.resultChan)

На стороне сервера, функция обработчик это единственное что меняется.

func handle(queue chan *Request) {
    for req := range queue {
        req.resultChan <- req.f(req.args)
    }
}

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

Параллелелизм (Parallelization)

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

К примеру, у нас есть дорогая операция выполнения на векторе элементов и эти операции могут выполнять независимо, то вот идеализированный пример.

type Vector []float64

// Apply the operation to v[i], v[i+1] ... up to 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    // signal that this piece is done
}

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

const numCPU = 4 // number of CPU cores

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // Buffering optional but sensible.
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // Drain the channel.
    for i := 0; i < numCPU; i++ {
        <-c    // wait for one task to complete
    }
    // All done.
}

Вместо того, чтобы создать постоянное значение для numCPU, мы можем задать во время выполнения необходимое значение. Функция runtime.NumCPU возвращает количество ядер CPU в машине, тогда мы должны записать:

var numCPU = runtime.NumCPU()

Есть также такая функция runtime.GOMAXPROCS, которая возвращает заданное пользователем количество ядер, которая программа Go может использовать. По умолчанию значение runtime.NumCPU, но может быть переопределен путем установки в среде с тем же именем или вызовом функции с положительным числом. Вызов с нулевым значением запрашивает значение. Поэтому если мы хотим выполнить запрос ресурсов пользователя, мы должны написать

var numCPU = runtime.GOMAXPROCS(0)

Будьте уверены, чтобы не путать идеи параллельно-структурированной(concurrency—structuring) программы как независимо исполняемых компонентов и паралелльно-выполняемые вычисления(parallelism—executing) для эффективности на нескольких процессорах. Хотя особенности concurrency в языке Go могут решить некоторые проблемы легко с использованием структур параллельного вычисления, Go является concurrent языком, не параллельным и не все проблемы параллелизма подходят модели Go. Для обсуждения различий, смотрите следующий блог.

Текущий буффер (A leaky buffer)

Инструменты конкарентси программирования позволяют для неконкаренси идей быть нагляднее. Вот пример из пакета RPC. Цикл клиента горутины принимает данные из нескольких источников, возможно из сети. Для того чтобы избежать выделения и освобождения буферов, он пустой список и использует буферизованный канал для его представления. Если канал пуст, то выделяется новый буфер. После того, как буфер готов, он высылает на сервер на serverChan.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not.
        select {
        case b = <-freeList:
            // Got one; nothing more to do.
        default:
            // None free, so allocate a new one.
            b = new(Buffer)
        }
        load(b)              // Read next message from the net.
        serverChan <- b      // Send to server.
    }
}

Цикл сервера принимает каждое сообщение из клиента, обрабатывает его и возвращает буфер на пустое список.

func server() {
    for {
        b := <-serverChan    // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
        case freeList <- b:
            // Buffer on free list; nothing more to do.
        default:
            // Free list full, just carry on.
        }
    }
}

Клиент пытаеться получить буфер из freeList; если ни один не доступен, он выделяется новые. Посылка от сервера в freeList подставляется назад b в свободный список, если список не полон, и в этом случаи буфер сбрасывается, чтобы утилизироваться сборщиком мусора.

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

Ошибки (Errors)

Библиотеки подпрограмм часто должны возвращать какой-то признак ошибки для вызывающего. Как уже упомяналось ранее, множественные значения в Go могут легко возвращать подробное описание ошибки вместе с нормальным возвращением значения. Использование данной особенности Go для возвращения детального описания ошибки является хорошим стилем. Например, как вы увидете os.Open при неудаче не просто возвращает указатель на nil, он также возвращает значение ошибки, описывающей что пошло не так.

В соответствии с соглашением, ошибки имеют тип error, простой встроенный интерфейс.

type error interface {
    Error() string
}

Библиотека записи может реализовать данный интерфейс с богатой моделью покрытия, что позволяет не только увидеть ошибку, но и также обеспечить некий контекст. Как уже отмечалось, наряду с обычным *os.File возвращением значения, os.Open также возвращает значение ошибки. Если файл будет успешно открыт то значение ошибки будет nil, но когда есть проблема, то будет передана os.PathError:

// PathError records an error and the operation and
// file path that caused it.
type PathError struct {
    Op string    // "open", "unlink", etc.
    Path string  // The associated file.
    Err error    // Returned by the system call.
}

func (e * PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

Ошибка Error в PathError сгенерирует строку как эта:

open /etc/passwx: no such file or directory

Такая ошибка, которая включает имя проблемного файла, операции, ошибка операционной системы и т.д., полезная, даже если напечатать далеко от вызова; это гораздо полезнее, что просто запись "файл или папка не найдены".

Если это возможно, то строка ошибки должна определять происхождение, например, при наличии префикса имен операции или пакета, который вызвал ошибку. Например, в пакете image при ошибки декодирования представлена от неизвестного формата: "image: unknown format".

Вызывающие, которые заботятся о точности ошибки, могут использовать переключатель типов type switch или type assertion для того специфицирования ошибок и получения большего количества деталей. Для PathErrors это означает включения изучения внутренних полей 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()  // Recover some space.
        continue
    }
    return
}

Здесь вторая проверка if это ещё другой тип type assertion. Если это не удается, то ok будет false и значение e будет nil. Если это удается, то ok будет true, который означает, что имеет тип *os.PathError, и затем когда e, который мы можем рассматривать для более подробной информации об ошибке.

Паника (Panic)

Обычный способ сообщить об ошибке к абоненту, это вернуть error, в качестве дополнительного возвращаемого значения. Канонический метод Read является хорошим примером, который возвращает количество байт и error. Но что если ошибка невосстановимая? Иногда программа просто не может продолжать работать.

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

// A toy implementation of cube root using Newton's method.
func CubeRoot(x float64) float64 {
    z := x/3   // Arbitrary initial value
    for i := 0; i < 1e6; i++ {
        prevz := z
        z -= (z*z*z-x) / (3*z*z)
        if veryClose(z, prevz) {
            return z
        }
    }
    // A million iterations has not converged; something is wrong.
    panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}

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

var user = os.Getenv("USER")

func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

Восстановление (Recover)

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

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

Одно 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("work failed:", err)
        }
    }()
    do(work)
}

В этом примере, если будет вызвана паника в do(work), то результат будет залогирован и горутина закончит работу без препятствия выполнения для других. Там нет необходимости делать что то дополнительно при отсроченном выполнении; вызывание recover обрабатывает сосотяние полностью.

Так как recover всегда возвращает nil, если вызывалась из отложенной функции, отложенный код может вызывать библиотеку функций, которые сами используют panic и recover без сбоя. К примеру, отложенная функция в safelyDo может вызвать функцию логирования до вызова recover, и этот код логирования будет работать не зависимо от состоянии паники.

С помощью данного шаблона восстановления , функция do (и все что он вызывает) может выйти из любой ситуации вызовом panic. Мы можеи использовать данную идею для простой обработки ошибок в сложной программе. Давайте взглянем на идеализированную версию пакета regexp, которая сообщает об ошибки с помощью panic с типом локальной ошибки. Это определение Error, в методе error и функции Compile.

// Error is the type of a parse error; it satisfies the error interface.
type Error string
func (e Error) Error() string {
    return string(e)
}

// error is a method of *Regexp that reports parsing errors by
// panicking with an Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile returns a parsed representation of the regular expression.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse will panic if there is a parse error.
    defer func() {
        if e := recover(); e != nil {
            regexp = nil    // Clear return value.
            err = e.(Error) // Will re-panic if not a parse error.
        }
    }()
    return regexp.doParse(str), nil
}

Если происходит паника в doParse, то блок восстановления будет устанавливать значение nil отложенная функция может модифицировать имя возвращаемых значений. Затем он проверяет, значение err, синтаксическая ошибка имеет ликальный тип Error. Если этого не произойдет, то это приведет к ошибке во время выполнения и будет раскручивать стек. Эта проверка означает что если происходит что-то необжиданное, как выход за пределы индексирования, код будет прерван даже при использовании panic и recover для обработки ошибок.

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

if pos == 0 {
    re.error("'*' illegal at start of expression")
}

Данный шаблон полезный в рамках только одного пакета. Превращение Parse внутреннего вызова panic в значение error, что позволяет на выставлять panics для клиента. Это хорошее правило, чтобы ему следовать.

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

Веб-сервер

Давайте закончим разработкой веб-вервера на Go. Google предоставлен сервис по адресу http://chart.apis.google.com с автоматическим форматированием данных графиков и диаграмм. Это трудно использовать в интерактивном режиме, но Вам необходимо добавить URL в качестве запроса. Здесь программа использует приятный простой интерфейс с одной формой для данных: для небольшого кусочка текста, который вызывает серсер диаграмм для создания QC кода, кодируя текст в матрицу пиксел. Эта картинка можно быть сфотографированна с помощью камеры телефона и интерпретирована, к примеру, как URL, экономя тем самым его набор на маленькой клавиатуре телефона.

Вот программа полностью с последующими пояснениями.

//{{code "/doc/progs/eff_qr.go" `/package/` `$`}}
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

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>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<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), имеються двойные скобки разделители обозначающие действия шаблона. Участок от {{html "{{if .}}"}} до {{html "{{end}}"}} выполняются только если значения текущей элемента данных, вызывают . (точка) не пустая. То есть, если строка пуста, то данный учаток шаблона игнорируется.

Два примере кода {{html "{{.}}"}} предназначены для показа существующих данных в запросе шаблона на веб странице. Пакет шаблонов HTML автоматически обеспечивает соответсвие, поэтому текст является безопасным для отображения.

Остальные строки шаблона, просто строки HTML , которые показываются при загрузки страницы. Если это слишком быстрое объяеснение, то смотрите документацию о пакете шаблонов для большего понимания.

В результате у Вас есть: полезный пример веб сервера из нескольких строк кода с управлением данных текста HTML. Язые Go достаточно мощный для создание много чего интересного за несколько строк.

About

Перевод - Эффективный Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • HTML 100.0%