За что ругают Golang и как с этим бороться?

от автора

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

Проблематика

Я стал программировать на Go после Java и PHP. И сейчас расскажу почему.

Java классная штука. У нее приятный синтаксис. Многие крупные проекты используют его в бою. Все было бы круто, если не JVM. Для того, чтобы развернуть серьёзное приложение на Java, вам понадобится тачка с большим количеством оперативной памяти. И это совершенно не годится для спартапов.

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

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

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

Когда нет привычных решений, на смену им приходят новые, но не менее эффективные: гибкая система типов, интерфейсы, разделение данных и поведения. Сейчас я продемонстрирую все на примерах.

Перегрузка методов и функций

В Go нет перегрузки методов и функций. Предлагается просто давать разные имена методам и функциям.

func SearchInts(a []int, x int) bool func SearchStrings(a []string, x string) bool 

Иной подход — это использование интерфейсов. Для примера создадим интерфейс и функцию для поиска:

type Slice interface {     Len() int     Get(int) interface{} }  Search(slice Slice, x interface{}) bool 

Теперь достаточно создать два типа:

type Ints []int type Strings []string 

И реализовать интерфейс в каждом из типов. После этого можно использовать поиск и по строкам и по числам:

var strings Strings = []string{"one", "two", "three"} fmt.Println(Search(strings, "one")) // true fmt.Println(Search(strings, "four")) // false  var ints Ints = []int{0, 1, 2, 3, 4, 5} fmt.Println(Search(ints, 0)) // true fmt.Println(Search(ints, 10)) // false 

ООП

В Go нет того ООП, к которому мы так привыкли. ООП в Go это по сути встраивание типов с возможностью перегрузки методов родителя методами потомка. Пример:

// родитель type A struct {}  // может быть переопределен в потомке func (a *A) CallFirst() {     fmt.Println("A CallFirst") } // потомок type B struct {     A }  // переопределяем метод в потомке func (b *B) CallFirst() {     fmt.Println("B CallFirst") }  a := new(A) a.CallFirst() // "A CallFirst" b := new(B) b.CallFirst() // "B CallFirst" 

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

// метод со сложной логикой func (a *A) CallSecond() {     fmt.Println(a.GetName(), a.GetMessage()) }  // может быть переопределен в потомке func (a *A) GetName() string {     return "A" }  // может быть переопределен в потомке func (a *A) GetMessage() string {     return "CallSecond" }  // переопределяем метод в потомке func (b *B) GetName() string {     return "B" }  a.CallSecond() // “A CallSecond” b.CallSecond() // “A CallSecond”, а нужно “B CallSecond” 

Я выбрал для себя такое решение — создаем и реализуем интерфейс для родителя и потомка. При вызове сложного метода передаем ссылку на интерфейс и в родителе и в потомке:

// создаем интерфейс type SuperType interface {     GetName() string     GetMessage() string     CallSecond() }  // метод со сложной логикой func (a *A) сallSecond(s SuperType) {     fmt.Println(s.GetName(), s.GetMessage()) }  // реализуем метод интерфейса в родителе func (a *A) CallSecond() {     a.callSecond(a) }  // реализуем метод в потомке func (b *B) CallSecond() {     b.callSecond(b) }  a.CallSecond() // “A CallSecond” b.CallSecond() // “B CallSecond” 

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

// создадим еще одного потомка type C struct {     A }  func (c *C) GetName() string {     return "C" }  func (c *C) CallSecond() {     c.callSecond(c) }  // функция, которая должна работать с A, B и C func DoSomething(a *A) {     a.CallSecond() }  DoSomething(a)  DoSomething(b) // ошибка, не тот тип DoSomething(c) // ошибка, не тот тип 

Переделаем функцию DoSomething так, что бы она принимала интерфейс:

// функция, которая должна работать с A, B и C func DoSomething(s SuperType) {     s.CallSecond() }  DoSomething(a) // “A CallSecond” DoSomething(b) // “B CallSecond” DoSomething(c) // “C CallSecond” 

Таким образом мы отделяем данные от поведения, что является хорошей практикой.

Обобщенное программирование

В Go все таки есть обобщенное программирование и это interface{}. Опять же это непривычно, т.к. нет синтаксического сахара, как в Java.

ArrayList<String> list = new ArrayList<>(); String str = list.get(0); 

Что же мы получаем в Go?

type List []interface{}  list := List{"one", "two", "three"} str := list[0].(string) 

На мой взгляд разница не велика! Если же использовать интерфейсы, то можно избежать явного приведения типов. Приведу пример:

// создаем интерсейсы type Getter interface {     GetId() int }  type Setter interface {     SetId(int) }  // обобщаем интерфейсы type Identifier interface {     Getter     Setter }  // создаем новый список type List []Identifier 

Добавим несколько типов, которые будут реализовывать Identifier и функции, которые будут работать с интерфейсами.

// реализует Identifier type User struct {     id int }  // реализует Identifier type Post struct {     id int }  func assign(setter Setter, i int) func print(getter Getter) 

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

list := List{new(User), new(User), new(Post), new(Post), new(Post)} for i, item := range list {     assign(item, i)     print(item) } 

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

Блок try/catch отсутствует в языке, вместо этого методы и функции должны возвращать ошибку. Кто то считает это недостатком языка, кто то нет. Есть люди, которые принципиально не используют try/catch, т.к. это медленный блок. Как правило хватает стандартной обработки ошибок:

func Print(str string) error  if Print(str) == nil {     // делаем что то еще } else {     //обработка ошибки } 

Если же нужна сложная обработка ошибок, как в блоке try/catch, то можно воспользоваться следующим приемом:

switch err := Print(str); err.(type) { case *MissingError:     // обработка ошибки case *WrongError:     // обработка ошибки default:     // делаем что то еще } 

Таким образом с помощью конструкции switch можно свести в минимуму отсутствие try/catch.

Подводя итоги

Итак, действительно ли язык имеет такие серьезные проблемы? Я думаю, нет! Это вопрос привычки! Для меня Go — это оптимальное решение, когда надо написать системную утилиту, сетевого демона или веб приложение, которое способно обработать десятки тысяч запросов в секунду. Go молод, но очень перспективен, хорошо решает свои задачи. Go предлагает новый подход, к которому нужно привыкнуть, обжиться в нем, чтобы чувствовать себя комфортно. Я это уже сделал, советую и вам!

P.S.: полный код примеров доступен здесь.

ссылка на оригинал статьи https://habrahabr.ru/post/282588/