четверг, 28 февраля 2019 г.

Go Code Review Comments: Время жизни goroutine

Когда вы порождаете go-процедуры, делайте четким и ясным время, когда они завершаются (или не завершаются).

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

Даже когда go-процедуры не приводят к утечкам памяти, оставление их в подвешенном состоянии, когда они больше не нужны, может вызвать другие тонкие и трудно диагностируемые проблемы. Отправка panic по закрытым каналам. Изменение находящихся в использовании вводимых данных «после того, как результат не нужен», может привести к "гонкам за данные" (data races). А оставление go-процедур в подвешенном состоянии в течение сколь угодно долгого времени может привести к непредсказуемому использованию памяти.

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


Читайте также:


Go Code Review Comments: комментарии к документу, panic, строки ошибок, примеры

Комментарии к документу (Doc comments)

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

Не используйте panic напрасно

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

Строки ошибок

Строки ошибок не должны начинаться с заглавной буквы (если только они не начинаются с собственных имен или аббревиатур) или заканчиваться пунктуацией, поскольку они обычно печатаются в другом контексте. То есть используйте fmt.Errorf("something bad") , а не fmt.Errorf("Something bad"), так что log.Printf("Reading %s: %v", filename, err) форматируется без ложной заглавной буквы в середине сообщения. Это не относится к сообщениям журнала (logging), которые ориентированы на использование сообщения как отдельной строки и не объединяются внутри других сообщений.

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

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

Примеры

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


Читайте также:


среда, 27 февраля 2019 г.

Go Code Review Comments: Пакет Crypto Rand

Не используйте пакет math/rand для генерации ключей, даже одноразовых. Без заданного семени, генератор полностью предсказуем. Заданный с семенем time.Nanoseconds(), присутствует всего несколько бит энтропии. Вместо этого используйте crypto/rand Reader, и если вам нужен текст, напечатайте в шестнадцатеричной или base64 кодировке:

import (
    "crypto/rand"
    // "encoding/base64"
    // "encoding/hex"
    "fmt"
)

func Key() string {
    buf := make([]byte, 16)
    _, err := rand.Read(buf)
    if err != nil {
        // потеря случайности, никогда не должна произойти
        panic(err)  
    }
    return fmt.Sprintf("%x", buf)
    // or hex.EncodeToString(buf)
    // or base64.StdEncoding.EncodeToString(buf)
}


Читайте также:


суббота, 23 февраля 2019 г.

Go Code Review Comments: определение пустых срезов

При определении пустого среза (slice) предпочтительней использовать

var t []string

вместо

t := []string{}

В первом случае объявляется нулевое значение среза (nil срез), в то время как во втором определяется не нулевое значение среза, но срез нулевой длины. Они функционально эквивалентны - их len и cap равны нулю, но nil срез является предпочтительным стилем.

Обратите внимание, что существуют ограниченные обстоятельства, когда предпочтителен срез не nil, а нулевой длины, например, при кодировании объектов JSON (нулевой срез кодируется в nil, а []string{} кодируется в массив JSON []).

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


Читайте также:


Go Code Review Comments: копирование

Чтобы избежать неожиданного псевдонима, будьте осторожны при копировании структуры из другого пакета. Например, тип bytes.Buffer содержит []byte срез (slice) и, в качестве оптимизации для небольших строк, небольшой байтовый массив (byte array), к которому может обращаться этот срез. Если вы копируете Buffer, срез в копии может иметь псевдоним (alias) массива в оригинале, вызывая неожиданные эффекты при последующих вызовах методов.

В целом, не копируйте значение типа T, если его методы связаны с типом указателя *T.


Читайте также:


Go Code Review Comments: контексты

Значения типа context.Context содержат учетные данные безопасности, информацию о трассировке, крайние сроки и сигналы отмены через API и границы процесса. Программы Go передают Контексты явно по всей цепочке вызовов функций от входящих RPC и HTTP-запросов до исходящих запросов.

Большинство функций, которые используют контекст, должны принять его как свой первый параметр:

func F (ctx context.Context, / * другие аргументы * /) {}

