Why Go is Bad for Non-Smart Programmers

The article was written as a response to a previously published antipode article.

Why Go is Bad for Non-Smart Programmers

Over the past two years I have been using Go to implement a specialized RADIUS server with a developed billing system. Along the way, I learn the intricacies of the language itself. The programs themselves are very simple and are not the purpose of this article, but the Go experience itself deserves a few words in its defense. Go is becoming more of a mainstream language for serious, scalable code. The language was created by Google, where it is actively used. To sum up, I sincerely believe that the design of the Go language is bad for non-smart programmers.

Designed for weak programmers?

The weak talk about problems. The strong talk about ideas and dreams...

Go is very easy to learn, so easy that you can read the code with little to no training at all. This feature of the language is used in many global companies when the code is read together with non-core specialists (managers, customers, etc.). This is very handy for methodologies like Design Driven Development.
Even novice programmers start producing quite decent code after a week or two. The book I learned from “Go is Programming in the Go Language” (by Mark Summerfield). The book is very good, it touches on many nuances of the language. After unnecessarily complicated languages ​​such as Java, PHP, the lack of magic is refreshing. But sooner or later, many limited programmers have to use old methods in a new field. Is it really necessary?

Rob Pike (the main ideologue of the language) created the Go language as an industrial language that is easy to understand, efficient to use. The language is designed for maximum productivity in large teams and there is no doubt about it. Many novice programmers complain that there are many features that they lack. This desire for simplicity was a conscious decision by the designers of the language, and in order to fully understand what it was for, we need to understand the motivations of the developers and what they wanted in Go.

So why was it made so simple? Here are a couple of quotes from Rob Pike:

The key point here is that our programmers are not researchers. They are usually quite young, they come to us after their studies, perhaps they studied Java, or C / C ++, or Python. They are not able to understand an outstanding language, but at the same time we want them to create good software. That is why the language should be easy to understand and learn.

It should be familiar, roughly speaking similar to C. Programmers at Google start their careers early and are mostly familiar with procedural languages, in particular the C family. The requirement for rapid productivity in a new programming language means that the language must not be too radical.

Wise words, right?

Artifacts of simplicity

Simplicity is a necessary condition for beauty. Lev Tolstoy.

Being simple is one of the most important aspirations in any design. As you know, a perfect project is not a project where there is nothing to add, but one from which there is nothing to remove. Many people think that in order to solve (or even express) complex problems, a complex tool is needed. However, it is not. Let's take PERL as an example. Language ideologists believed that a programmer should have at least three different ways to solve one problem. The ideologues of the Go language took a different path, they decided that one way is enough to achieve the goal, but a really good one. This approach has a serious foundation: the only way is easier to learn and harder to forget.

Many migrants complain that the language does not contain elegant abstractions. Yes, this is true, but this is one of the main advantages of the language. The language contains a minimum of magic in its composition - therefore, deep knowledge is not required to read the program. As for the verbosity of the code, this is not a problem at all. A well-written Golang program reads vertically, with little or no structuring. In addition, the speed of reading a program is at least an order of magnitude greater than the speed of writing it. If you consider that all code has uniform formatting (performed using the gofmt built-in command), then reading a few extra lines is not a problem at all.

Not very expressive

Art does not tolerate when its freedom is restricted. Accuracy is not his responsibility.

Due to the desire for simplicity, Go lacks constructs that in other languages ​​are perceived as something natural by people who are used to them. At first, this may be somewhat inconvenient, but then you notice that the program is read many times easier and more unambiguous.

For example, a console utility that reads stdin or a file from command line arguments would look like this:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "log"
    "os"
)

func main() {

    flag.Parse()

    scanner := newScanner(flag.Args())

    var text string
    for scanner.Scan() {
        text += scanner.Text()
    }

    if err := scanner.Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Println(text)
}

func newScanner(flags []string) *bufio.Scanner {
    if len(flags) == 0 {
        return bufio.NewScanner(os.Stdin)
    }

    file, err := os.Open(flags[0])

    if err != nil {
        log.Fatal(err)
    }

    return bufio.NewScanner(file)
}

The solution of the same problem in the D language, although it looks somewhat shorter, however, is no easier to read.

import std.stdio, std.array, std.conv;

void main(string[] args)
{
    try
    {
        auto source = args.length > 1 ? File(args[1], "r") : stdin;
        auto text   = source.byLine.join.to!(string);

        writeln(text);
    }
    catch (Exception ex)
    {
        writeln(ex.msg);
    }
}

