Написав несколько проектов на 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/
Добавить комментарий