Функция, которая никогда не зависит от запроса, может использовать context.Background(), кроме ошибки при передаче Context, даже если вы считаете, что вам это не нужно. По умолчанию - передавайте Context; используйте context.Background() только в том случае, если у вас есть веская причина, по которой альтернатива является ошибкой.

Не добавляйте член Context в тип структуры; вместо этого добавьте параметр ctx к каждому методу того типа, который должен передать его. Единственное исключение - для методов, чья подпись должна соответствовать интерфейсу в стандартной библиотеке или в сторонней библиотеке.

Не создавайте пользовательские типы контекста и не используйте интерфейсы, отличные от контекста, в сигнатурах функций.

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

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


Читайте также:


Go Code Review Comments: комментируйте предложениями

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

// Request представляет запрос на выполнение команды.
type Request struct { ...

// Encode записывает JSON кодированый req в w.
func Encode(w io.Writer, req *Request) { ...


Читайте также:


Go Code Review Comments: введение, gofmt

В серии постов Go Code Review Comments (комментарии просмотра(оценки) Go кода) будут рассмотрены практики написания и работы с Go кодом рекомендуемые для использования в своих проектах. Данные рекомендации были собраны разработчиками Go сообщества и включают советы помимо тех что перечислены в постах Эффективный Go.

Gofmt

Запустите gofmt для своего кода, чтобы автоматически исправить большинство механических проблем со стилем. Почти весь существующий Go код отформатирован с использованием gofmt. В серии постов Go Code Review Comments рассматриваются немеханические вопросы стиля.


Читайте также:


Go FAQ: Зачем создавать коллекцию мусора? Не слишком ли это дорогая операция?

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

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

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

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

Текущая реализация представляет собой сборщик меток и разверток (mark-and-sweep collector). Если машина является многопроцессорной, коллектор работает на отдельном процессорном ядре параллельно с основной программой. Большая работа над коллектором в последние годы сократила время пауз, часто с точностью до миллисекунды, даже для больших куч (heaps), почти исключая одно из основных возражений против сбора мусора в сетевых серверах. Продолжается работа по уточнению алгоритма, дальнейшему снижению накладных расходов и задержки.

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


Читайте также:


Go FAQ: Почему в Go есть фигурные скобки, но нет точек с запятой? И почему я не могу поставить открытие фигурной скобки на следующей строке?

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

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


Читайте также:


пятница, 22 февраля 2019 г.

Go FAQ: Почему ++ и -- в Go являются операторами, а не выражениями? И почему постфикс, а не префикс?

Без арифметики указателей, удобство префикс и постфикс операторов приращения пропадает. Удаление их из иерархии выражений в целом, упростило синтаксис выражений и грязные проблемы оценки порядка ++ и -- (например, f(i++) и p[i] = q[++i]) также устранены. Упрощение значительное. Что касается постфикса и префикса, то все будет работать нормально, но постфиксная версия более традиционна; настойчивое требование префикса возникло с STL, библиотеки для языка, имя которого, по иронии судьбы, содержит постфиксный прирост (C++).


Читайте также:


Go FAQ: Почему в Go нет арифметики с указателями?

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


Читайте также:


Go FAQ: Почему в Go декларации пишутся в обратном направлении?

Декларации в Go выглядят как написанные в обратном направлении только если вы привыкли к C. В C, идея заключается в том, что переменная объявляется как выражение, обозначающее ее тип, что является хорошей идеей, но грамматики типа и выражения не очень хорошо сочетаются и результаты могут сбивать с толку; рассмотрим указатели функции. Go в основном разделяет синтаксис выражений и типов, что упрощает дело (префикс * для указателей является исключением, подтверждающим правило). В С, декларация

int* a, b;

объявляет a указателем, но не b; в Go

var a, b *int

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

var a uint64 = 1

имеет тот же эффект, что и

a := uint64(1)

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


Читайте также:


четверг, 21 февраля 2019 г.

Go FAQ: Можно ли прекратить жалобы компилятора на неиспользованную переменную/импорт?

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

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

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

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

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

import "unused"

// Это объявление помечает импорт как использованный 
// путем ссылки на элемент из пакета.
var _ = unused.Item  // TODO: Удалить перед коммитом!

func main() {
    debugData := debug.Profile()
    _ = debugData // Использовано только в ходе отладки.
    ....
}

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


Читайте также:


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

Компоновщик (linker) в цепочке инструментов gc создает статически связанные бинарные файлы по умолчанию. Все бинарные файлы Go, следовательно, включают Go runtime, наряду с информацией о runtime типах, необходимой для поддержки динамической проверки типов, отражения (reflection) и даже для stack trace'ов времени паники (panic-time).

Простая программа на языке C "hello, world", скомпилированная и статически связанная с использованием gcc в Linux составляет около 750 кБ, включая реализацию printf. Эквивалентная Go программа с использованием fmt.Printf весит пару мегабайт, но она включает более мощную поддержку во время выполнения (run-time support), а также информация о типах и отладочную информацию.

Программа на Go, скомпилированная с помощью gc, может быть связана с флагом -ldflags=-w для отключения генерации DWARF, удаляя отладочную информацию из бинарного файла, но без других потерь функциональности. Это может существенно уменьшить размер бинарного файла.


Читайте также:


Go FAQ: Как реализована runtime поддержка в Go?

Опять же из-за проблем с загрузкой, упоминавшихся в предыдущем посте, runtime код изначально был написан в основном на C (с небольшим количеством ассемблера), но с тех пор он был переведен на Go (за исключением некоторых битов ассемблера). Поддержка Gccgo runtime использует glibc. Компилятор gccgo реализует программы с использованием техники, называемой сегментированными стеками, поддерживаемой недавними изменениями в gold linker. Gollvm аналогичным образом построен на соответствующей LLVM инфраструктуре.


Читайте также:


Go FAQ: Какие компиляторы используются для создания самих Go компиляторов?

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

Компилятор по умолчанию, gc, включен в распространяемый Go как часть поддержки go команды. Gc изначально был написан на C из-за трудностей с загрузкой - понадобился компилятор Go, чтобы настроить среду Go. Но дела пошли дальше, и после выпуска Go 1.5 компилятор был Go программой. Компилятор был преобразован из C в Go с использованием инструментов автоматического перевода. Таким образом, компилятор теперь "самодостаточен", что означает, что приходится сталкиваться с проблемой начальной загрузки. Решение состоит в том, чтобы иметь уже работающую установку Go, как обычно делают с работающей установкой Си.

Gc написан на Go с парсером рекурсивного спуска и использует собственный загрузчик, также написанный на Go, но на основе загрузчика Plan 9 для создания бинарных файлов ELF/Mach-O/PE.

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

Компилятор Gccgo - это интерфейс, написанный на C++ с парсером рекурсивного спуска, соединенным с стандартным бекендом GCC.

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

Хотя gc не использует их, нативный лексер и парсер доступны в пакете go, а также есть встроенная программа проверки типов.


Читайте также:


вторник, 19 февраля 2019 г.

Go FAQ: Где вспомогательные функции для тестирования в Go?

Стандартный пакет testing Go позволяет легко создавать юнит-тесты, но в нем нет функций, предоставляемых в средах тестирования других языков, такие как функции утверждений (assert). Правильная обработка ошибок означает запуск других тестов после того, как один из них потерпел неудачу, поэтому человек, отлаживающий ошибку, получает полную картину того, что произошло не так. Для теста полезней сообщить, что isPrime дает неправильный ответ для 2, 3, 5 и 7 (или для 2, 4, 8 и 16), чем сообщить, что isPrime выдает неверный ответ для 2 и, следовательно, больше тестов не проводилось. Программист, который получает тестовый сбой, возможно, не знаком с кодом, который терпит неудачу. Время, потраченное на написание хорошего сообщения об ошибке, окупается позже, когда тесты не проходят.

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

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


Читайте также:


Go FAQ: Как написать юнит-тест в Go?

Создайте новый файл, заканчивающийся на _test.go в том же каталоге, где и ваши исходные файлы пакета. Внутри этого файла import "testing" и напишите функции следующего вида:

func TestFoo(t *testing.T) {
    ...
}

Запустите go test в этом каталоге. Этот скрипт находит функции Test, создает тестовый бинарный файл и запускает его.


Читайте также:


Go FAQ: Как создать многофайловый пакет в Go?

Поместите все исходные файлы для пакета в каталог самостоятельно. Исходные файлы могут ссылаться на элементы из разных файлов по желанию; нет необходимости в предварительных декларациях (forward declarations) или заголовочном файле (header file).

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


Читайте также:


понедельник, 18 февраля 2019 г.

Go FAQ: Почему в Go нет тернарного (?:) оператора?

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

if expr {
    n = trueVal
} else {
    n = falseVal
}

Причина, по которой ?: (тернарный оператор) отсутствует в Go, заключается в том, что разработчики языка видели, что операция использовалась слишком часто, чтобы создавать излишне сложные выражения. Форма if-else, хотя и дольше, но, несомненно, яснее. Языку нужна только одна условная конструкция потока управления.


Читайте также:


суббота, 16 февраля 2019 г.

Go FAQ: Что происходит с замыканиями, выполняемыми как go-процедуры?

Некоторая путаница может возникнуть при использовании замыканий с конкурентностью. Рассмотрим следующую программу:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // ждем завершения всех go-процедур перед выходом
    for _ = range values {
        <-done
    }
}

Можно ошибочно ожидать, что a,b,c будет отображаться как результат. Вместо этого вы, вероятно, увидите c,c,c. Это потому, что каждая итерация цикла использует один и тот же экземпляр переменной v, поэтому каждое замыкание разделяет эту единственную переменную. Когда замыкание выполняется, оно печатает значение v в момент выполнения fmt.Println, но v, возможно, был изменен с момента запуска программы. Чтобы помочь обнаружить эту и другие проблемы, прежде чем они возникнут, запустите go vet.

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

for _, v := range values {
    go func(u string) {
        fmt.Println(u)
        done <- true
    }(v)
}

В этом примере значение v передается в качестве аргумента анонимной функции. Это значение затем доступно внутри функции как переменная u.

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

for _, v := range values {
    v := v // создаем новую 'v'.
    go func() {
        fmt.Println(v)
        done <- true
    }()
}

Это поведение языка, что не происходит определения новой переменной для каждой итерации, возможно, было ошибкой в ​​ретроспективе. Это может быть решено в более поздней версии, но, для совместимости, не может измениться в версии Go 1.


Читайте также:


Go FAQ: Почему T и *T имеют разные наборы методов?

Как сказано в спецификации Go, набор методов типа T состоит из всех методов с типом приемника T, в то время как соответствующий указатель типа *T состоит из всех методов с получателем *T или T. Это означает, что набор методов *T включает в себя T, но не наоборот.

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

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

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

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


Читайте также:


Go FAQ: Почему не существует идентификаторов go-процедур (goroutine ID)?

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

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

Вот иллюстрация трудностей. Однажды кто-то называет go-процедуру и строит модель вокруг нее, эта go-процедура становится особенной, и возникает искушение связать все вычисления с этой go-процедурой, игнорируя возможность использования нескольких, возможно, совместно используемых go-процедур для обработки. Если пакет net/http связан каждым запросом с go-процедурой, клиенты не смогут использовать больше go-процедур при обслуживании запроса.

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

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


Читайте также:


Go FAQ: Как можно контролировать количество процессоров, используемых в Go программе?

Количество процессоров, доступных одновременно для выполнения go-процедур (goroutines), управляется переменной среды оболочки GOMAXPROCS, значение по умолчанию - количество доступных процессорных ядер. Поэтому программы с возможностью параллельного выполнения должны получить все процессоры по умолчанию на многопроцессорной машине. Чтобы изменить количество параллельных процессоров, установите переменную среды или используйте одноименную функцию runtime пакета для настройки использования различного количества потоков во время выполнения. Установка его в 1 исключает возможность истинного параллелизма, заставляя независимые go-процедуры (goroutines) выполняться по очереди.

Среда выполнения (runtime) может выделить больше потоков, чем значение GOMAXPROCS для обслуживания нескольких невыполненных запросов ввода/вывода. GOMAXPROCS влияет только на количество go-процедур (goroutines), которые могут реально выполниться сразу; произвольно больше может быть заблокировано в системных вызовах.

Планировщик go-процедур (goroutines) в Go не так хорош, как это должно быть, хотя это со временем улучшилось. В будущем он может лучше оптимизировать использование потоков операционной системы. На данный момент, если есть проблемы с производительностью, настройка GOMAXPROCS для каждого приложения может помочь.


Читайте также:


Go FAQ: Почему программа не работает быстрее с большим количеством процессоров?

Будет ли программа работать быстрее с большим количеством процессоров, зависит от проблемы, которую она решает. Язык Go предоставляет конкурентные примитивы, такие как go-процедуры (goroutines) и каналы, но конкурентность позволяет параллелизм только когда основная проблема внутренне параллельна. Проблемы, которые являются последовательными, не могут быть ускорены путем добавления большего количества процессоров, в то время как те, которые могут быть разбиты на части, которые могут выполняться параллельно, могут быть ускорены, иногда значительно.

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


Читайте также:


пятница, 15 февраля 2019 г.

Go FAQ: Какие операции атомарные? Как насчет мьютексов?

Описание атомарности операций в Go можно найти в описании модели памяти Go.

Низкоуровневая синхронизация и атомарные примитивы доступны в sync и sync/atomic пакетах. Эти пакеты хороши для простых задач, таких как увеличение значения счетчика ссылок или гарантирование небольшого взаимного исключения.

Для операций более высокого уровня, таких как координация между одновременными серверами, высоко-уровневые техники могут привести к более приятным программам, и Go поддерживает этот подход через его go-процедуры (goroutines) и каналы. Например, вы можете структурировать вашу программу так, чтобы только одна go-процедура (goroutine) одновременно несет ответственность за конкретную часть данных. Этот подход обобщен оригинальным Go высказыванием:

Не общайтесь, разделяя память. Вместо этого делитесь памятью, общаясь.

Большие параллельные программы, вероятно, могут использовать преимущества обоих этих наборов инструментов.


Читайте также:


Go FAQ: Как узнать, где в памяти расположена переменная - в куче (heap) или в стеке (stack)?

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

Место хранения влияет на написание эффективных программ. Когда это возможно, компиляторы Go будут выделять переменные, которые локальны для функции в кадре стека (stack frame) этой функции. Однако если компилятор не может доказать, что на переменную не ссылаются после того как функция выполняет возврат, то компилятор должен разместить переменную в куче (heap), проверяемой сборщиком мусора во избежание ошибок висячих указателей. Кроме того, если локальная переменная очень большая, может иметь больше смысла хранить ее в куче, а не в стеке.

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


Читайте также:


Go FAQ: Каков размер int на 64-битной машине?

Размеры int и uint зависят от реализации, но одинаковы на данной платформе. Для переносимости, код, который опирается на определенный размер значения, должен использовать явно размерный тип, например, int64. На 32-битных машинах компиляторы используют 32-битные целые числа по умолчанию, в то время как на 64-битных машинах целые числа имеют 64-битный размер. (Исторически это не всегда было так.)

С другой стороны, скаляры с плавающей точкой и комплексные типы всегда имеют определенный размер (нет базовых типов float или complex), потому что программисты должны знать точность при использовании чисел с плавающей точкой. Тип по умолчанию, используемый для (нетипизированной) константы с плавающей точкой: float64. Таким образом, foo := 3.0 объявляет переменную foo типа float64. Для переменной float32, которая инициализирована (нетипизированной) константой, тип должен быть указан явно в объявлении переменной:

var foo float32 = 3.0

В качестве альтернативы, константе должен быть присвоен тип с преобразованием, как в foo: = float32(3.0).


Читайте также:


Go FAQ: Следует ли определять методы для значений или указателей?

// метод для указателя
func (s *MyStruct) pointerMethod() { } 

// метод для значения
func (s MyStruct)  valueMethod()   { } 

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

Во-первых, и это наиболее важно, метод должен изменить получатель? Если это так, получатель должен быть указателем. (Срезы и карты выступают в качестве ссылок, поэтому их история немного более тонкая, но, например, для изменения длины среза в методе получатель должен быть указателем.) В приведенных выше примерах, если pointerMethod изменяет поля s, вызывающая сторона увидит эти изменения, но valueMethod вызывается с копией аргумента вызывающей стороны (это определение передачи значения), поэтому изменения, которые он вносит, будут невидимы для вызывающей стороны.

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

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

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

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


Читайте также:


Go FAQ: Когда следует использовать указатель на интерфейс?

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

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

Рассмотрим объявление переменной,

var w io.Writer

Функция печати fmt.Fprintf принимает в качестве первого аргумента значение, которое удовлетворяет io.Writer - то, что реализует канонический метод Write. Таким образом, мы можем написать

fmt.Fprintf(w, "hello, world\n")

Однако если мы передадим адрес w, программа не скомпилируется.

// Compile-time error (Ошибка компиляции).
fmt.Fprintf(&w, "hello, world\n") 

Единственным исключением является то, что любое значение, даже указатель на интерфейс, может быть назначено переменной пустого типа интерфейса (interface{}). Тем не менее, это почти наверняка ошибка, если значение является указателем на интерфейс; результат может сбивать с толку.


Читайте также:


четверг, 14 февраля 2019 г.

Go FAQ: Когда параметры функции передаются по значению?

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

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

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


Читайте также:


Go FAQ: Как управлять версиями пакетов, используя "go get"?

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

Релиз Go 1.11 добавил новую экспериментальную поддержку для контроля версий пакетов в команду go, в виде модулей Go.

Независимо от фактической технологии управления пакетами, "go get" и больший набор инструментов Go обеспечивает изоляцию пакетов с разными путями импорта. Например, стандартные библиотеки html/template и text/template сосуществуют, хотя оба являются "package template". Это наблюдение приводит к некоторым советам для авторов пакетов и пользователей пакетов.

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

Если вы используете поставляемый извне пакет и беспокоитесь о том, что он может измениться неожиданными способами, но еще не используете модули Go, самое простое решение - скопировать его в локальный репозиторий. Это тот подход, который Google использует для себя и поддерживается командой go с помощью метода, называемого "vendoring". Он включает в себя хранение копии зависимости по новому пути импорта, который идентифицирует ее как локальную копию.


Читайте также:


Go FAQ: Почему "go get" использует HTTPS при клонировании репозитория?

Компании часто разрешают исходящий трафик только через стандартные порты TCP 80 (HTTP) и 443 (HTTPS), блокируя исходящий трафик на других портах, включая TCP-порт 9418 (git) и TCP-порт 22 (SSH). При использовании HTTPS вместо HTTP git выполняет проверку сертификата по умолчанию, обеспечивая защиту от атак "человек посередине", подслушивания и взлома. Поэтому команда go get использует HTTPS для безопасности.

Git можно настроить для аутентификации по HTTPS или для использования SSH вместо HTTPS. Для аутентификации через HTTPS вы можете добавить строку в файл $HOME/.netrc, к которому обращается git:

machine github.com login USERNAME password APIKEY

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

Git также можно настроить для использования SSH вместо HTTPS для URL-адресов, соответствующих данному префиксу. Например, чтобы использовать SSH для всего доступа к GitHub, добавьте следующие строки в ваш ~/.gitconfig:

[url "ssh://git@github.com/"]
    insteadOf = https://github.com/


Читайте также:


среда, 13 февраля 2019 г.

Go FAQ: Существует ли руководство по Go стилю программирования?

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

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

Документ Go Code Review Comments это коллекция очень коротких эссе о деталях идиом Go, которые часто пропускаются программистами. Это удобный справочник для людей, которые делают review кода для проектов Go.


Читайте также:


Go FAQ: Как документируются библиотеки?

Существует программа godoc, написанная на Go, которая извлекает пакетную документацию из исходного кода и использует ее как веб страницу со ссылками на объявления, файлы и так далее. Экземпляр работает на golang.org/pkg/. Фактически, godoc реализует полный сайт по адресу golang.org.

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

Для доступа к документации из командной строки, go tool имеет doc подкоманду, которая обеспечивает текстовый интерфейс с той же информацией.


Читайте также:


Go FAQ: карты (maps) в Go

Почему карты в Go являются встроенным типом?

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

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

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

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

Почему карты, срезы и каналы ссылаются, в то время как массивы являются значениями?

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


Читайте также:


Go FAQ: Как работают константы в Go?

Хотя Go строг по преобразованию между переменными разных числовых типов, константы в языке гораздо более гибкие. Литеральные константы, такие как 23, 3.14159 и math.Pi занимают своего рода идеальное числовое пространство с произвольной точностью и нет переполнения или ненаполнения. Например, значение math.Pi указано в 63 местах в исходном коде, а константные выражения, включающие значение, сохраняют точность выше того, что может хранить float64. Только когда константа или константное выражение присваиваются переменной - области памяти в программе - она становится «компьютерным» числом с обычными свойствами, с плавающей точкой и точностью.

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

sqrt2 := math.Sqrt(2)

без претензий от компилятора, потому что идеальное число 2 может быть преобразовано безопасно и точно в float64 для вызова math.Sqrt.


Читайте также:


вторник, 12 февраля 2019 г.

Go FAQ: Почему Go не предоставляет неявных числовых преобразований?

Удобство автоматического преобразования между числовыми типами в C перевешивается путаницей, которую это вызывает. Как происходит преобразование когда выражение не подписано? Насколько велика цена преобразования? Происходит ли переполнение при преобразовании? Является ли результат портативным, независимым от машины, на которой он выполняется? Это также усложняет компилятор; "обычные арифметические преобразования" не просты в реализации и несовместимы между архитектурами. Из соображений мобильности создатели Go решили сделать все ясно и понятно ценой некоторых явных преобразований в коде. Определение констант в Go - значений произвольной точности, свободных от аннотации подписи и размера - значительно улучшает положение дел, однако.

Связанная деталь заключается в том, что, в отличие от C, int и int64 разные типы, даже если int является 64-битным типом. int тип является общим; если вы заботитесь о том, сколько битов содержит целое число, Go призывает вас быть явным.


Читайте также:


понедельник, 11 февраля 2019 г.

Go FAQ: Почему в Go нет ковариантных типов результатов?

Ковариантные типы результатов означают, что интерфейс

type Copyable interface {
    Copy() interface{}
}

будет удовлетворен методом

func (v Value) Copy() Value

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


Читайте также:


Go FAQ: Почему в Go нет вариантных типов (variant types)?

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

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

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


Читайте также:


Go FAQ: Почему nil значение ошибки не равно nil?

Под капотом интерфейсы реализованы в виде двух элементов, типа T и значения V. V - это конкретное значение, такое как int, struct или указатель, никогда не сам интерфейс, и имеет тип T. Например, если мы храним int значение 3 в интерфейсе, результирующее значение интерфейса имеет, схематически, (T = int, V = 3). Значение V также известно как динамическое значение интерфейса, поскольку данная переменная интерфейса может содержать разные значения V (и соответствующие типы T) во время исполнения программы.

Значение интерфейса равно nil, только если V и T оба не заданы (T = nil, V не задано), в частности, интерфейс nil всегда будет содержать тип nil. Если мы сохраним указатель nil типа *int внутри значения интерфейса, внутренний тип будет *int независимо от значения указателя: (T = *int, V = nil). Следовательно, такое значение интерфейса будет не nil, даже если значение указателя V внутри равно nil.

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

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Всегда будет возвращать не-nil ошибку.
}

Если все идет хорошо, функция возвращает nil p, поэтому возвращаемое значение является значением интерфейса error содержащее (T = *MyError, V = nil). Это означает, что если вызывающая сторона сравнивает возвращенную ошибку с nil, это всегда будет выглядеть так, как будто произошла ошибка, даже если ничего плохого не произошло. Чтобы вернуть правильную nil error вызывающей стороне, функция должна возвращать явный nil:

func returnsError() error {
    if bad() {
        return ErrBad
    }
    return nil
}

Это хорошая идея для функций, которые возвращают ошибки всегда использовать тип error в их сигнатуре (как мы сделали выше), а не конкретный тип, такой как *MyError, чтобы гарантировать, что ошибка создана правильно. В качестве примера, os.Open возвращает error, хотя, если не nil, она всегда конкретного типа *os.PathError.

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


Читайте также: