среда, 18 декабря 2019 г.

Исследование утечек памяти в Golang с помощью pprof

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

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

Мы можем представить отображение памяти в виде дерева, и обход этого дерева проведет нас через различные распределения объектов и отношений. Это означает, что все, что находится в корне, является причиной для "удержания" памяти, а не для ее сбора (сбора мусора). Поскольку в Go нет простого способа проанализировать полный дамп ядра, добраться до корней объекта, который не собирается сборщиком мусора, сложно.

Утечки памяти

Утечки памяти или давление памяти (memory pressure) могут проявляться во многих формах во всей системе. Обычно мы обращаемся к ним как к ошибкам, но иногда их коренная причина может заключаться в проектных решениях.

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

  • Слишком много распределений (allocations), неправильное представление данных
  • Интенсивное использование отражения (reflection) или строк (strings)
  • Использование глобальных переменных (globals)
  • Осиротевшие, бесконечные горутины (goroutines)

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

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

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

go tool pprof

Пакет pprof создает файл дампа кучи (heap), который вы можете позже проанализировать/визуализировать, чтобы получить карту:

  • Текущих распределений памяти
  • Общего (накопительного) распределения памяти

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

Профили pprof

Pprof работает с использованием профилей.

Профиль - это набор трассировок стека, показывающих последовательности вызовов, которые привели к появлению определенного события, такого как распределение (allocation).

Файл runtime/pprof/pprof.go содержит подробную информацию и реализацию профилей.

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

  • goroutine - стек трассировок всех текущих goroutines
  • heap - выборка выделений памяти живых объектов
  • allocs - выборка всех прошлых распределений памяти
  • threadcreate - стек трассировок, которые привели к созданию новых потоков ОС
  • block - стек трассировок, которые привели к блокировке примитивов синхронизации
  • mutex - стек трассировок держателей конфликтующих мьютексов

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

Куча (Heap)

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

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

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

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

Получение данных кучи (heap) с помощью pprof

Существует два основных способа получения данных для этого инструмента. Первый обычно является частью теста или ветви и включает импорт runtime/pprof и затем вызов pprof.WriteHeapProfile(some_file) для записи информации кучи.

Обратите внимание, что WriteHeapProfile является синтаксическим сахаром для запуска:

// Lookup принимает имя профиля
pprof.Lookup("heap").WriteTo(some_file, 0)

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

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

import (  
    "net/http"  
    _ "net/http/pprof"
)
...
func main() {  
    ...  
    http.ListenAndServe("localhost:8080", nil)
}

"Побочным эффектом" импорта net/http/pprof является регистрация конечных точек pprof в корневом каталоге веб-сервера в /debug/pprof. Теперь, используя curl, мы можем получить файлы с информацией для исследования:

curl -sK -v http://localhost:8080/debug/pprof/heap > heap.out

Добавление http.ListenAndServe() выше требуется только в том случае, если ваша программа ранее не имела прослушивателя http. Если он у вас есть, он зацепит его, и вам не нужно снова слушать. Существуют также способы настроить его с помощью ServeMux.HandleFunc(), который более понятен для более сложной программы с поддержкой http. Например:

router := mux.NewRouter()
router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux)

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

Итак, мы собрали данные, что теперь? Как упоминалось выше, есть две основные стратегии анализа памяти с помощью pprof. Один из них заключается в рассмотрении текущих распределений (байтов или количества объектов), называемых inuse. Другой просматривает все выделенные байты или количество объектов во время выполнения программы, называемой alloc, независимо от того, были ли они собраны сборщиком мусора, то есть суммирование всех выборок.

Это хорошее место, чтобы повторить, что профиль heap является выборкой распределения памяти. За кулисами pprof использует функцию runtime.MemProfile, которая по умолчанию собирает информацию о распределении на каждые 512 КБ выделенных байтов. Можно изменить MemProfile для сбора информации обо всех объектах. Обратите внимание, что, скорее всего, это замедлит работу вашего приложения.

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

Как только мы собрали файл профиля, пришло время загрузить его в интерактивную консоль pprof. Сделайте это, запустив:

> go tool pprof heap.out

Давайте посмотрим на отображаемую информацию

Type: inuse_spaceTime: Jan 22, 2019 at 1:08pm (IST)Entering interactive mode (type "help" for commands, "o" for options)(pprof)

Здесь важно отметить Type: inuse_space. Это означает, что мы смотрим на данные о распределении определенного момента (когда мы захватили профиль). Type является значением конфигурации sample_index, и возможными значениями являются:

  • inuse_space - объем памяти, выделенной и еще не освобожденной
  • inuse_objects - количество объектов, выделенных и еще не освобожденных
  • alloc_space - общий объем выделенной памяти (независимо от освобождения)
  • alloc_objects - общее количество объектов, выделенных (независимо от освобожденных)

Теперь введите top в интерактивном режиме, будет вывод наибольших потребителей памяти.

Мы можем видеть строку, рассказывающую нам о Dropped Nodes, это означает, что они отфильтрованы. Узел (Node) - это запись объекта или "узел" в дереве. Удаление узлов - это хорошая идея, чтобы уменьшить шум, но иногда это может скрывать основную причину проблемы с памятью. Мы увидим пример этого, продолжая наше расследование.

Если вы хотите включить все данные профиля, добавьте опцию -nodefraction = 0 при запуске pprof или введите nodefraction = 0 в интерактивном режиме.

В выводимом списке мы видим два значения, flat и cum.

  • flat означает память, выделенную этой функцией и удерживаемой этой функцией
  • cum означает, что память была выделена этой функцией или функцией, вызванной стеком

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

Еще одна хитрость в верхней части интерактивного окна заключается в том, что он на самом деле работает в top10. Команда top поддерживает формат topN, где N - количество записей, которые вы хотите увидеть.

Визуализация

В то время как topN предоставляет текстовый список, есть несколько очень полезных опций визуализации, которые поставляются с pprof. Можно набрать png или gif и многое другое (полный список см. go tool pprof -help).

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

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

