Программирование на Go для начинающих | OTUS

Программирование на Go для начинающих

Go_Deep_14.8-5020-3ed31e.png

Многие из нас привыкли считать, что действительно универсальных языков не бывает. Если нам нужна эффективность, мы используем «Си» и одновременно с этим миримся с его ограничениями.

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

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

Язык Go — это «Си» на стероидах?

Когда 10 лет тому назад Кена Томпсона, который активно участвовал в создании языка «Си», спросили, а каковым бы он сделал данный язык на тот момент, разработчик ответил, что он стал бы похож на Limbo. После этого прошло достаточно много времени, и Томпсон вместе с Робом Пайком (другой автора языка C) приняли участие в создании Go, ставшего в каком-то смысле переосмыслением и результатом развития Limbo. Таким образом, как только Go представили миру, он сразу стал бестселлером. И произошло это 10 ноября 2009 года.

Во многом успеху способствовали имена авторов, известных в качестве создателей операционной системы UNIX, кодировки UTF-8, языка «Си». Сыграло свою роль и покровительство Google, так как именно в лабораториях этой корпорации родился на свет Go.

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

«Си» сегодняшнего дня

Разработчики Go позиционируют его как системный язык, который сочетает в себе скорость исполнения и эффективность кода, написанного на C, с простотой разработки, присущей более высокоуровневым скриптовым языкам, включая языки, имеющие встроенные средства параллельного программирования. При этом если говорить о внешних признаках, то здесь Go напоминает нам некую странную солянку, состоящую из синтаксисов C, Pascal и ADA. В совокупности с приведенным выше описанием создаётся сильное ощущение некого подвоха (так бывает, если видишь новость о супер-мега-разработке группы студентов из Урюпинска). Но скепсис достаточно быстро исчезает, когда ты начинаешь изучать Go и понимаешь, почему он стал именно такой, какой есть сейчас.

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

Хотите знать, что всё это значит в реальности? Что же, давайте разберёмся по каждому пункту в отдельности.

Производительность

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

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

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

Множество других элементов языка, которые не имеют прямого отношения к синтаксису, тоже были заранее оптимизированы. Например, язык не содержит механизма неявного приведения типов, а это оберегает программиста от ошибок, позволяя сделать проще компилятор. Также в языке отсутствует полноценная реализация классов с их полиморфизмом и наследованием. Что касается механизма параллельной разработки, то он задействует собственную реализацию потоков внутри любой программы — это делает потоки настолько лёгкими, что можно сказать, что их создание обходится почти даром. Весьма проворен и встроенный сборщик мусора — в языке попросту нет элементов, способных усложнить его работу. А в стандартную поставку Go включены плагины для любых популярных средств программирования, включая Vim.

Простота в разработке и сопровождении

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

Однако основное, что даёт Go разработчику, есть та самая очевидность синтаксиса и прямолинейность, о которой мы уже упоминали. В этом смысле Go напоминает такие языки, как Modula, Pascal и Oberon. И почти любой синтаксический элемент языка соответствует общей логике, поэтому может явно и безошибочно интерпретироваться вне зависимости от положения в коде. Например, вы просто не сможете сделать известную ошибку объявления переменных, которая описана во всех гайдах по стилю оформления кода на «Си»:

int* a, b; // В Си и C++ переменная "a" будет указателем, но "b" — нет
var a, b *int; // В Go обе переменные будут указателями

В общем Go создан разработчиками для разработчиков. И проявляется данное обстоятельство во всём, начиная с обрамления блоков кода по аналогии со стилем «Си», неявного объявления типов, отсутствия необходимости проставлять точку с запятой после каждого выражения и заканчивая отсутствием механизма исключений и полноценных классов (создавались они, разумеется, для упрощения жизни, однако на практике часто становятся причиной запутывания кода).

И главная идея языка заключается в том, чтобы быть инструментом, позволяющим писать программы, а не постоянно думать о том, а будут ли они вообще работать (эта черта, как известно, присуща для таких языков, как «Си» и С++).

Средства параллельного программирования

Это одна из самых сильных черт Go. Пожалуй, среди языков общего назначения «голангу» просто нет равных (кроме, разве что, Limbo, однако он привязан к операционной системе Inferno).

Выигрыш тут состоит не только в том, что средства встроены. Намного большее значение имеет факт, что эти средства реализуют простую и эффективную модель, которая полностью соответствует CSP — теории взаимодействующих последовательных процессов. Те, кто знаком с Occam и Limbo, хорошо понимают все плюсы CSP, а кто не знаком, сейчас поясним. Смотрите, вместо того, чтобы нагромождать потоки, мьютексы, блокировки и прочие системы синхронизации, делающие параллельное программирование настоящей мукой и приводящие и переизданию многостраничных томов о том, как правильно писать многопоточные приложения, у нас существует CSP. Его автор, Тони Хоар, предлагает элегантное и довольно простое решение: позволить приложению в любое время создать новую нить, которая будет иметь возможность общаться с родителями и прочими нитями посредством отправки синхронных сообщений.

Если говорить о Go, то в данном случае эта идея выглядит следующим образом: 1. Создаётся переменная-канал. 2. Определяется функция, принимающая переменную-канал в виде аргумента и в своём теле содержащая код, который выполняется в отдельной нити. В конце эта функция отправляет результат выполнения в канал, что делается благодаря специальному оператору. 3. Функция запускается в отдельном потоке посредством ключевого слова «go». 4. Выполняется чтение из канала.

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

Пример

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

     package main

     import "time"
     import "fmt"

     func timer(ch chan string, ns, count int) {
         for j := 1; j <= count; j++ {
             time.Sleep(int64(ns))
             if j == count {
                fmt.Printf("[timer] Отправляю последнее сообщение...n")
                ch <- "стоп!"
            } else {
                fmt.Printf("[timer] Отправляю...n")
                ch <- "продолжаем"
            }
            fmt.Printf("[timer] Отправил!n")
        }
     }

     func main() {
         var str string

         ch := make(chan string)
         go timer(ch, 1000000000, 10)

         for {
             fmt.Printf("[main] Принимаю...n")
             str = <-ch
             if str == "стоп!" {
                 fmt.Printf("[main] Принял последнее сообщение, завершаю работу.n")
                 return
             } else {
                 fmt.Printf("[main] Принято!n")
             }
         }
     }

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

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
1 комментарий
-3

Пока в Go не появятся генерики, не о чем говорить :)

Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто