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

Добро

Простой

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

  • Минимальный набор языковых конструкций.
  • Простые проектные и модульные конструкции.
  • Минималистичный контроль видимости (только общедоступный и частный для пакета).
  • Легко определять типы (структуры).
  • Легко писать тесты - нет необходимости во внешних фреймворках для тестирования.

Быстро

Программы Go, компилируемые в машинный код и имеющие систему статических типов, делают их очень быстрыми во время выполнения. Кроме того, время запуска намного меньше, чем у чего-то вроде Java или любого языка JVM.

Встроенное управление сборкой и пакетами

Одна область, в которой Go действительно хорош по сравнению с традиционными языками программирования старой школы, такими как C ++, Java и т. Д., Заключается в том, что он поставляется со встроенной системой сборки и системой управления пакетами. Это устраняет необходимость в сторонних системах управления пакетами и сборками, таких как Gradle, Maven, make и т. Д., И значительно упрощает жизнь разработчикам.

Тип переключатель

Я много лет использую Java и немного C ++, и переключение типов - это то, чего мне всегда не хватало в этих языках. Ему больше не нужно выполнять дорогостоящие операции, такие как instanceof проверки в Java с большим количеством условий if-else или вводить альтернативные свойства переключения для объектов / классов (например, тега) для переключения. Переключатель типа Go делает свое дело. И не говоря уже о том, что это безумно быстро!

Горутины

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

Ценности против указателей

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

Однако это также может оставить некоторую путаницу. См. Следующий раздел.

Плохо

Ценности против указателей - путаница

  1. Отсутствие проверки указателей сбивает с толку

Посмотрите пример ниже. Когда вы смотрите на метод NewApple(), он возвращает nil. Однако, если вы запустите этот основной метод, он скажет: «Это не ноль»!

type Fruit interface {
}
type Apple struct {
}

func NewApple() *Apple {
   return nil  // return nil
}

func Main() {
   var fruit Fruit = NewApple()

   if fruit == nil {
      fmt.Println("It's nil")
   } else {
      fmt.Println("It's not nil")
   }
}

Причина в том, что NewApple() возвращает указатель на nil, а сам указатель не nil. Чтобы проверить, равно ли значение, на которое указывает указатель, nil, вам нужно будет проверить fruit == (*Apple)(nil). Но, опять же, обратная сторона - вам нужно знать, что это возвращает Apple.

2. При присвоении значений будет создана «копия»

Присвоение «значения» другой переменной будет копировать значение, если не указано иное. См. Пример ниже:

apple := Apple{Price: 50}
newApple := apple
newApple.Price++
fmt.Println(apple)

Значение по-прежнему будет печататься как {50}, потому что оно присвоило копию переменной newApple, и приращение происходит на копии. Кроме того, значения, копируемые каждый раз, когда выполняется назначение, означает, что это окажет большое влияние на приложения, критичные к производительности.

Но у Go также есть решение, которое заключается в передаче ссылки вместо значения:

apple := Apple{Price: 50}
newApple := &apple    // pass the pointer
newApple.Price++
fmt.Println(apple)

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

3. Карты и фрагменты по умолчанию передаются по ссылке.

Ну, мы только что говорили о разнице между «значением» и «указателем». Но сюрприз! карты и фрагменты не имеют концепции передачи по значению. Они всегда передаются по ссылке. Это непоследовательно и отличается от всех других структурированных типов, что может легко запутать пользователей.

Нет конструкторов для конструкций

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

Не так много библиотек

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

Нет IDE

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

  • Плагин IntelliJ IDEA Go - поддерживает только корпоративную версию. Не поддерживает версию сообщества (бесплатную).
  • Плагин Eclipse Go - устарел, активной разработки не ведется.
  • GoLand - имеет хорошие характеристики. Но опять же, бесплатной версии нет.
  • Плагин VSCode Go - он бесплатный, но имеет меньше функций по сравнению с GoLand. Поиск интерфейсов / реализации для структуры / интерфейса не поддерживается в плагине VSCode. Это критический момент, поскольку в Go есть система структурных типов, и вычислить ее вручную без поддержки IDE невозможно.
  • Плагин Atom / VIM - не совсем полноценные IDE.

Уродливый

Без наследования - писать ООП - настоящая боль

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

Вот простой пример: я писал простой синтаксический анализатор, который создавал простое синтаксическое дерево, состоящее из узлов. У каждого узла есть «тег» или «nodeKind», который используется для однозначной идентификации узла (в таких случаях, как сериализация и десериализация). Затем есть метод получения, скажем getTag(), который возвращает тег каждого узла.