Обратите внимание, что на изображении выше png с режима выполнения inuse_space. Много раз вы также должны взглянуть на inuse_objects, так как это может помочь в поиске проблем с распределением.

Копать глубже, находить первопричину

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

В нашем случае мы могли видеть, что память сохраняется membuffer'ами, которые являются нашей библиотекой сериализации данных. Это не означает, что у нас есть утечка памяти в этом сегменте кода, это означает, что память удерживается этой функцией. Важно понимать, как читать граф и вывод pprof в целом. В этом случае, понимая, что когда мы сериализуем данные, что означает, что мы выделяем память для структур и примитивных объектов (int, string), они никогда не освобождаются.

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

Где-то в цепочке мы видим нашу библиотеку журналов, отвечающую за >50 МБ выделенной памяти. Это память, которая выделяется функциями, вызываемыми нашим логгером. Это на самом деле ожидаемо. Логгер вызывает выделение памяти, поскольку ему необходимо сериализовать данные для вывода их в журнал и, следовательно, он вызывает выделение памяти в процессе.

Мы также можем видеть, что на пути выделения памяти память сохраняется только при сериализации и больше нигде. Кроме того, объем памяти, сохраняемой логгером, составляет около 30% от общего объема. Вышесказанное говорит нам, что, скорее всего, проблема не в логгере. Если бы это было 100%, или что-то близкое к этому, то мы должны были бы искать там - но это не так. Это может означать, что логируется что-то, чего не должно быть, но это не утечка памяти логгером.

Самое время представить еще одну команду pprof, которая называется list. Она принимает регулярное выражение, которое будет фильтровать то, что перечислить. "Список" (list) в действительности представляет собой аннотированный исходный код, связанный с распределением. В контексте логгера, который мы рассматриваем, мы выполним list RequestNew, так как мы хотим видеть вызовы, сделанные в логгере. Эти вызовы поступают из двух функций, которые начинаются с одного и того же префикса.

Мы можем видеть, что сделанные распределения находятся в столбце cum, что означает, что выделенная память сохраняется в стеке вызовов. Это соответствует тому, что граф также показывает. В этот момент легко увидеть, что причина, по которой логгер выделял память, заключается в том, что мы отправили ему весь "block" объект. Нужно было как минимум сериализовать некоторые его части (наши объекты являются объектами-оболочками, которые всегда реализуют некоторую функцию String()). Это полезное сообщение в журнале или хорошая практика? Вероятно, нет, но это не утечка памяти, не на стороне логгера или кода, который вызвал логгер.

list может найти исходный код при поиске в вашей среде GOPATH. В случаях, когда корень, который он ищет, не совпадает, что зависит от вашей машины сборки, вы можете использовать опцию -trim_path. Это поможет исправить его и позволить увидеть аннотированный исходный код. Не забудьте установить свой git на правильный коммит, который работал, когда был захвачен профиль кучи.

Так почему память сохраняется?

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

На этом этапе, в случае Java или .Net, мы бы открыли некоторый анализатор или профилировщик "gc root" и добрались до реального объекта, который ссылается на эти данные и создает утечку. Как объяснено, это не совсем возможно с Go, как из-за проблем с инструментами, так и из-за низкоуровнего представления памяти в Go.

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

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

При установке nodefraction = 0 мы увидим всю карту выделенных объектов, включая меньшие. Давайте посмотрим на результат:

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

Более короткое, синего цвета, у которого есть ребро, соединяющее ее со всей системой, - inMemoryBlockPersistance. Это имя также объясняет "утечку", которую мы себе представили. Это серверная часть данных, которая хранит все данные в памяти и не сохраняется на диске. Приятно отметить, что мы сразу увидели, что в нем находятся два больших объекта. Почему два? Поскольку мы видим, что размер объекта составляет 1,28 МБ, а функция сохраняет 2,57 МБ, то есть два из них.

Проблема понятна на данный момент. Мы могли бы использовать delve (отладчик), чтобы увидеть, что это массив, содержащий все блоки для in-memory persistence драйвера, который у нас есть.

Пользовательский интерфейс pprof

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

go tool pprof -http=:8080 heap.out

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

Для просмотра графов в системе должна быть установлена graphviz. Для установки например на Ubuntu: sudo apt-get install graphviz

Вывод

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


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


пятница, 22 ноября 2019 г.

Golang puzzlers: синхронизации go-процедур (goroutine)

В этом посте рассмотрим задачу синхронизации go-процедур (goroutine). Дан код:

package main

import (
  "fmt"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}

  for i : range hosts {
    go func(){
      fmt.Println(hosts[i])
    }()
  }
}

Вопрос: что выведет данный код? Ответ: он вообще не скомпилируется. Это видно невооруженным взглядом - здесь банальная ошибка в синтаксисе range - range для среза будет возвращать 2 значения - индекс элемента и содержимое элемента, а также оператор присваивания написан неверно - вместо := написано :. Хорошо, исправим:

package main

import (
  "fmt"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    go func(){
      fmt.Println(hosts[i])
    }()
  }
}

Вопрос: что выведет код сейчас? Ответ: ничего. Почему? Потому что запустив 3 goroutine основной поток исполнения функции main не будет ждать их выполнения и выполнит возврат, поэтому в консоль не будет ничего напечатано. Необходимо чтобы были напечатаны все три значения hosts - по одному на каждую goroutine. Попробуем исправить: здесь у нас появляется выбор, поскольку получить такой результат можно разными способами. Рассмотрим использование sync.WaitGroup:

package main

import (
  "fmt"
  "sync"
)

func main() {

  var wg sync.WaitGroup

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    wg.Add(1)
    go func() {
      fmt.Println(hosts[i])
      wg.Done()
    }()
  }
  wg.Wait()
}

