Показаны сообщения с ярлыком Go FAQ. Показать все сообщения
Показаны сообщения с ярлыком Go FAQ. Показать все сообщения

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

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


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