Почему язык Go плох для НЕумных программистов -20


Статья написана, как ответ на опубликованную ранее статью-антипод.


image


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


Создан для слабых программистов?


Слабые говорят о проблемах. Сильные говорят об идеях и мечтах…

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


Даже начинающие программисты начинают выдавать вполне приличный код спустя неделю-другую. Книга, по которой я изучал Go называется “Программирование на языке Go” (автор Марк Саммерфилд). Книга весьма хороша, в ней затрагиваются многие нюансы языка. После неоправданно усложненных языков таких, как Java, PHP, отсутствие магии действует освежающе. Но рано или поздно у многих ограниченных программистов возникает желание использовать старые методы на новом поприще. Действительно ли это так необходимо?


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


Так для чего же он был создан таким простым? Вот пара цитат Роба Пайка:


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

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

Мудрые слова, не правда ли?


Артефакты простоты


Простота — необходимое условие прекрасного. Лев Толстой.

Быть простым — это одно из важнейших стремлений в любом дизайне. Как известно, совершенный проект это не тот проект, куда нечего добавить, а тот – в из которого нечего удалить. Многие считают, что для того, чтобы решить (или даже выразить) сложные задачи, необходим сложный инструмент. Однако, это не так. Возьмем к примеру язык PERL. Идеологи языка считали, что программист должен иметь как минимум три разных пути для решения одной задачи. Идеологи языка Go пошли другим путем, они решили, что для достижения цели достаточно одного пути, но действительно хорошего. Такой подход имеет под собой серьезный фундамент: единственный путь легче учится и тяжелей забывается.


Многие мигранты жалуются, что язык не содержит элегантных абстракций. Да, это так, однако это и есть одно из главных достоинств языка. Язык содержит в своем составе минимум магии – поэтому не требуется глубоких знаний для чтения программы. Что же касается многословности кода, то это и вовсе не проблема. Хорошо написанная программа на языке Golang читается по вертикали, практически без структурирования. Кроме того, скорость чтения программы как минимум на порядок превосходит скорость ее написания. Если учесть, что весь код имеет единообразное форматирование (выполненное при помощи встроенной команды gofmt), то прочитать несколько лишних строк вообще не является проблемой.


Не очень выразительный


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

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


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


package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

Решение этой же задачи на языке D хотя и выглядит несколько короче, однако, читается ничуть не проще


import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

Ад копирования


Человек носит ад в самом себе. Мартин Лютер.

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


package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

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


package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a - b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

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


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


import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

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


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


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


Обобщенное программирование в ряде случаев дает нам неоспоримую выгоду. Это наглядно иллюстрирует нам пакет sort. Так, для сортировки любого списка нам достаточно реализовать интерфейс sort.Interface.


import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

Если Вы возьмете любой open source проект и выполните команду grep "interface{}" -R, то увидите, как часто используются путые интерфейсы. Недалекие товарищи сразу же скажут, что все это из-за отсутствия дженериков. Однако, это далеко не всегда так. Возьмем к примеру язык DELPHI. Несмотря на наличие у него этих самых дженериков, он содержит специальный тип VARIANT для операций с произвольными типами данных. Аналогично поступает и язык Go.


Из пушки по воробьям


И смирительная рубашка должна соответствовать размеру безумия. Станислав Лец.

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


Роб Пайк предупреждает нас:


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

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


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

Однако, как известно, за все необходимо платить. В данном случае это:


  • сложность написания программ
  • скорость исполнения программ

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


Культурный багаж из Си? Нет, из ряда языков!


Вместе с состоянием наследникам оставляют и долги.

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


Синтаксис


Прежде всего, синтаксис грамматических конструкций основывается на синтаксисе языка Си. Однако, существенное влияние оказал и язык DELPHI. Так, мы видим, что полностью убраны избыточные скобки, так сильно снижающие читаемость программы. Также язык содержит оператор ":=", присущий языку DELPHI. Понятие пакетов заимствовано из языков, подобных ADA. Декларация неиспользуемых сущностей заимствована из языка PROLOG.