Вопрос: что выведет код сейчас? Ответ: три раза chost. Почему? Потому что каждая goroutine, хотя и является замыканием и может использовать переменную i, определенную вне анонимной функции, запускаемой как go-процедура (goroutine), но не запоминает значения i, а на момент исполнения каждой из go-процедур цикл range уже завершился и в i сохранилось последнее значение 2, поэтому все go-процедуры печатают значение hosts[2]. Исправим, чтобы получить значение i, которое было во время запуска go-процедуры:

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    wg.Add(1)
    go func(i int) {
      fmt.Println(hosts[i])
      wg.Done()
    }(i)
  }
  wg.Wait()
}

Теперь будут выведены все 3 значения hosts. go-процедуры будут выполняться конкурентно, но порядок вывода не гарантируется. Похожий на первый взгляд результат мы можем получить используя для синхронизации небуферизованный канал:

package main

import (
  "fmt"
)

func main() {

  // Создаем небуферизованный канал
  ch := make(chan int)

  hosts := []string{"ahost", "bhost", "chost"}

  for i, _ := range hosts {
    go func(i int) {
      fmt.Println(hosts[i])
      // Получаем сообщение из канала
      <-ch
    }(i)
    // Отправляем 0 в канал
    ch <- 0
  }

}

Теперь также будут выведены все 3 значения hosts. За счет того что прием из небуферизованного канала происходит до завершения отправки по небуферизованному каналу мы гарантируем что последняя go-процедура будет выполнена и все значения будут напечатаны.

Но у данного подхода есть проблема - мы потеряли конкурентность в исполнении go-процедур - теперь они запускаются последовательно и каждая следующая go-процедура, ждет завершения предыдущей, поскольку в range цикле запись в небуферизованный канал может быть выполнена только когда канал пуст. Можно вернуть конкурентность, для этого используем буферизованный канал с размером равным размеру hosts - в данному случае кроме конкурентности мы сможем также получить сохранение последовательности вывода в консоль значений hosts, поскольку значения i будут записаны в канал в порядке их итерации. Для ожидания завершения всех go-процедур используем другой буферизованный канал out размером равным размеру hosts. С помощью утверждения select считываем из него значения и подсчитываем количество вернувшихся значений либо ожидаем 1 мс и снова проверяем содержимое канала:

package main

import (
  "fmt"
  "time"
)

func main() {

  hosts := []string{"ahost", "bhost", "chost"}
  size := len(hosts)

  ch := make(chan int, size)
  out := make(chan int, size)

  for i, _ := range hosts {
    ch <- i
    go func() {
      fmt.Println(hosts[<-ch])
      out <- 0
    }()
  }
  var count int
  for {
    select {
    case <-out:
      count++
      if count == size {
        return
      }
    default:
      time.Sleep(1 * time.Millisecond)
    }
  }
}

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

Запустить пример в песочнице play.golang.org


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


четверг, 21 ноября 2019 г.

Golang puzzlers: длина строки

Пример этого поста: определение длины строки. Дан код:

package main

import "fmt"

func main() {
  test := "frйday"
  fmt.Println(len(test))
}

Вопрос: что выведется при запуске?

На первый взгляд покажется что ответ должен быть 6, поскольку в строке 6 символов. Но при запуске в выводе будет 7!

В чем подвох? Дело в том что len измеряет строку по байтам, а не по символам. 5 символов frday являются ASCII символами и в UTF-8 (а исходный код всех строк в Go представлен UTF-8 текстом) каждый из этих символов занимает 1 байт, а символ й не является ASCII символом, и в UTF-8 занимает 2 байта, поэтому 5+2=7. Стоит сразу отметить, что исходный код строк в Go состоит из UTF-8 текста, но в UTF-8 если символ вмещается в 1 байт (как с ASCII символами), то он и записывается 1 байтом. Символ й занимает 2 байта, но если бы мы взяли какой-нибудь китайский иероглиф, который в UTF-8 занимает 3 байта, в нашем примере, то могли бы получить даже 8 в ответе, например:

package main

import "fmt"

func main() {
  test := "fr語day"
  fmt.Println(len(test))
}

Вывод:

8

Запустить пример в песочнице play.golang.org

Для более подробной информации о строках, символах и кодировке читайте этот пост.


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


Golang puzzlers: возврат ошибки

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

Пример этого поста. Дан код:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  return
 }
 fmt.Println("ok")
}

Вопрос: что выведет данный код?

На первый взгляд все очевидно: задан тип myType, указатель на который реализует интерфейс error. Раз указатель на myType реализует интерфейс ошибки, то кажется что все верно - функция test возвращает указатель на myType, а в функции main возвращаемое значение будет записано в переменную типа error. При запуске программа скомпилируется и кажется, что раз мы возвращаем nil в test, то будет выведено ok, но в консоль выведется occured error!

Начнем разбор - распечатаем переменную err, которая должна быть равна nil:

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("occured error")
  fmt.Println(err)
  return
 }
 fmt.Println("ok")
}

При запуске получим:

occured error
<nil>

Значит err равно nil? В чем подвох? Мы сможем понять это использовав пакет reflect для разбора:

package main

import (
 "fmt"
 "reflect"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() *myType {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  fmt.Println(err)
  e := reflect.ValueOf(err)
  fmt.Println(e)
  fmt.Println(e.Kind())
  return
 }
 fmt.Println("ok")
 fmt.Println(err)
}

При запуске получим:

error
<nil>
<nil>
ptr

Что получается? В err записывается указатель со значением nil. Это указатель на тип myType, который реализует интерфейс error, поэтому этот указатель можно записать в переменную типа error - компилятор позволит нам это. Но при проверке на nil за nil засчитается только значение записанное по типу интерфейса error - то есть если бы функция test в качестве возвращаемого значения в сигнатуре функции указывала бы интерфейс error, тогда все бы работало как предполагается и в консоль было бы выведено ok.

package main

import (
 "fmt"
)

type myType struct {
 msg string
}

func (m *myType) Error() string {
 return m.msg
}

func test() error {
 return nil
}

func main() {
 var err error
 err = test()
 if err != nil {
  fmt.Println("error")
  return
 }
 fmt.Println("ok")
}

При запуске получим:

ok

Запустить пример в песочнице play.golang.org


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


