[Перевод] Почему дизайн Go плох для умных программистов

habr.png

На протяжении последних месяцев я использую Go для имплементаций Proof of Concept (прим.пер.: код для проверки работоспособности идеи) в свободное время. Отчасти это было для изучения самого языка программирования. Программы сами по себе очень просты и не являются целью статьи, но сам опыт использования Go заслуживает того, чтобы сказать о нем пару слов. Go обещает быть (прим.пер.: статья написана в 2015) массовым языком для серьезного масштабируемого кода. Язык создан в Google, в котором активно им пользуются. Подведя черту, я искренне считаю, что дизайн языка Go плох для умных программистов.


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


Go очень просто научиться, настолько просто, что введение заняло у меня один вечер, после чего уже мог продуктивно писать код. Книга по которой я изучал называется An Introduction to Programming in Go (перевод), она доступна в сети. Книгу, как и сам исходный код на Go, легко читать, в ней есть хорошие примеры кода, и содержит порядка 150 страниц, которые можно прочесть за раз. Изначальная эта простота действует освежающе, особенно в мире программирования, полного переусложненных технологий. В итоге рано или поздно возникает мысль: «Так ли это на самом деле?»


Google утверждает, что простота Go — это подкупающая черта и она предназначена для максимальной продуктивности в больших командах, но я сомневаюсь в этом. Есть фичи, которых либо недостает, либо чрезмерно подробны. А все из-за отсутствия доверия к разработчикам, полагая, что они не в состоянии сделать что-либо правильно. Это стремление к простоте было сознательным решением, сделанное разработчиками языка и, для того, чтобы полностью понять для чего это нужно, мы должны понять мотивацию разработчиков и чего они добивались в Go.
Так для чего же он был создан таким простым? Вот пара цитат Роба Пайка (прим.пер.: один из соавторов языка Go):


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


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

Что? Так Роб Пайк в сущности говорит, что разработчики в Google не столь хороши, потому они создали язык для идиотов (прим.пер.: dumbed down), так чтобы они были в состоянии что-то сделать. Что за высокомерный взгляд на собственных коллег. Я всегда считал, что разработчики Google отобраны из самых ярких и лучших на Земле. Конечно они могут справиться с чем-то посложнее?


Артефакты чрезмерной простоты


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


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


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


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


package main

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

func main() {

    flag.Parse()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

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

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

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

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

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

    fmt.Println(text)
}


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


Вот, к примеру, решение той же задачи на 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);
    }
}


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


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


Популярная функциональность в предложениях улучшения 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 int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

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

func main() {

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

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}


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


Тот же пример на D:


import std.stdio;
import std.algorithm;

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


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


Простой обход системы типов


Я полагаю читая это, программисты Go будут с пеной во рту кричать: «Ты делаешь это не так!». Что же, есть еще один способ сделать общую функцию и типы, но это полностью разрушает систему типов!


Взгляните на этот пример глупого исправления языка для обхода проблемы:


package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}


Эта имплементация Reduce была позаимствована из статьи Idiomatic generics in Go (прим.пер.: перевод не нашел, буду рад, если поможете с этим). Что же, если это идиоматично, я бы не хотел увидеть не идиоматичный. Использование interface{} — фарс, и в языке он нужен лишь для обхода типизации. Это пустой интерфейс и все типы его реализуют, позволяя полную свободу для всех. Этот стиль программирования до ужаса безобразен, и не только это. Для подобных акробатических трюков требуется использовать рефлексию времени выполнения. Даже Робу Пайку не нравятся индивиды, злоупотребляющие этим и упоминал это в одном из своих докладов.


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

Я бы взял шаблоны D вместо этой чепухи. Как кто-то может сказать, что interface{} более читаем или даже типобезопасен?


Горе управления зависимостями


У Go есть встроенная система зависимостей, построенная поверх популярных хостингов VCS. Поставляемые с Go инструменты знают об этим сервисах и могу скачивать, собирать и устанавливать из них код одним махом. Хотя это и здорово, есть крупная оплошность с версионированием! Да действительно, можно получить исходный код из сервисов вроде github или bitbucket с помощью инструментов Go, но нельзя указать версию. И снова это кричит про простоту в ущерб полезности. Я не в состоянии понять логику за подобным решением.


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


Культурный багаж из Си


По-моему мнению, Go был разработан людьми, которые использовали Си всю свою жизнь и теми, кто не хотел попытаться использовать что-то новое. Язык можно описать как Си с дополнительными колесиками (ориг.: training wheels). В нем нет новых идей, кроме поддержки параллелизма (который, кстати, прекрасен) и это так обидно. У вас есть отличная параллельность в едва ли годном к употреблению, хромающем языке.


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


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


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


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

© Habrahabr.ru