Семантика


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


Операция реализации методом делегирования была заимствована из языка DELPHI.


Компиляция


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


Высокоуровневые конструкции


Язык содержит множество различных высокоуровневых конструкций, никак не связанных с низкоуровневыми языками типа Си.


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

Управление памятью


Управление памятью вообще заслуживает отдельной статьи. Если в языках типа C++, управление полностью отдано на откуп разработчика, то в более поздних языках типа DELPHI, была использована модель подсчета ссылок. При таком подходе не допускалось циклических ссылок, поскольку образовывались потерянные кластера, то в Go встроено детектирование таких кластеров (как в C#). Кроме того, по эффектвности garbage collector превосходит большинство известных на текущий момент реализаций и уже может быть использован для многих real time задач. Язык сам распознает ситуации, когда значение для хранения переменной может быть выделено в стеке. Это уменьшает нагрузку на менеджер памяти и повышает скорость работы программы.


Параллельность и конкурентность


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


Дополнительные выгоды


Если это будет выгодно — бескорыстными станут все.

Язык также предоставляет нам также ряд несомненных выгод:


  • Единственный исполнимый файл после сборки проекта существенно упрощает deploy приложения.
  • Статическая типизация и вывод типов позволяют существенно сократить число ошибок в коде даже без написания тестов. Я знаю некоторых программистов, которые вообще обходятся без написания тестов и при этом качество их кода существенно не страдает.
  • Очень простая кросс-компиляция и отличная портабельность стандартной библиотеки, что сильно упрощает разработку кросс-платформенных приложений.
  • Регулярные выражения RE2 потокобезопасные и с предсказуемым временем выполнения.
  • Мощная стандартная библиотека, что позволяет в большинстве проектов обходиться без сторонних фреймворков.
  • Язык достаточно мощный, чтобы концентрироваться на задаче, а не на методах ее решения и в то же время достаточно низкоуровневый, чтобы задачу можно было решить эффективно.
  • Эко система Go содержит уже из коробки развитый инструментарий на все случаи жизни: тесты, документация, управление пакетами, мощные линтеры, кодогенерация, детектор race conditions и т. д.
  • У Go версии 1.11 появилась встроенное семантическое управления зависимостями, построенное поверх популярных хостингов VCS. Все инструменты, входящие в состав экосистемы Go используют эти сервисы, чтобы скачивать, собирать и устанавливать из них код одним махом. И это здорово. С приходом версии 1.11 также полностью разрешилась проблема с версированием пакетов.
  • Поскольку основной идеей языка является уменьшение магии, язык стимулирует разработчиков выполнять обработку ошибок явно. И это правильно, поскольку в противном случае, он просто будет забывать вообще про обработку ошибок. Другое дело, что большинство разработчиков сознательно игнорирую обработку ошибок, предпочитая вместо их обработки, просто пробрасывать ошибку вверх.
  • Язык не реализует классической методологии ООП, поскольку в чистом виде в Go нет виртуальности. Однако, это не является проблемой при использовании интерфейсов. Отсутствие ООП существенно снижает входной барьер для новичков.

Простота для выгоды сообщества


Усложнять просто, упрощать сложно.

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


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


Заключение


Большой размер мозга еще не сделал ни одного слона лауреатом Нобелевской премии.

Для тех программистов, у которых личное эго превалирует над командным духом, а также теоретиков, которые любят академические задачи и бесконечное "самосовершенствование", язык действительно плох, поскольку это ремесленнический язык общего назначения, не позволяющий получить эстетического удовольствия от результата своей работы и показать себя профессионалом перед коллегами (при условии, что мы измеряем ум именно этими критериями, а не коэффициентом IQ). Как и все в жизни — это вопрос личных приоритетов. Как и все стоящие новшества, язык уже проделал достаточный путь от всеобщего отрицания к массовому признанию. Язык гениален по своей простоте, а, как известно, все гениальное — просто!


Резюме


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


  • Нет дженериков. Если мы взглянем на статистику самых востребованных языков то заметим, что половину языков из верхней десятки не имеют дженериков. Преимущественно дженерики нужны только в контейнерах. Поэтому выигрыш от них не слишком большой.
  • Другие языки типа Rust много лучше (по крайней по номинациям сайта XXX). Опять же, если мы взглянем на статистику самых востребованных языков, то мы вообще не обнаружим язык Rust в списке или же он будет где-то внизу рейтинга. Лично мне, Rust нравится, но я выбрал Go.
  • У языка XXX есть вот такая плюшка. Это обратная сторона медали простоты. Недостаток это или нет решать каждому. Однако, разработчики проекта отдали свои предпочтения в пользу простоты.
  • Вот выпустят Go 2.0, тогда и посмотрим. Такую позицию занимают наблюдатели, а не практики.
  • Не достаточно выразительный. Согласен, на некоторых участах выразительность хромает, однако в целом это простой и непротиворечивый язык. Кроме того, из-за бедности языка, мы вынуждены большее внимание уделять архитектуре разрабатываемого приложения, что позитивно сказыввается на его гибкости.

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


Эксперимент


Не верь словам — ни своим, ни чужим, а верь делам – и своим и чужим.

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


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


  • Действительно, гипотеза моих знакомых подтвердилась, однако, среди хабражителей еще встречаются адекватные люди, хотя процент их стремительно падает. Юрий Быков называет таких людей “дураками”, на которых держится вся страна. По его версии, их процент невелик (около 2%). Я не так пессиместичен и считаю, что их значительно больше.
  • Закон СМИ. Деструктивная информация вызывает значительно больший интерес, чем конструктивная.
  • Психология толпы. Это ужасная вещь, она даже из адекватного человека делает жестокого барана. Человек в толпе – это уже не человек. Ни о какой объективности не может быть и речи. Никакие логические доводы, никакие авторитетные источники или прецеденты на него уже не действуют.
  • Ответсвенность и безнаказанность. Люди с удовольствием готовы унизить другого, чтобы возвеличить себя (хотя бы в собственных глазах). Особенно, если за это не придется отвечать (что может быть проще — нажал минус и даже не требуется писать комментарий). Между словами и делами остается столько же общего, как между каналом и канализацией.
  • Тщеславие. Большинство снобов готовы выделиться любым способом. Никакие моральные преграды им не страшны.
  • Пессимизм. В отличие от западных стран (и тем более Америки), в стране превалируют пессимистические настроения. Как известно, оптимист ищет возможности среди сложностей, а пессимист — сложности среди возможностей. У нас в стране, практически никто, не обращает внимание на положительные качества чего бы то ни было.
  • Профессионализм и круг мировоззрения. Большинство людей выбирает инструменты, как самоцель, а не как средство достижения поставленной цели. Люди разучились работать с информацией. Люди не видят за деревьями леса. Из массива информации они не в состоянии выделять главные мысли. Никто не хочет взглянуть с другой, не стандартной для себя, точки зрения. Инакомыслие подавляется. Здесь так не принято.
  • Дружность и уважение. Хваленые дружные коллективы существуют только на словах. Ценности Agile разработки — только на бумаге.
  • Лицемерие. Об этом вообще можно написать отдельную статью.
  • Принципиальность. Есть люди, которые задаются правильным вопросом: “Что я вообще нахрен делаю?”, однако не все понимают, что из-за отстутствия приципиальности для нас сиюминутный шкурный интерес важней, чем все наши принципы вместе взятые. Легче всего свалить все на обстоятельства и сказать, что от нас ничего не зависит.

С глубоким уважением и сочуствием ко всем конструктивно мыслящим оптимистам.


Adverax.




К сожалению, не доступен сервер mySQL