copy hell

Man carries hell within himself. Martin Luther.

Beginners constantly complain about Go in terms of the lack of generics. To solve this issue, most of them use direct code copying. For example, a function for summing up a list of integers, such unfortunate professionals believe that it is impossible to implement the functionality in any other way than by simple copy-pasting for each data type.

package main

import "fmt"

func int64Sum(list []int64) (uint64) {
    var result int64 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func int32Sum(list []int32) (uint64) {
    var result int32 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

The language has sufficient means to implement such constructions. For example, generic programming is fine.

package main

import "fmt"

func Eval32(list []int32, fn func(a, b int32)int32) int32 {
    var res int32
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int32Add(a, b int32) int32 {
    return a + b
}

func int32Sub(a, b int32) int32 {
    return a + b
}

func Eval64(list []int64, fn func(a, b int64)int64) int64 {
    var res int64
    for _, val := range list {
        res = fn(res, val)
    }
    return res
}

func int64Add(a, b int64) int64 {
    return a + b
}

func int64Sub(a, b int64) int64 {
    return a - b
}

func main() {

    list32 := []int32{1, 2, 3, 4, 5}
    list64 := []int64{1, 2, 3, 4, 5}

    fmt.Println(Eval32(list32, int32Add))
    fmt.Println(Eval64(list64, int64Add))
    fmt.Println(Eval64(list64, int64Sub))
}

And, although our code turned out to be somewhat longer than the previous case, it became generalized. Therefore, it will not be difficult for us to implement all the arithmetic operations.

Many people will say that a D program looks significantly shorter, and they will be right.

import std.stdio;
import std.algorithm;

void main(string[] args)
{
    [1, 2, 3, 4, 5].reduce!((a, b) => a + b).writeln;
}

However, it is only shorter, but not more correct, since the D implementation completely ignores the problem of error handling.

In real life, as the complexity of logic increases, the gap narrows rapidly. The gap narrows even more rapidly when it is required to perform an action that cannot be performed using standard language operators.

From the point of view of maintainability, extensibility, readability, in my opinion, the Go language wins, although it loses in terms of verbosity.

Generic programming in a number of cases gives us undeniable benefits. This is clearly illustrated by the sort package. So, to sort any list, it is enough for us to implement the sort.Interface interface.

import "sort"

type Names []string

func (ns Names) Len() int {
    return len(ns)
}

func (ns Names) Less(i, j int) bool {
    return ns[i] < ns[j]
}

func (ns Names) Swap(i, j int) {
    ns[i], ns[j] = ns[j], ns[i]
}

func main() {
    names := Names{"London", "Berlin", "Rim"}
    sort.Sort(names)
}

If you take any open source project and grep "interface{}" -R, you'll see how often obfuscated interfaces are used. Close-minded comrades will immediately say that all this is due to the lack of generics. However, this is not always the case. Let's take DELPHI as an example. Despite the presence of these same generics, it contains a special type VARIANT for operations with arbitrary data types. The Go language does the same.

From a cannon on sparrows

And the straitjacket must fit the size of the madness. Stanislav Lets.

Many extreme sports enthusiasts may claim that Go has another mechanism for creating generics - reflection. And they will be right... but only in rare cases.

Rob Pike warns us:

This is a powerful tool that should be used with care. It should be avoided unless absolutely necessary.

Wikipedia tells us the following:

Reflection means the process during which a program can track and modify its own structure and behavior at run time. The programming paradigm underlying reflection is called reflective programming. This is one of the types of metaprogramming.

However, as you know, you have to pay for everything. In this case it is:

  • complexity of writing programs
  • program execution speed

Therefore, reflection must be used with caution, like large-caliber weapons. Thoughtless use of reflection leads to unreadable programs, constant errors and low speed. Just the thing for a snobby programmer to be able to show off his code in front of other, more pragmatic and modest colleagues.

Cultural baggage from Xi? No, from a number of languages!

Together with the state, debts are also left to the heirs.

Despite the fact that many people believe that the language is completely based on the legacy of C, this is not so. The language has incorporated many aspects of the best programming languages.

Syntax

First of all, the syntax of grammatical constructions is based on the syntax of the C language. However, the DELPHI language also had a significant impact. So, we see that the redundant brackets, which greatly reduce the readability of the program, have been completely removed. The language also contains the ":=" operator inherent in the DELPHI language. The concept of packages is borrowed from languages ​​like ADA. The declaration of unused entities is borrowed from the PROLOG language.

Semantics

The packages were based on the semantics of the DELPHI language. Each package encapsulates data and code and contains private and public entities. This allows the package interface to be reduced to a minimum.

The delegate implementation operation was borrowed from the DELPHI language.

Compilation

No wonder there is a joke: Go was developed while a C program was being compiled. One of the strengths of the language is super-fast compilation. The idea was borrowed from the DELPHI language. Each Go package corresponds to a DELPHI module. These packages are only recompiled when really necessary. Therefore, after the next edit, it is not required to compile the entire program, but it is enough to recompile only the changed packages and packages that depend on these changed packages (and even then, only if the package interfaces have changed).

High level constructs

The language contains many different high-level constructs that have nothing to do with the low-level language like C.

  • Rows
  • Table hash
  • slices
  • Duck typing is borrowed from languages ​​like RUBY (which, unfortunately, many do not understand and do not use to their full potential).

Memory management

Memory management generally deserves a separate article. If in languages ​​like C++, control is completely at the mercy of the developer, then in later languages ​​like DELPHI, the reference counting model was used. With this approach, circular references were not allowed, since lost clusters were formed, Go has built-in detection of such clusters (as in C #). In addition, the garbage collector outperforms most currently known implementations in terms of efficiency and can already be used for many real-time tasks. The language itself recognizes situations when the value to store a variable can be allocated on the stack. This reduces the load on the memory manager and increases the speed of the program.

Concurrency and Concurrency

The parallelism and competitiveness of the language is beyond praise. No low-level language can even remotely compete with Go. In fairness, it is worth noting that the model was not invented by the authors of the language, but simply borrowed from the good old ADA language. The language is able to process millions of parallel connections using all CPUs, while having complex problems with deadlocks and race conditions typical of multithreaded code by an order of magnitude.

Additional benefits

If it is profitable, everyone will become disinterested.

The language also provides us with a number of undoubted benefits:

  • The only executable file after the project is built greatly simplifies deploy applications.
  • Static typing and type inference can significantly reduce the number of errors in the code even without writing tests. I know some programmers who do not write tests at all, and the quality of their code does not suffer significantly.
  • Very easy cross-compilation and excellent portability of the standard library, which greatly simplifies the development of cross-platform applications.
  • RE2 regular expressions are thread-safe and have predictable execution times.
  • A powerful standard library that allows most projects to do without third-party frameworks.
  • The language is powerful enough to focus on the task rather than the methods of solving it, and at the same time low-level enough that the task can be solved efficiently.
  • The Go eco system contains out of the box advanced tools for all occasions: tests, documentation, package management, powerful linters, code generation, race conditions detector, etc.
  • Go version 1.11 has built-in semantic dependency management built on top of popular VCS hosts. All the tools that make up the Go ecosystem use these services to download, build, and install code from them in one fell swoop. And that's great. With the arrival of version 1.11, the problem with versioning packages was also completely resolved.
  • Since the main idea of ​​the language is to reduce magic, the language encourages developers to do error handling explicitly. And this is correct, because otherwise, it will simply forget about error handling altogether. Another thing is that most developers deliberately ignore error handling, preferring to simply throw the error up instead of handling them.
  • The language does not implement the classical OOP methodology, since there is no virtuality in Go in its purest form. However, this is not a problem when using interfaces. The absence of OOP significantly lowers the entry barrier for beginners.

Simplicity for the benefit of the community

Easy to complicate, hard to simplify.

Go was designed to be simple, and it succeeds in that purpose. It was written for smart programmers who understand all the virtues of teamwork and are tired of the endless variability of Enterprise-level languages. Having a relatively small set of syntactic constructions in its arsenal, it is practically not subject to changes over time, so developers have a lot of time for development, and not for endless study of language innovations.

Companies also receive a number of advantages: a low entry threshold allows you to quickly find a specialist, and the immutability of the language allows you to use the same code even after 10 years.

Conclusion

The large size of the brain has not yet made a single elephant a Nobel Prize winner.

For those programmers whose personal ego prevails over team spirit, as well as theorists who love academic tasks and endless "self-improvement", the language is really bad, because it is a general-purpose artisanal language that does not allow you to get aesthetic pleasure from the result of your work and show yourself. professional in front of colleagues (provided that we measure the mind precisely by these criteria, and not by the IQ coefficient). Like everything in life, it's a matter of personal preference. Like all worthwhile innovations, language has already come a long way from universal denial to mass acceptance. Language is ingenious in its simplicity, and, as you know, all ingenious is simple!

Source: habr.com

Add a comment