📜 ⬆️ ⬇️

What are they scolding Golang for and how to fight it?

After writing a few projects on Go, I looked back. I looked at the language, at its pros and cons. In this article I would like to talk about what Go is criticized for. Of course, it will be about the absence of OOP as such, overloading of methods and functions, generalized programming and exceptions. Does it really cause so many problems? Or is this a development approach problem? I will share my experience solving these issues.

Problematics


I started to program on Go after Java and PHP. And now I will tell you why.

Java cool stuff. It has a nice syntax. Many large projects use it in battle. Everything would be cool if not a JVM. In order to deploy a serious Java application, you will need a wheelbarrow with a lot of RAM. And this is absolutely not suitable for startups (in my case).

In turn, in PHP, with each request, the application is initialized again, there is no multithreading out of the box, performance is lower.
')
Every programming language, and every technology, has its weak points. You always have to sacrifice something - performance, system resources, development complexity.

Go in this regard is no exception. Giving out good performance and economical use of resources, the language requires a special approach from the developer. If to approach development of applications on Go, as on Java, then difficulties really will arise. We are confronted with the absence of such usual things as OOP, overloading of methods and functions, generalized programming, as well as exceptions.

When there are no usual solutions, they are replaced by new, but no less effective ones: a flexible type system, interfaces, separation of data and behavior. Now I will demonstrate everything with examples.

Overloading methods and functions


There is no overload of methods and functions in Go. It is proposed to simply give different names to methods and functions.

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

A different approach is the use of interfaces. For example, create an interface and a function to search for:

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

Now it’s enough to create two types:

 type Ints []int type Strings []string 

And implement the interface in each of the types. After that, you can use the search and the lines and numbers:

 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 

OOP


In Go, there is no PLO to which we are so used. OOP in Go is essentially type embedding with the ability to overload parent methods with descendant methods. Example:

 //  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" 

In this case, everything works as it should. What to do if we need the implementation of a method in the parent type, whose work depends on the methods overridden in the descendant? We add a method with complex logic and several methods for overriding to the parent type:

 //     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” 

I chose this solution for myself - we create and implement an interface for the parent and the descendant. When calling a complex method, we pass a link to the interface both in the parent and in the descendant:

 //   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” 

You may be that this is not quite elegant, but we avoid duplicating logic. In addition, interfaces will be needed when a method or function takes a generic type as an argument:

 //     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) // ,    

We will remake the DoSomething function so that it accepts the interface:

 // ,     A, B  C func DoSomething(s SuperType) { s.CallSecond() } DoSomething(a) // “A CallSecond” DoSomething(b) // “B CallSecond” DoSomething(c) // “C CallSecond” 

In this way, we separate the data from the behavior, which is good practice.

Generalized programming


In Go, there is still generalized programming and this interface {}. Again, this is unusual, because no syntax sugar like in java.

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

What do we get in Go?

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

In my opinion the difference is not great! If you use interfaces, you can avoid explicit type conversions. I will give an example:

 //   type Getter interface { GetId() int } type Setter interface { SetId(int) } //   type Identifier interface { Getter Setter } //    type List []Identifier 

Add a few types that will implement the Identifier and functions that will work with interfaces.

 //  Identifier type User struct { id int } //  Identifier type Post struct { id int } func assign(setter Setter, i int) func print(getter Getter) 

Now we can loop through the array without explicitly casting

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

Error processing


The try / catch block is not in the language; instead, methods and functions should return an error. Someone thinks this is a lack of language, someone is not. There are people who basically do not use try / catch, because This is a slow block. As a rule, standard error handling is enough:

 func Print(str string) error if Print(str) == nil { //     } else { //  } 

If you need complex error handling, as in a try / catch block, you can use the following technique:

 switch err := Print(str); err.(type) { case *MissingError: //   case *WrongError: //   default: //     } 

Thus, using the switch construction, you can minimize the lack of try / catch.

Summing up


So, does a language really have such serious problems? I think no! This is a matter of habit! For me, Go is the best solution when you need to write a system utility, a network daemon, or a web application that can handle tens of thousands of requests per second. Go is young, but very promising, well solves his problems. Go offers a new approach to which you need to get used to, take root in it, to feel comfortable. I already did it, I advise you too!

PS: the full code of examples is available here .

Source: https://habr.com/ru/post/282588/


All Articles