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

в 9:31, , рубрики: Go, 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.: полный код примеров доступен здесь.

Автор: asolomonoff

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js