Go - это COVID-19 языков; он распространился очень быстро, сначала это не казалось таким уж плохим, но это отстой, и я его ненавижу.

За последние два года я большую часть времени занимался разработкой начального уровня в го. Если вы не знакомы с языком программирования Go, позвольте мне быстро подытожить его: возьмите современный, статически связанный, строгий по типу язык и удалите все достижения в разработке за последние 10 лет или около того, сделайте некоторые претенциозные разговоры о том, что ваш язык лучший , и у вас есть Go. Не поймите меня неправильно, го - фантастический язык. Проблема в том, что это ужасный язык.

Обработка ошибок

В Go ошибки «всплывают» вверх в стеке вызовов, что означает, что функции должны возвращать error в качестве одного из возвращаемых значений. Это изложено в «Effective Go», официальном руководстве Golang.org по написанию «идиоматического кода Go».

Библиотечные подпрограммы часто должны возвращать вызывающему объекту какую-либо индикацию ошибки. Как упоминалось ранее, многозначный возврат Go позволяет легко возвращать подробное описание ошибки вместе с обычным возвращаемым значением. Это хороший способ использовать эту функцию для предоставления подробной информации об ошибках. Например, как мы увидим, os.Open не просто возвращает указатель nil при ошибке, он также возвращает значение ошибки, описывающее, что пошло не так.