четверг, 14 ноября 2019 г.

Работа с JSON в Golang

JSON (JavaScript Object Notation) - это простой формат обмена данными. Синтаксически это напоминает объекты и списки JavaScript. Он чаще всего используется для связи между веб-интерфейсами и программами JavaScript, работающими в браузере, но также используется и во многих других местах. Его домашняя страница, json.org, предоставляет четкое и краткое определение стандарта.

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

Кодирование

Для кодирования данных в JSON используется функция Marshal.

func Marshal(v interface{}) ([]byte, error)

Задав структуру данных Go, Message,

type Message struct {
    Name string
    Body string
    Time int64
}

и экземпляр сообщения

m := Message{"Alice", "Hello", 1294706395881547000}

мы можем получить JSON-кодированную версию m, используя json.Marshal:

b, err := json.Marshal(m)

Если все хорошо, err будет nil, а b будет []byte, содержащим JSON данные:

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

Будут закодированы только структуры данных, которые могут быть представлены валидным JSON:

  • Объекты JSON поддерживают только строки в качестве ключей; чтобы кодировать Go тип map он должен иметь вид map[string]T (где T - любой тип Go, поддерживаемый пакетом json).
  • Channel, complex и func типы не могут быть закодированы.
  • Циклические структуры данных не поддерживаются; они приведут к попаданию Marshal в бесконечный цикл.
  • Указатели будут закодированы как значения, на которые они указывают (или 'null', если указатель равен nil).

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

Декодирование

Для декодирования JSON данных используем функцию Unmarshal.

func Unmarshal(data []byte, v interface{}) error

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

var m Message

и вызвать json.Unmarshal, передав ему []byte JSON данных и указатель на m

err := json.Unmarshal(b, &m)

Если b содержит допустимый JSON, который соответствует m, после вызова err будет nil и данные из b будут сохранены в структуре m, как если бы это было сделано с помощью присваивания, подобного следующему:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Как Unmarshal идентифицирует поля для хранения декодированных данных? Для заданного ключа JSON "Foo" Unmarshal просматривает поля структуры назначения, чтобы найти (в порядке предпочтения):

  • Экспортированное поле с тегом "Foo"
  • Экспортированное поле с именем "Foo"
  • Экспортированное поле с именем "FOO" или "FoO" или другое нечувствительное к регистру совпадение "Foo"

Что происходит, когда структура данных JSON не совсем соответствует типу Go?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal будет декодировать только те поля, которые он может найти в типе назначения. В этом случае будет заполнено только поле Name в m, а поле Food будет игнорироваться. Это поведение особенно полезно, когда вы хотите выбрать только несколько определенных полей из большого JSON-объекта. Это также означает, что Unmarshal не затронет любые неэкспортированные поля в структуре назначения.

Но что, если вы заранее не знаете структуру данных JSON?

Универсальный JSON с interface{}

Тип interface{} (пустой интерфейс) описывает интерфейс с нулевыми методами. Каждый тип Go реализует как минимум ноль методов и поэтому удовлетворяет пустому интерфейсу.

Пустой интерфейс служит общим типом контейнера:

var i interface{}
i = "a string"
i = 2011
i = 2.777

Утверждение типа обращается к подлежащему конкретному типу:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

Или, если базовый тип неизвестен, переключатель типа определяет тип:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i ни один из типов выше
}

Пакет json использует значения map[string]interface{} и []interface{} для хранения произвольных объектов и массивов JSON; он с радостью разархивирует любой допустимый большой бинарный объект JSON (blob) в простое значение interface{}. Конкретные типы Go по умолчанию:

  • bool для логических выражений JSON
  • float64 для JSON чисел
  • string для JSON строк
  • nil для JSON null

Декодирование произвольных данных

Рассмотрим JSON данные, хранящиеся в переменной b:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

Не зная структуры этих данных, мы можем декодировать их в значение interface{} с помощью Unmarshal:

var f interface{}
err := json.Unmarshal(b, &f)

В этот момент значением Go в f будет карта, ключи которой являются строками, а сами значения хранятся как пустые значения интерфейса:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

Чтобы получить доступ к этим данным, мы можем использовать утверждение типа для доступа к подлежащему map[string]interface{}:

m := f.(map[string]interface{})

Затем мы можем перебрать карту с помощью оператора range и использовать переключатель типа для доступа к ее значениям в качестве их конкретных типов:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

Таким образом, вы можете работать с неизвестными JSON данными, сохраняя при этом преимущества безопасности типов.

Ссылочные типы

Определим тип Go, который будет содержать данные из предыдущего примера:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

Демонстрация этих данных в значение FamilyMember работает, как и ожидалось, но если мы посмотрим внимательнее, то увидим, что произошла замечательная вещь. С помощью оператора var мы выделили структуру FamilyMember, а затем предоставили указатель на это значение для Unmarshal, но в то время поле Parents было нулевым значением среза. Чтобы заполнить поле Parents, Unmarshal выделил новый срез за кулисами. Это типично для того, как Unmarshal работает с поддерживаемыми ссылочными типами (указатели, срезы и карты).

Рассмотрим конвертирование в эту структуру данных:

type Foo struct {
    Bar *Bar
}

Если бы в объекте JSON было поле Bar, Unmarshal выделил бы новый Bar и заполнил его. Если нет, Bar будет оставлен как нулевой указатель.

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

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

и отправляющая сторона может заполнить поле Cmd и/или поле Msg объекта JSON верхнего уровня, в зависимости от типа сообщения, которое они хотят передать. Unmarshal при декодировании JSON в структуру IncomingMessage будет выделять только структуры данных, присутствующие в JSON данных. Чтобы узнать, какие сообщения обрабатывать, программисту просто нужно проверить, что Cmd или Msg не ноль.

Потоковые кодировщики и декодеры

Пакет json предоставляет типы Decoder и Encoder для поддержки обычной операции чтения и записи потоков данных JSON. Функции NewDecoder и NewEncoder оборачивают типы интерфейсов io.Reader и io.Writer.

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