Итак, реализация узлов выглядит так:

type Node interface {
    getTag() int
}
type FunctionDefinition struct {
   tag int
}
type VariableDefinition struct {
   tag int
}
type BinaryExpression struct {
   tag int
}
...

// Implementing the getTag() method for all nodes
func (node *FunctionDefinition) getTag() int {
    return node.tag
}
func (node *VariableDefinition) getTag() int {
    return node.tag
}
func (node *BinaryExpression) getTag() int {
    return node.tag
}
...

Сразу вы можете увидеть здесь проблему. Метод getType() должен быть реализован для каждого узла, несмотря на то, что все методы выглядят одинаково и делают одно и то же. А теперь представьте, что это синтаксическое дерево имеет около 50 узлов (полностью реализованное синтаксическое дерево для современного языка может иметь такое количество узлов) - вы в конечном итоге напишете и дублируете один и тот же код пятьдесят раз! Что еще хуже, если таких методов будет больше, значит, работа увеличится в 50 раз (уфу!).

Проблема здесь в отсутствии наследования в Go. В настоящих языках программирования ООП, таких как Java и C ++, этого гораздо легче достичь, расширив класс «Node», имеющий реализацию getNode(), а не реализуя его для всех узлов.

Примечание. Go имеет функцию «Состав», которая аналогична наличию поля типа структуры T, но с доступными методами T. Не следует путать это с «наследованием», хотя это может помочь решить некоторые проблемы.

Отсутствие явного соответствия интерфейса

Возьмите тот же пример, что и выше. Здесь все узлы соответствуют Node интерфейсу только за счет реализации его методов. Кроме этого, мы нигде не упоминали, что FunctionDefinition реализует Node. Другими словами, реализация интерфейса неявная. Опять же, именно так интерфейсы Go разработаны для работы со структурной типизацией. Это затрудняет определение того, какая структура какой интерфейс реализует.

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

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

Уловка:

Чтобы убедиться, что узел действительно реализует Node интерфейс, можно сделать следующее:

var _ Node = FunctionDefinition{}

Это просто попытка присвоить значение FunctionDefinition переменной типа Node, что приведет к ошибке времени компиляции, если вы измените интерфейс. Однако это ужасный прием, и вам придется писать один и тот же код пятьдесят раз для пятидесяти узлов (снова).

Нет дженериков!

Как я упоминал ранее, у Go нет очень обширной базы сторонних библиотек. И в результате в Go нет некоторых базовых структур данных, таких как упорядоченная карта, наборы и т. Д. Я столкнулся с необходимостью в упорядоченной карте для хранения разных значений в разное время. (например, карта определений функций, карта определений переменных и т. д.). Поскольку нет встроенной / сторонней библиотеки для упорядоченных карт, мне пришлось написать ее самому.

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

  • Дублируйте реализацию для разных типов значений,
struct FuncDefMap {
    // implementation
}
func (m *FuncDefMap) set(key string, value FunctionDefinition) {
    // implementation
}
func (m *FuncDefMap) get(key string) FunctionDefinition {
    // implementation
}
struct VarDefMap {
    // implementation
}
func (m *VarDefMap) set(key string, value VariableDefinition) {
    // implementation
}
func (m *VarDefMap) get(key string) VariableDefinition {
    // implementation
}
  • Или напишите упорядоченную карту с типом interface{} в качестве типа значения, а затем реализуйте оболочки, которые приводят / утверждают значение к соответствующему типу.
struct AnyOrderedMap {
    // implementation
}
func (m *AnyOrderedMap) set(key string, value interface{}) {
    // implementation
}
func (m *AnyOrderedMap) get(key string) interface{} {
    // implementation
}

// Implement type-safe wrappers
struct FuncDefMap {
    AnyOrderedMap
}
func (m *FuncDefMap) set(key string, value *FunctionDefinition) {
    m.AnyOrderedMap.set(key, value)
}
func (m *FuncDefMap) get(key string) *FunctionDefinition {
    return m.AnyOrderedMap.get(key).(*FunctionDefinition)
}

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

Проверка ошибок - каждый раз!

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

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

Вердикт

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

Однако он может с треском провалиться при работе с большим проектом с большой кодовой базой, особенно если сильно требуются объектно-ориентированные возможности. Ремонтопригодность кода также может стать проблемой по мере роста базы кода.

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