[https://golang.org/doc/effective_go.html#errors]

Имея это в виду, типичная идиоматическая функция Go может выглядеть примерно так:

func Foo() (string, error) {
    val, err := Bar()
    if err != nil {
        return val, err
    }
    return val, nil
}

Мы видим, что функция Foo вызывает другую функцию Bar и проверяет, не вернул ли Bar ошибку; если это так, эта ошибка возвращается вместе с любым значением, которое могло быть возвращено Bar, в противном случае мы возвращаем значение val и nil для ошибки. Это может показаться несколько разумным, и вы будете правы. Этот шаблон является стандартом для программ Go и реализован повсюду в стандартной библиотеке. Кроме того, где его нет.

Видите ли, хотя Go говорит нам, что мы всегда должны выдавать ошибки, stdlib может делать все, что захочет. Почему? Мы к этому еще вернемся. Но взгляните на одну такую ​​функцию, которая противодействует этому, strconv.Itoa(). Эта функция позволяет преобразовывать целые числа в строки, что может быть полезно в определенных ситуациях. Вот его источник:

// Itoa is equivalent to FormatInt(int64(i), 10).
func Itoa(i int) string {
    return FormatInt(int64(i), 10)
}

Как видите, Itoa возвращает только строку. Кроме того, функция, которую вызывает Itoa, также не возвращает ошибку, а также возвращает строку. Фактически, большая часть пакета strconv также делает это. Из 31 функции, доступной в пакете strconv, только семь фактически возвращают ошибку. Фактически, беглый просмотр библиотеки stdlib показывает, что это вопиющее пренебрежение к собственным стилевым соглашениям языка проявляется повсюду, включая основные пакеты верхнего уровня, такие как runtime, time, regexp, os и многие другие.

Обработка ошибок, часть 2

Пока мы говорим об обработке ошибок, давайте еще раз вернемся к нашей демонстрационной функции.

func Foo() (string, error) {
    val, err := Bar()
    if err != nil {
        return val, err
    }
    return val, nil
}

Вы замечаете что-то посередине? if err != nil { ... } - это фрагмент кода, который, как разработчик Go, должен очень привыкнуть к вводу текста. Это почему? Потому что в Go нет понятия try/except. Каждый раз, когда функция вызывается и возвращает error для одного из своих значений, вы обязательно должны проверить это значение, чтобы убедиться, что оно не равно нулю. Это означает, что для каждой вызываемой функции вы должны набирать if err != nil { ... } и проверять наличие этой ошибки. Есть ли способы отложить обработку ошибок? Конечно, вы можете создать срез ошибок и добавить к нему возвращенные ошибки, а затем итерировать срез, позже ища ненулевые значения, но это не «идиоматический» и анти-шаблон. "Не повторяйся" должно относиться не только к функциям.

Dead Rising: Гото

Конечно, Go, язык, появившийся в 2012 году, не имеет ярлыков и обязательных заявлений, - я слышу, как вы говорите. Ну, угадайте, что? Оно делает. Не то чтобы вы узнали об этом, прочитав официальные руководства по стилю. Фактически, goto ни разу не упоминается в Idiomatic Go. Тем не менее, просматривая исходный код stdlib Go, вы обнаруживаете множество вариантов использования меток и goto операторов в Go. Пакет syscall - наверное, худший нарушитель в этом отношении, но вы можете найти его и в другом месте. Операторы Goto долгое время объявлялись либо устаревшими, либо провозвестниками подобных спагетти паутины непоследовательного кода и долгое время считались мертвыми в современных языках. Тем не менее, в 2020 году мы пишем программное обеспечение на языке, который свободно использует goto.

В Go нет общих типов, кроме тех случаев, когда они есть

Одним из основных преимуществ Go, по крайней мере, на этикетке, является то, что у него нет общих типов. Это означает, что каждый объект должен иметь объявленный тип и, надеюсь, ошибки типа и связанные с ними проблемы могут быть обнаружены компилятором, а не во время выполнения. Теоретически это должно сделать программы более стабильными и производительными. Только вот это не совсем так. Введите interface{}.

У интерфейсов в Go две цели. Первый - позволить объектам реализовать методы и характеристики других объектов. Проще говоря, он позволяет вам написать объект, который выполняет интерфейс для другого объекта, и его можно использовать вместо этого объекта. Хорошим примером является то, что вы можете написать объект, который реализует те же методы и значения, что и json.Unmarshal, и может использоваться во всех тех же местах, что и он, что позволяет вам написать собственную функцию демаршалинга. Второе использование интерфейсов - это универсальные типы; или, что более уместно, тип undefined.

func Baz() (interface{}, error) {
    return "my name is Bob", nil
}

В приведенной выше функции вы можете видеть, что наша функция Baz возвращает два значения: одно типа interface{} и ошибку. Глядя на возвращаемое значение, вы могли предположить, что реальное возвращаемое значение будет string, но ошиблись. Если бы мы вызывали эту функцию, реальные значения были бы interface{} и error. Затем мы можем преобразовать наш интерфейс в string, завершив преобразование:

data, err := Baz()
if err != nil {
    panic(err) // you shouldn't do this
}
newData := data.(string) + "!"
fmt.Println(newData)
// my name is Bob!

Итак, что произойдет, если мы попытаемся выполнить приведение к типу, который не соответствует базовым данным, например int? Что ж, паника. Это потому, что, хотя мы, разработчики видят тип как interface{}, Go на самом деле видит его как строку. Мы можем подтвердить это, используя пакет reflect для проверки data. Однако мы можем использовать это в своих интересах, особенно когда дело доходит до распаковки данных, тип которых мы еще не знаем, например JSON. Мы можем без проблем распаковать необработанный JSON в map[string]interface{}, несмотря на то, что данные JSON не являются настоящим интерфейсом в классическом смысле. Оттуда мы можем проверить базовые типы данных и при необходимости выполнить приведение.

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

Go Tooling. Какие инструменты?

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

гоплс

Возможно, самая большая беда в моем существовании как разработчика - это языковой сервер Go, gopls. Он существует для того, чтобы упростить разработку в современной среде IDE, такой как VS Code или Atom, путем предоставления линтинга в реальном времени, форматирования, документации и множества других функций, но факт в том, что это ошибочная и ужасная в использовании часть программного обеспечения. Хотите, чтобы одновременно было открыто несколько проектов Go? Не надо. Языковой сервер полностью нервничает и начинает забывать о существовании stdlib. Есть причина, по которой GO: Restart Language Server является наиболее часто используемым элементом в моем списке команд VS Code. Более того, gopls не дает возможности отключить определенные проверки. В своем последнем обновлении gopls начал отмечать то, что он считает «ненужными сложными» составными литералами, например карты структур. Это полностью игнорирует тот факт, что существуют вполне разумные ситуации, когда такая вещь может быть полезна, например, представление сложных структур данных. Для тех из нас, кому нравится видеть на вкладке «Проблемы» в VS Code значение «0», это серьезная проблема. Более того, при разработке стандартов кодирования в команде разработчиков вы должны иметь возможность указывать компилятору, о каких вещах нужно предупреждать, а какие игнорировать; Go не дает вам таких возможностей даже за пределами gopls.

Отладка

Это говорит мне о том, что наиболее часто используемый отладчик Golang не является частью проекта Go. Delve - это основной отладчик в VS Code, и в целом он довольно хорош, но я не могу понять, почему Go не поставлял эту функциональность как часть своего основного набора инструментов. Хороший отладчик неоценим для диагностики ошибок в вашем коде, поскольку «современный» язык, поставляемый без него, мне не по силам. На самом деле, я ошибался, Go действительно имеет отладчик под названием GDB, но позвольте мне показать вам это из документации:

Go обеспечивает поддержку GDB через стандартный компилятор Go и Gccgo. Управление стеком, многопоточность и среда выполнения содержат аспекты, которые достаточно отличаются от модели выполнения, которую ожидает GDB, что они могут запутать отладчик, даже когда программа скомпилирована с помощью gccgo. Хотя GDB можно использовать для отладки программ Go, он не идеален и может создавать путаницу.

Отслеживание

Современные языки должны предоставлять средства для сбора показателей и функций отслеживания. Python, Java, NodeJS, Rust - это все (и это лишь некоторые из них), которые позволяют осуществлять подобный детальный мониторинг без обширной и исчерпывающей реализации отдельных функций. Добавление трассировки в программы Go требует много времени и требует серьезной модификации кода вашего приложения из-за требований передавать контексты диапазона от функции к функции. Хотя существуют такие инструменты, как Jaeger, Skywalker и Zipkin, чтобы уловить эти следы, фактическая их реализация обходится настолько дорого, что зачастую не стоит затраченных усилий. Такие пакеты, как pprof, могут помочь, но только до определенной степени и не обеспечивают понимание отдельных стеков вызовов или запросов, как это может сделать хорошая библиотека трассировки. Реализация трассировки в Go правильным способом включает либо разработку вашей программы с первого дня, чтобы трассировка была включена для определенного поставщика или выбранного формата, либо написание дорогостоящих (как в человеко-часах, так и в производительности) абстракций по функциям трассировки. для инструментирования вашего кода. Ни одно из решений не является таковым и не должно рассматриваться как реальное решение.

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