Вот пример программы, которая читает серию объектов JSON из стандартного ввода, удаляет все, кроме поля Name, из каждого объекта, а затем записывает объекты в стандартный вывод:

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

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

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

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

package main

import (
 "encoding/json"
 "fmt"
)

type Item struct {
 ID      uint   `json:"id"`
 Title   string `json:"title"`
 BrandID uint   `json:"brand_id"`
}

func main() {

 item := Item{ID: 1, Title: "Car", BrandID: 1}
 jitem, err := json.Marshal(item)
 if err != nil {
  fmt.Println(err.Error())
  return
 }
 fmt.Println(string(jitem))
}

Вывод:

{"id":1,"title":"Car","brand_id":1}

Запустить пример в песочнице play.golang.org


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


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

Паттерны в Golang: Fan-Out

Fan-Out - это шаблон обмена сообщениями, используемый для распределения работы между работниками.

Можно смоделировать Fan-Out, используя каналы Go.

// Разделить канал на n каналов, 
// которые получают сообщения в циклическом порядке.
func Split(ch <-chan int, n int) []<-chan int {

  // Создаем пул из n каналов
  cs := make([]chan int)
  for i := 0; i < n; i++ {
    cs = append(cs, make(chan int))
  }

  // Распределяет работу в круговом порядке 
  // среди указанного числа каналов, 
  // пока основной канал не будет закрыт. 
  // При закрытии основного канала закрывает 
  // все каналы и возвращается.
  toChannels := func(ch <-chan int, cs []chan<- int) {

    // Закрываем каждый канал, 
    // когда выполнение заканчивается.
    defer func(cs []chan<- int) {
      for _, c := range cs {
        close(c)
      }
    }(cs)

    // Направляем сообщения из
    // основного канала ch
    // в каналы из пула cs
    for {
      for _, c := range cs {
        select {
        case val, ok := <-ch:
          if !ok {
            return
          }

          c <- val
        }
      }
    }
  }

  go toChannels(ch, cs)

  return cs
}

Функция `Split` преобразует один канал в список каналов с помощью goroutine для копирования полученных значений по каналам в списке в циклическом порядке.


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


среда, 6 ноября 2019 г.

Паттерны в Golang: Fan-In

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

Мы можем смоделировать Fan-In паттерн используя каналы Go.

// Объединяем разные каналы в один канал
func Merge(cs ...<-chan int) <-chan int {
  var wg sync.WaitGroup

  out := make(chan int)

  // Запускаем send goroutine 
  // для каждого входящего канала в cs. 
  // send копирует значения из c в out 
  // до тех пор пока c не закрыт, затем вызываем wg.Done.
  send := func(c <-chan int) {
    for n := range c {
      out <- n
    }
    wg.Done()
  }

  wg.Add(len(cs))
  for _, c := range cs {
    go send(c)
  }

  // Запускаем goroutine чтобы закрыть out 
  // когда все send goroutine выполнены
  // Это должно начаться после вызова wg.Add.
  go func() {
    wg.Wait()
    close(out)
  }()
  return out
}

Функция `Merge` преобразует список каналов в один канал, запуская goroutine для каждого входящего канала, которая копирует значения в единственный исходящий канал.

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


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


понедельник, 4 ноября 2019 г.

Golang в Google: дизайн языка на службе разработки программ, часть 1

Rob Pike, 2012

Язык программирования Go был задуман в конце 2007 года как ответ на некоторые проблемы, с которыми мы сталкивались при разработке программной инфраструктуры в Google. Сегодняшний вычислительный ландшафт практически не связан с той средой, в которой были созданы используемые языки, в основном C++, Java и Python. Проблемы, создаваемые многоядерными процессорами, сетевыми системами, массивными вычислительными кластерами и моделью веб-программирования, скорее были решены как обходные пути, чем решены как задуманные изначально. Более того, масштаб изменился: современные серверные программы состоят из десятков миллионов строк кода, работают с сотнями или даже тысячами программистов и обновляются буквально каждый день. Что еще хуже, время сборки, даже на больших кластерах компиляции, увеличилось до многих минут, даже часов.

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

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

1. Введение

Go - это компилируемый, конкурентный, имеющий сборщик мусора, статически типизированный язык, разработанный в Google. Это проект с открытым исходным кодом: Google импортирует публичный репозиторий, а не наоборот.

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

2. Go в Google

Go - это язык программирования, разработанный Google для решения проблем Google, и у Google большие проблемы.

Аппаратное обеспечение большое, а программное обеспечение большое. Есть много миллионов строк программного обеспечения, с серверами в основном на C++ и множеством Java и Python для других частей. Тысячи инженеров работают над кодом, находящимся в "голове" одного дерева, включающего все программное обеспечение, поэтому со дня на день происходят значительные изменения на всех уровнях дерева. Большая специализированная распределенная система сборки делает разработку в таком масштабе осуществимой, но она все еще медлительная.

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

Кратко, разработка в Google велика, может быть медленной и часто неуклюжей. Но это эффективно.

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

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

Но как язык может помочь в разработке программного обеспечения? Остальная часть этой статьи является ответом на этот вопрос.

3. Болевые точки

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

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

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

В качестве простого, автономного примера рассмотрим представление структуры программы. Некоторые наблюдатели возражали против C-образной блочной структуры Go с фигурными скобками, предпочитая использовать пробелы для отступов в стиле Python или Haskell. Тем не менее, у нас был большой опыт отслеживания сбоев сборки и тестирования, вызванных межъязыковыми сборками, где фрагмент Python, встроенный в другой язык, например, посредством вызова SWIG, тонко и незаметно нарушается изменением отступа окружающего кода. Поэтому наша позиция такова: хотя места для отступов хороши для небольших программ, они плохо масштабируются, и чем больше и разнороднее кодовая база, тем больше проблем это может вызвать. Лучше отказаться от удобства из-за безопасности и надежности, поэтому у Go есть ограниченные скобками блоки.

4. Зависимости в C и C++

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

