Роб Пайк об истории появления Go: less is more

Что: 2e27d5c264f5b59374b193e972190ff3af53bbc6

Когда: 2022-11-14 09:49:50+03:00

Темы: go

Роб Пайк об истории появления Go: less is more

https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html
Какие упрощения были сделаны относительно C/C++ того времени?

    * regular syntax (don't need a symbol table to parse)
    * garbage collection (only)
    * no header files
    * explicit dependencies
    * no circular dependencies
    * constants are just numbers
    * int and int32 are distinct types
    * letter case sets visibility
    * methods for any type (no classes)
    * no subtype inheritance (no subclasses)
    * package-level initialization and well-defined order of initialization
    * files compiled together in a package
    * package-level globals presented in any order
    * no arithmetic conversions (constants help)
    * interfaces are implicit (no "implements" declaration)
    * embedding (no promotion to superclass)
    * methods are declared as functions (no special location)
    * methods are just functions
    * interfaces are just methods (no data)
    * methods match by name only (not by type)
    * no constructors or destructors
    * postincrement and postdecrement are statements, not expressions
    * no preincrement or predecrement
    * assignment is not an expression
    * evaluation order defined in assignment, function call (no "sequence point")
    * no pointer arithmetic
    * memory is always zeroed
    * legal to take address of local variable
    * no "this" in methods
    * segmented stacks
    * no const or other type annotations
    * no templates
    * no exceptions
    * builtin string, slice, map
    * array bounds checking

    We also added some things that were not in C or C++, like slices and
    maps, composite literals, expressions at the top level of the file
    (which is a huge thing that mostly goes unremarked), reflection,
    garbage collection, and so on. Concurrency, too, naturally.

Ну и заканчивает это всё очень правильными высказываниям

    Python and Ruby programmers come to Go because they don't have to
    surrender much expressiveness, but gain performance and get to play
    with concurrency.

    C++ programmers don't come to Go because they have fought hard to
    gain exquisite control of their programming domain, and don't want
    to surrender any of it. To them, software isn't just about getting
    the job done, it's about doing it a certain way.

    The issue, then, is that Go's success would contradict their world view.

    And we should have realized that from the beginning. People who are
    excited about C++11's new features are not going to care about a
    language that has so much less.  Even if, in the end, it offers so
    much more.

оставить комментарий

комментарий 0:

From: kmeaw
Date: 2022-11-14 09:42:15Z

    * no "this" in methods

Кажется, не получилось. Или я не понимаю, в чём принципиальное отличие
receiver от this.

    * no const or other type annotations
    * no exceptions

Очень спорные решения, не уверен, что от этого становится проще и лучше.
А вот всё остальное - очень правильные решения, которые резко снижают
когнитивную нагрузку при написании (и чтении!) кода без ущерба для
производительности и выразительности.

Есть ещё одна вещь, которой не хватает - context уровня языка, а не
стандартной библиотеки.

комментарий 1:

From: Sergey Matveev
Date: 2022-11-16 09:13:55Z


>Кажется, не получилось. Или я не понимаю, в чём принципиальное отличие
>receiver от this.

Согласен -- тоже не вижу отличия.

>* no const or other type annotations

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

>* no exceptions

Ну тут я всеми руками за это. С exceptions знаком только по Python языку
по серьёзной практике, но я всецело за if err != nil код. Вчера говорил
с одним знакомым, который в своё время на C и C++ писал. И он спросил
про Go (что это и всё в таком духе). И именно на отсутствие exception-ов
он сказал что для него это прям огромный плюс.

>Есть ещё одна вещь, которой не хватает - context уровня языка, а не
>стандартной библиотеки.

Эх, мне стыдно, но я до сих пор так и нигде его не использовал. Ну кроме
как подставлял какую-то пустышку если библиотека требовала его использование.
Читал и в Go-блоге про use-case-ы, но что-то пока так и не возникали лично
у меня они.

комментарий 2:

From: kmeaw
Date: 2022-11-16 12:39:00Z

> >* no const or other type annotations

> но в голове думая про то, что это поможет компилятору

Это ещё и защищает от ошибок программиста. memcpy(dst, src, n) не даст
перепутать src и dst в большинстве случаев, потому что src - указатель
на константу. А в C++ ещё и const-методы бывают.

По сигнатуре сразу видно, изменяет вызов данные или нет.

> я всецело за if err != nil код

Я вижу три проблемы.

Первая - замусоривание кода одинаковыми строчками вида if err != nil {
return nil, err }. От этого при чтении замыливается глаз, и легко
пропустить if err == nil в этом потоке.

Вторая - язык не делает ничего, чтобы вместо

if err != nil {
    return nil, err
}

заставить меня написать

if err != nil {
    return nil, fmt.Errorf("cannot save config: %w", err)
}

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

И если такой код становится библиотечным, то ко мне прилетает
необёрнутая ошибка типа io.ErrUnexpectedEOF, которая никак не подскажет
пользователю, что пошло не так. С исключениями такого не будет - если
вызываемый код не умеет обрабатывать какую-то исключительную ситуацию,
то исключение рано или поздно долетит до catch-all обработчика, у
которого будет вся контекстуальная информация, включая стек вызовов -
даже если просто его распечатать, это очень поможет пользователю понять,
что пошло не так.

Третья - механизмов работы с ошибками два, есть ещё panic. Когда первый
раз читаешь документацию, то кажется, что всё просто и очевидно. Но
когда программа развивается, становится всё сложнее, и, наконец,
превращается в библиотеку, которая используется другой программой, а
прежний main - в метод Init(), то приходится рефакторить: все log.Fatalf
заменять на return fmt.Errorf, добавлять второе return value для ошибок
и протаскивать их наверх по стеку.

Исключения также могут оказаться более эффективными по
производительности, чем десятки проверок на err != nil. Хотя и не любят
их по той же причине - они могут коварно скрыть увеличение времени
исполнения в плохом сценарии, из-за чего тяжело давать гарантии в
системах реального времени.

комментарий 3:

From: Sergey Matveev
Date: 2022-11-16 13:05:24Z


>По сигнатуре сразу видно, изменяет вызов данные или нет.

Да, про это забыл. Соглашусь.

>Первая - замусоривание кода одинаковыми строчками вида if err != nil {
>return nil, err }. От этого при чтении замыливается глаз, и легко
>пропустить if err == nil в этом потоке.

По моему это субъективно. Можно в редакторе сделать подсветку err != nil
каким-то другим цветом. Я не забуду как в SQL сложно бывает читать
конструкции, где куча одинаковых слов, иногда отличающихся на одну букву
(приходится подсветкой помогать (f4a041639f520162b246b48ef248e7116b4a5919).

>if err != nil {
>    return nil, fmt.Errorf("cannot save config: %w", err)
>}

Но так ведь такая штука и далеко не в первой версии Go появилась
(9425ee6ed097e608340912647fa97b817d093cbd). Типа и так уже тьма кода
написана и уже обратную совместимость приходится блюсти -- поздно
подталкивать к более корректным ошибкам.

Всему должна быть мера. Вот в Go нельзя сказать что эта штука реализует
такой то интерфейс, хотя бы ради того, чтобы помочь читающему и
намекнуть что хотел сделать автор. А Java заставляет. И многие считают
её избыточность кода проблемой и неудобством. Это я к тому, что если
переборщить с "подсказками" (и вынужденными человекочитаемыми
сообщениями) для читающего программиста, то это создаст не стоящий
геморрой пишущему.

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

>превращается в библиотеку, которая используется другой программой, а
>прежний main - в метод Init(), то приходится рефакторить: все log.Fatalf
>заменять на return fmt.Errorf, добавлять второе return value для ошибок
>и протаскивать их наверх по стеку.

Всё так. Но я всё равно тут не вижу проблемы. Если программист опытный,
то или сразу будет писать с мыслью о том, что это может стать
библиотекой, или писать попроще, менее парясь, быстрее, если понимает
что не надо ничего усложнять, ибо библиотекой не станет. Неопытный --
получит опыт. Я сужу по своему опыту с Python и понимаю о чём тут речь и
что, безусловно, они много где могут упростить жизнь. Но они требуют
большей аккуратности/опытности/понимания/квалификации от разработчика,
что означает что (грамотное) использование языка усложняется.

>системах реального времени.

Но если речь про Go, то разве о них вообще стоит говорить, раз в нём
garbage collector? Мне кажется это просто не его вотчина чтобы об этом
вообще думать.

комментарий 4:

From: kmeaw
Date: 2022-11-17 19:42:32Z

> Хороший разработчик и исключения будет проверять и ошибки.

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

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

func f() (*Result, error) {
    r1, err := g1()
    if err != nil {
        return nil, fmt.Errorf("cannot g1: %w", err)
    }

    r2, err := g2(r1)
    if err != nil {
        return nil, fmt.Errorf("cannot g2: %w", err)
    }

    return r2, nil
}

func main() {
    res, err := f()
    if err != nil {
        log.Fatalf("cannot f(): %s", err)
    }
}

может написать "cannot f(): cannot g2: unexpected EOF"

против (на несуществующем диалекте go-with-exceptions)

func f() *Result {
    return g2(g1())
}

func main() {
    res := f()
}

может написать:
    uncaught exception: unexpected EOF

    goroutine 1 [running]:
    main.g2(0xfee1dead)
        /go/src/foo/g2.go:11
    main.main()
        /go/src/foo/main.go:22

И сразу понятна причина.

комментарий 5:

From: Sergey Matveev
Date: 2022-11-17 19:56:31Z


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

Согласен.

>А для аналогичного результата с исключениями не надо вообще никакого кода писать.

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

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

Я просто не вижу как можно "совместить" два кардинально разных подхода
(возврат error или выброс исключения). Либо то, либо другое. На одной
чаше весов упрощение жизни писателю библиотеки (исключения). А на другой
дисциплинирование и принуждение проверять ошибки потребителям библиотеки.
На мой взгляд, последнее куда сильнее перевешивает. В будущем заменить
ошибки на более понятные можно и прозрачно и без потери обратной
совместимости.

комментарий 6:

From: Sergey Matveev
Date: 2022-11-17 20:00:39Z

Плюс мне кажется что авторы библиотек априори всё же немного более
квалифицированы, чем те, кто просто на основе рецептов со stackoverflow
что-то клепают. Сделать "package main" программу -- это одно. А сделать
нечто что может использоваться как библиотека -- уже чуть более сложнее
сделать, автоматом означая и чуть-чуть более продвинутый уровень
разработчика. Поэтому помогать и подталкивать к нужным действиям
(проверять и проверять ошибки) нужно наименее "квалифицированных"/опытных.

--
Sergey Matveev (http://www.stargrave.org/)
OpenPGP: 12AD 3268 9C66 0D42 6967  FD75 CB82 0563 2107 AD8A

Сгенерирован: SGBlog 0.34.0