Go. Обзор языка

Представленный ниже текст – компиляция доклада, посвященного языку Go.  Доклад был представлен на конференции Chebit`13, состоявшейся 21 декабря 2013 года.

Немного истории

Язык Go – системный язык общего назначения. Его разработка была начата в 2007 году в компании Google. Впервые широкая публика узнала о языке в 2009 году, именно тогда он был анонсирован. Версия 1.0 была выпущена в марте 2012 года, в ней спецификация языка была зафиксирована, были даны гарантии, что в последующих версиях ветки 1.х не будет никаких изменений, делающих код пользователей нерабочим. На данный момент текущей является версия 1.2, выпущенная 1 декабря этого года.

Прежде чем перейти к описанию целей, которые преследовались при создании языка, необходимо сказать пару слов о его создателях. Их имена должны быть известны многим программистам, особенно тем, кто много писал или пишет на С. Это Кен Томпсон – один из создателей Unix и UTF-8. Второй из создателей UTF-8, Роб Пайк, также является одним из создателей Go. Роб Пайк также является одним из основных разработчиков ОС Plan 9. Третий создатель языка – Роберт Гриземер, менее известен.

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

Требования к языку

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

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

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

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

Особенности Go

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

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

  • компилируемый язык;
  • статически типизированный;
  • поддерживает параллелизм;
  • простой (всего 25! ключевых слов);
  • полностью поддерживает utf-8.

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

Пример кода.

Первое, что бросается в глаза, когда только начинаешь знакомиться с языков – это обработка ошибок. Функции в Go могут возвращать несколько значений. Одно из этих значений часто объект типа error. В Go нет исключений, функции могут вернуть ошибку, их обработка полностью ложится на плечи пользователя. В первое время это вызывает недоумение. Например, код функции, открывающей файл на чтение и пишущей в него какие-то данные, на половину может состоять из кода, проверяющего ошибки и каким-либо образом реагирующего на него. Конечно, ошибки можно игнорировать, однако это может привести к печальным последствиям. Недаром раз в несколько месяцев в рассылке go-nuts возникает тема, основной смысл которой можно свести к следующему – “я уже несколько пишу на Go, язык мне очень нравится, но считаю, что почему нет исключений”. Думаю, многие согласятся с тем, что управление логикой поведения через исключения не является лучшей практикой, может быть поэтому разработчики Go твердо стоят на существующем положении дел. В ходе разработки нескольких приложений на Go мое отношение к практике постоянной проверки возвращаемых функцией значений сменилось с отрицательной на положительно-нейтральное. Понимание того, что код может выполниться с ошибкой приводит к тому к более тщательному размышлению над кодом, что не может не привести к повышению его качества.

Пример работы с ошибками в Go.

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

Пример работы с функциями

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

Пример использования интерфейсов

Говоря об интерфейсах, нельзя не сказать пару слов о пустом интерфейса – interface{}. Это интерфейсу соответствуют все объекты в Go. Его можно рассматривать как указатель на void, если говорить в терминах С. Использование этого интерфейса вместе со пакетом reflect, входящим в состав стандартной библиотеки, дает возможность получать информацию об объекте – его тип, поля, методы – во время выполнения. Это дает возможность писать код, универсально работающий со всеми типами данных, примеры использования можно посмотреть, например, в пакете encoding/json, метод Unmarshal которого может преобразовать JSON данные в объект нужного вам типа.

Пример использования interface{}.

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

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

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

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

Инструментарий Go

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

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

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

Еще один аспект программирования, определенно влияющий на качество кода – тестирование, также не обделен в Go вниманием. Во-первых, в состав стандартной библиотеки входит пакет testing, которые предоставляет базовые инструменты для написания тестов. Во-вторых, для запуска тестов есть утилита gotest. А в текущей версии (на декабрь 2013 года – это версия 1.2), появилась возможность получить отчет о том, какой код покрыт тестами, а какой нет.

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

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