Стандарт ANSI C, впервые принятый в 1989 году, продвигал идею #ifndef "охраняет" ("guards") в стандартных заголовочных файлах. Идея, которая сейчас распространена повсеместно, заключается в том, что каждый заголовочный файл заключен в скобки с условным блоком компиляции, чтобы файл мог быть включен несколько раз без ошибок. Например, заголовочный файл Unix <sys/stat.h> схематически выглядит так:

/* Крупная заметка об авторском праве и лицензии */
#ifndef _SYS_STAT_H_
#define _SYS_STAT_H_
/* Типы и другие определения */
#endif

Предполагается, что препроцессор C читает файл, но игнорирует содержимое во втором и последующих чтениях файла. Символ _SYS_STAT_H_, определяемый при первом чтении файла, "охраняет" ("guards") последующие вызовы.

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

Но это очень плохо масштабируется.

В 1984 году компиляция ps.c, исходного кода команды Unix ps, #include <sys/stat.h> наблюдалась 37 раз к тому времени, когда была выполнена вся предварительная обработка. Хотя при этом содержимое отбрасывается 36 раз, большинство реализаций C открывают файл, читают его и сканируют все 37 раз. Фактически, без большой хитрости это поведение требуется потенциально сложной макросемантикой препроцессора С.

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

Технически говоря, так не должно быть. Понимая долгосрочные проблемы с использованием средств защиты #ifndef, разработчики библиотек Plan 9 выбрали другой, не-ANSI подход. В Plan 9 было запрещено содержать в заголовочных файлах дополнительные пункты #include; все #include должны были находиться в файле C верхнего уровня. Конечно, это требовало некоторой дисциплины - программист должен был перечислять необходимые зависимости ровно один раз, в правильном порядке - но документация помогла, и на практике это сработало очень хорошо. В результате, независимо от того, сколько зависимостей имел исходный файл C, каждый файл #include читался ровно один раз при компиляции этого файла. И, конечно же, было легко увидеть, нужен ли был #include, убрав его: отредактированная программа скомпилируется только тогда, когда зависимость не понадобится.

Наиболее важным результатом подхода Plan 9 была гораздо более быстрая компиляция: объем ввода-вывода, который требуется для компиляции, может быть значительно меньше, чем при компиляции программы с использованием библиотек с защитой #ifndef.

Однако вне Plan 9 "guards" подход является общепринятой практикой для C и C++. Фактически, C++ усугубляет проблему, используя тот же подход с более высокой степенью детализации. По соглашению, программы на C++ обычно структурированы с одним заголовочным файлом на класс или, возможно, небольшим набором связанных классов, группировка намного меньше, чем, скажем, <stdio.h>. Поэтому дерево зависимостей намного сложнее, отражая не библиотечные зависимости, а полную иерархию типов. Более того, заголовочные файлы C++ обычно содержат реальный код - объявления типа, метода и шаблона, а не только простые константы и сигнатуры функций, типичные для заголовочного файла C. Таким образом, C++ не только отправляет больше компилятору, но и то, что он отравляет, сложнее компилировать, и каждый вызов компилятора должен повторно обрабатывать эту информацию. При создании большого бинарного файла C++, компилятор мог бы тысячи раз научиться представлять строку, обрабатывая заголовочный файл <string>. (Для сведения, около 1984 года Том Каргилл заметил, что использование препроцессора C для управления зависимостями будет долгосрочным обязательством для C++ и должно быть решено.)

Создание одного бинарного файла C++ в Google может открывать и читать сотни отдельных заголовочных файлов десятки тысяч раз. В 2007 году инженеры по сборке в Google разработали сборку основного бинарного файла Google. Файл содержал около двух тысяч файлов, которые, если их просто объединить, составили 4,2 мегабайта. Ко времени расширения #include на вход компилятора было доставлено более 8 гигабайт, что составляет 2000 байт на каждый исходный байт C++.

В качестве другой точки данных, в 2003 году система сборки Google была перемещена из одного файла Makefile в дизайн для каждого каталога с более управляемыми, более явными зависимостями. Типичный бинарный файл сократился примерно на 40% по размеру файла, просто из-за более точной записи зависимостей. Несмотря на это, свойства C++ (или C в этом отношении) делают непрактичным автоматическую проверку этих зависимостей, и сегодня у нас все еще нет точного понимания требований к зависимостям больших бинарных файлов Google C++.

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

Даже с распределенной системой сборки большая сборка Google может занять много минут. Этот бинарный файл 2007 года занял 45 минут с использованием системы распределенной сборки предшественника; сегодняшняя версия той же программы занимает 27 минут, но, конечно, программа и ее зависимости за это время выросли. Инженерные усилия, необходимые для масштабирования системы сборки, едва ли могли опередить рост создаваемого программного обеспечения.


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


Golang в Google: дизайн языка на службе разработки программ, часть 3

Rob Pike, 2012

Продолжение, начало в части 1 и части 2.

9. Синтаксис

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

Таким образом, Go был разработан с ясностью и удобством и имеет чистый синтаксис. По сравнению с другими языками в семействе C его грамматика скромна по размеру: всего 25 ключевых слов (C99 - 37; C++ 11 - 84; цифры продолжают расти). Что еще более важно, грамматика является правильной и поэтому легко разбирается (в основном; есть несколько причуд, которые мы могли бы исправить, но не обнаружили достаточно рано). В отличие от C и Java и особенно C++, Go может быть проанализирован без информации о типе или таблицы символов; нет конкретного типа контекста. Грамматику легко разъяснить, и поэтому инструменты легко писать.

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

var fn func([]int) int
type T struct { a, b int }

по сравнению с C

int (*fn)(int[]);
struct T { int a, b; }

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

var buf *bytes.Buffer = bytes.NewBuffer(x) // явное
buf := bytes.NewBuffer(x)                  // производное

Синтаксис функции прост для простых функций. В этом примере объявляется функция Abs, которая принимает одну переменную x типа T и возвращает единственное значение float64:

func Abs(x T) float64

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

func (x T) Abs() float64

А вот переменная (замыкание) с аргументом типа T; Go имеет функции первого класса и замыкания:

negAbs := func(x T) float64 { return -Abs(x) }

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

func ReadByte() (c byte, err error)

c, err := ReadByte()
if err != nil { ... }

Мы поговорим об ошибках позже.

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

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

10. Именование

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

  • заглавная буква: Name видно клиентам пакета
  • в противном случае: name (или _Name) не видно клиентам пакета

Это правило распространяется на переменные, типы, функции, методы, константы, поля - на все. Это все, что нужно сделать.

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

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

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

  • всеобъемлящая (universe) (предварительно объявленные идентификаторы, такие как int и string)
  • пакет (все исходные файлы пакета находятся в одной области видимости)
  • файл (только для импорта пакетов переименовывает; на практике это не очень важно)
  • функция (обычная)
  • блок (обычный)

Нет пространства для пространства имен, класса или другой оберточной конструкции. Имена происходят из очень немногих мест в Go, и все имена соответствуют одной и той же иерархии областей видимости: в любом заданном месте в исходном коде идентификатор обозначает ровно один языковой объект, независимо от того, как он используется. (Единственное исключение - метки утверждений, цели утверждений break и т.п.; они всегда имеют область видимость функции.)

Это имеет последствия для ясности. Обратите внимание, например, что методы объявляют явного получателя и что он должен использоваться для доступа к полям и методам типа. Там нет неявного this. То есть всегда пишут

rcvr.Field

(где rcvr - это любое имя, выбранное для переменной-получателя), поэтому все элементы типа всегда отображаются лексически связанными со значением типа-получателя. Точно так же квалификатор пакета всегда присутствует для импортированных имен; всегда пишут io.Reader, а не Reader. Это не только ясно, но и освобождает идентификатор Reader как полезное имя для использования в любом пакете. На самом деле в стандартной библиотеке есть несколько экспортируемых идентификаторов с именем Reader или Printf, но какой из них упоминается, всегда однозначно.

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

Кратко, имена локальные. В C, C++ или Java имя y может относиться к чему угодно. В Go y (или даже Y) всегда определяется внутри пакета, в то время как интерпретация x.Y ясна: найдите x локально, Y принадлежит ему.

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

Следует упомянуть еще один аспект именования: поиск метода всегда осуществляется только по имени, а не по сигнатуре (типу) метода. Другими словами, у одного типа никогда не может быть двух методов с одинаковым именем. Учитывая метод x.M, существует только один M, связанный с x. Опять же, это позволяет легко определить, к какому методу относится только имя. Это также упрощает реализацию вызова метода.

11. Семантика

Семантика операторов Go обычно C-подобна. Это компилируемый, статически типизированный, процедурный язык с указателями и так далее. По замыслу он должен чувствовать себя знакомым программистам, привыкшим к языкам в семействе С. При запуске нового языка важно, чтобы целевая аудитория могла быстро его выучить; укоренение Go в семействе C помогает молодым программистам, большинство из которых знают Java, JavaScript и, возможно, C, находить Go легким в освоении.

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

  • нет арифметики указателей
  • нет неявных числовых преобразований
  • границы массива всегда проверяются
  • нет псевдонимов типов (после типа X int, X и int являются разными типами, а не псевдонимами)
  • ++ и -- являются утверждениями, а не выражениями
  • присваивание не является выражением
  • законно (даже рекомендуется) брать адрес стековой переменной
  • и многое другое

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

  • конкурентности
  • сборки мусора
  • типов интерфейсов
  • отражения (reflection)
  • переключателей типа

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


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


Golang в Google: дизайн языка на службе разработки программ, часть 4

Rob Pike, 2012

Продолжение, начало в части 1, части 2, части 3.

12. Конкурентность

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

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

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

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

Таким образом, CSP является практичным для Go и для Google. При написании веб-сервера, канонической программы Go, модель отлично подходит.

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

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

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

13. Сборка мусора

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

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

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

Язык намного проще в использовании из-за сборки мусора.

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

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

Ключевым моментом является то, что Go предоставляет программисту инструменты для ограничения выделения путем управления макетом структур данных. Рассмотрим определение простого типа структуры данных, содержащей буфер (массив) байтов:

type X struct {
    a, b, c int
    buf [256]byte
}

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

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

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

Чтобы дать программисту такую ​​гибкость, Go должен поддерживать то, что мы называем внутренними указателями на объекты, расположенные в куче. Поле X.buf в приведенном выше примере находится внутри структуры, но допустимо захватить адрес этого внутреннего поля, например, чтобы передать его в процедуру ввода-вывода. В Java, как и во многих языках со сборщиком мусора, невозможно создать внутренний указатель, подобный этому, но в Go это идиоматично. Эта точка проектирования влияет на то, какие алгоритмы сбора данных можно использовать, и может усложнить их, но после тщательного обдумывания мы решили, что необходимо разрешить внутренние указатели из-за преимуществ для программиста и способности снизить нагрузку (возможно, сложнее реализовать) на сборщик. Пока что наш опыт сравнения похожих программ на Go и Java показывает, что использование внутренних указателей может оказать существенное влияние на общий размер арены, задержку и время сбора.

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

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


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


Golang в Google: дизайн языка на службе разработки программ, часть 5

Rob Pike, 2012

Продолжение, начало в части 1, части 2, части 3, части 4.

14. Композиция, а не наследование

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

Вместо этого у Go есть интерфейсы, идея, которая далее кратко описана.

В Go интерфейс - это просто набор методов. Например, вот определение интерфейса Hash из стандартной библиотеки.

type Hash interface {
    Write(p []byte) (n int, err error)
    Sum(b []byte) []byte
    Reset()
    Size() int
    BlockSize() int
}

Все типы данных, которые реализуют эти методы, неявно удовлетворяют этому интерфейсу; нет implements декларации. Тем не менее, удовлетворенность интерфейса статически проверяется во время компиляции, поэтому отвязывающие (decoupling) интерфейсы безопасны для типов.

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

type Writer interface {
    Write(p []byte) (n int, err error)
}

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

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

Одним из крайних примеров является ядро ​​Plan 9, в котором все элементы данных системы реализованы точно в одном интерфейсе, API файловой системы, определенный 14 методами. Это единообразие допускало уровень композиции объектов, редко достигаемый в других системах, даже сегодня. Примеров предостаточно. Вот один из них: система может импортировать (в терминологии Plan 9) стек TCP на компьютер, который не имеет TCP или даже Ethernet, и через эту сеть подключиться к машине с другой архитектурой процессора, импортировать свое дерево /proc и запустить локальный отладчик для отладки точки останова удаленного процесса. Такая операция была обычным делом для Plan 9, ничего особенного. Способность делать такие вещи выпала из дизайна; это не требовало особой договоренности (и все было сделано на простом C).

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

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

Рассмотрим интерфейс Writer, показанный выше, который определен в пакете io: любой элемент, имеющий метод Write с этой сигнатурой, хорошо работает с дополнительным интерфейсом Reader:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Эти два взаимодополняющих метода позволяют создавать безопасные цепочки с богатым поведением, как обобщенные каналы Unix. Файлы, буферы, сети, шифраторы, компрессоры, кодировщики изображений и т.д. Могут быть соединены вместе. Процедура ввода-вывода в формате Fprintf использует io.Writer, а не, как в C, FILE*. Отформатированный принтер не знает, о чем пишет; это может быть кодировщик изображения, который, в свою очередь, пишет в компрессор, который, в свою очередь, пишет в шифратор, который, в свою очередь, пишет в сетевое соединение.

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

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

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

func ReadAll(r io.Reader) ([]byte, error)

Оболочки (wrappers) - функции, которые принимают интерфейс и возвращают интерфейс - также широко распространены. Вот несколько прототипов. LoggingReader регистрирует каждый вызов Read на входящем Reader. LimitingReader прекращает чтение после n байтов. ErrorInjector помогает тестировать, имитируя ошибки ввода/вывода. И еще много других.

func LoggingReader(r io.Reader) io.Reader
func LimitingReader(r io.Reader, n int64) io.Reader
func ErrorInjector(r io.Reader) io.Reader

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

15. Ошибки

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

Ключевой особенностью языка для обработки ошибок является предопределенный тип интерфейса, называемый error, который представляет значение, которое имеет метод Error, возвращающий строку:

type error interface {
    Error() string
}

Библиотеки используют тип error, чтобы вернуть описание ошибки. В сочетании с возможностью для функций возвращать несколько значений, легко вернуть вычисленный результат вместе со значением ошибки, если оно есть. Например, эквивалентный getchar в C не возвращает внеполосное значение в EOF и не генерирует исключение; он просто возвращает значение ошибки рядом с символом, при этом нулевое значение ошибки означает успех. Вот сигнатура метода ReadByte типа bufio.Reader буферизованного пакета ввода-вывода:

func (b *Reader) ReadByte() (c byte, err error)

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

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

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

f, err := os.Open(fileName)
if err != nil {
    return err
}

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

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

16. Инструменты

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

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

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

Наиболее известным из них является gofmt, форматировщик исходного кода Go. С самого начала проекта мы планировали форматировать программы Go на компьютере, исключая целый класс споров между программистами: как мне выложить свой код? Gofmt запускается на всех программах Go, которые мы пишем, и большая часть сообщества с открытым исходным кодом также использует его. Он запускается как "предварительная проверка" для репозиториев кода, чтобы удостовериться, что все отмеченные в Go программы отформатированы одинаково.

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

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

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

gofmt -r 'a[b:len(a)] -> a[b:]'

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

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

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

gofix

Обратите внимание, что эти инструменты позволяют нам обновлять код, даже если старый код все еще работает. В результате, репозитории Go легко обновляются по мере развития библиотек. Старые API могут быть устаревшими быстро и автоматически, поэтому необходимо поддерживать только одну версию API. Например, мы недавно изменили реализацию буфера протокола Go, чтобы использовать функции "getter", которых раньше не было в интерфейсе. Мы запустили gofix для всего кода Go в Google, чтобы обновить все программы, использующие буферы протокола, и теперь используется только одна версия API. Подобные радикальные изменения в библиотеках C++ или Java практически невозможны в масштабах кодовой базы Google.

Наличие пакета синтаксического анализа в стандартной библиотеке Go также позволило использовать ряд других инструментов. Примеры включают инструмент go, который управляет построением программы, включая получение пакетов из удаленных репозиториев; программа извлечения документов godoc, программа для проверки соблюдения соглашения о совместимости API при обновлении библиотеки и многое другое.

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

Заключение

Использование Go растет внутри Google.

Его используют несколько крупных пользовательских служб, в том числе youtube.com и dl.google.com (сервер загрузки, который обеспечивает загрузку Chrome, Android и других загрузок), а также наш собственный golang.org. И, конечно же, многие маленькие делают это, в основном, с использованием встроенной поддержки Google App Engine для Go.

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

  • BBC Worldwide
  • Canonical
  • Heroku
  • Nokia
  • SoundCloud

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

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

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

Однако не все было исправлено. Мы все еще учимся (но язык пока зафиксирован).

Существенным недостатком языка является то, что реализация все еще нуждается в работе. Сгенерированный код компилятора и производительность среды выполнения, в частности, должны быть лучше, и работа над ними продолжается. Уже есть прогресс; на самом деле, некоторые тесты показывают удвоение производительности в версии для разработчиков сегодня (конец 2012 года) по сравнению с первым выпуском Go версии 1 в начале 2012 года.

Резюме

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

Свойства, которые привели к этому, включают:

  • ясные зависимости
  • ясный синтаксис
  • ясная семантика
  • композиция, а не наследование
  • простота, обеспечиваемая моделью программирования (сборка мусора, конкурентность)
  • легкие инструменты (инструмент go, gofmt, godoc, gofix)

Если вы еще не попробовали Go, мы советуем вам это сделать.


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