Why Go design is bad for smart programmers

Over the past months I have been using Go for implementations Proof of Concept (approx.trans.: code to test the working of an idea) in my spare time, partly to study the programming 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 about it. Go promises to be (approx.trans.: article written in 2015) in mass 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 smart programmers.

Designed for weak programmers?

Go is very easy to learn, so easy that it took me an evening to get started, after which I could code productively. The book I learned Go from is called An Introduction to Programming in Go (translation) is available online. The book, like the Go source code itself, is easy to read, has good code examples, and contains about 150 pages that can be read at a time. At first, this simplicity is refreshing, especially in a programming world full of over-engineered technologies. But in the end, sooner or later, the thought arises: β€œIs this really so?”

Google claims that Go's simplicity is a redeeming feature and the language is designed for maximum productivity in large teams, but I doubt it. There are features that are either missing or overly detailed. And all because of the lack of trust in the developers, with the assumption that they are not able to do anything right. 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 Rob Pike (approx.trans.: one of the co-authors of the Go language):

The key point here is that our programmers (approx.trans.: googlers) 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 their language should be easy for them 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.

What? So Rob Pike is essentially saying that the developers at Google aren't that good, which is why they created a language for idiots (approx.trans.: dumbed down) so that they can do something. What an arrogant look at your own colleagues? I have always believed that Google developers are selected from the brightest and best on earth. Surely they can handle something more difficult?

Artifacts of oversimplicity

Being simple is a worthy endeavor in any design, and trying to make something simple is hard. However, when trying to solve (or even express) complex problems, sometimes a complex tool is needed. Complexity and obfuscation are not the best features of a programming language, but there is a golden mean in which it is possible to create elegant abstractions in the language that are easy to understand and use.

Not very expressive

Due to the desire for simplicity, Go lacks constructs that other languages ​​take as something natural. At first, this may seem like a good idea, but in practice it ends up being verbose code. The reason for this should be obvious - it is necessary for developers to be easy to read someone else's code, but in fact, these simplifications only hurt readability. There are no shortcuts in Go: either a lot or nothing.

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()
    flags := flag.Args()

    var text string
    var scanner *bufio.Scanner
    var err error

    if len(flags) > 0 {

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

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

        scanner = bufio.NewScanner(file)

    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }

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

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

    fmt.Println(text)
}

Although this code tries to be as general as possible, Go's forced verbosity gets in the way, and as a result, solving a simple problem results in a large amount of code.

Here, for example, is the solution to the same problem on D:

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);
    }
}

And who is more readable now? I'll give my vote to D. His code is much more readable, as it describes the actions more clearly. D uses much more complex concepts (approx.trans.: alternative function call ΠΈ Patterns) than in the Go example, but it's really not hard to figure them out.

copy hell

A popular suggestion for improving Go is generalization. This will at least help avoid unnecessary code copying to support all data types. For example, a function for summing a list of integers can be implemented in no other way than by copy-pasting its base function for each integer type, there is no other way:

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 int16Sum(list []int16) (uint64) {
    var result int16 = 0
    for x := 0; x < len(list); x++ {
        result += list[x]
    }
    return uint64(result)
}

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

func main() {

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

    fmt.Println(int8Sum(list8))
    fmt.Println(int16Sum(list16))
    fmt.Println(int32Sum(list32))
    fmt.Println(int64Sum(list64))
}

And this example doesn't even work for signed types. This approach completely violates the principle of not repeating yourself (DRY), one of the most well-known and obvious principles, the ignorance of which is the source of many errors. Why does Go do this? This is a terrible aspect of the language.

Same example in D:

import std.stdio;
import std.algorithm;

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

Simple, elegant and straight to the point. Here the function is used reduce for template type and predicate. Yes, this is again more complicated than the Go version, but not so difficult for smart programmers to understand. Which example is easier to maintain and easier to read?

Simple traversal of the type system

I imagine Go programmers will be foaming at the mouth when they read this, β€œYou're doing it wrong!”. Well, there is another way to make a generic function and types, but that completely breaks the type system!

Take a look at this example of a stupid language fix to get around the problem:

package main

import "fmt"
import "reflect"

func Reduce(in interface{}, memo interface{}, fn func(interface{}, interface{}) interface{}) interface{} {
    val := reflect.ValueOf(in)

    for i := 0; i < val.Len(); i++ {
        memo = fn(val.Index(i).Interface(), memo)
    }

    return memo
}

func main() {

    list := []int{1, 2, 3, 4, 5}

    result := Reduce(list, 0, func(val interface{}, memo interface{}) interface{} {
        return memo.(int) + val.(int)
    })

    fmt.Println(result)
}

This implementation Reduce was taken from the article Idiomatic generics in Go (approx.trans.: I did not find the translation, I will be glad if you help with this). Well, if it's idiomatic, I'd hate to see a non-idiomatic example. Usage interface{} is a farce, and in the language it is needed only to bypass typing. It's an empty interface and all types implement it, allowing complete freedom for everyone. This style of programming is downright ugly, and that's not all. These acrobatics require the use of runtime reflection. Even Rob Pike doesn't like individuals who abuse it, as he mentioned in one of his talks.

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

I would take D patterns instead of this nonsense. How can anyone say that interface{} more readable or even type safe?

The woe of dependency management

Go has a built-in dependency system built on top of popular hosts VCS. The tools that come with Go are aware of these services and can download, build, and install code from them in one fell swoop. While this is great, there is a big oversight with versioning! Yes, indeed, you can get the source code from services like github or bitbucket using the Go tools, but you cannot specify the version. Again, simplicity over utility. I am unable to understand the logic of such a decision.

After asking questions about solving this problem, the Go development team created forum threadwhich outlined how they are going to get around this issue. Their recommendation was to simply copy the entire repository into your project one day and leave it "as is". What the hell are they thinking? We have amazing version control systems with great tagging and versioning that the creators of Go ignore and just copy the source code.

Cultural baggage from Xi

In my opinion, Go was developed by people who have used C all their lives and by people who didn't want to try something new. The language can be described as C with extra wheels(orig.: training wheels). There are no new ideas in it, except for support for parallelism (which, by the way, is beautiful) and this is a shame. You have excellent parallelism in a barely usable, lame language.

Another creaky problem is that Go is a procedural language (like the silent horror of C). You end up writing code in a procedural style that feels archaic and outdated. I know object-oriented programming is not a silver bullet, but it would be great to be able to abstract the details into types and provide encapsulation.

Simplicity for your own benefit

Go was designed to be simple, and it succeeds in that purpose. It was written for weak programmers, using the old language as a template. It comes complete with simple tools to do simple things. It's easy to read and easy to use.

It is extremely wordy, inexpressive, and bad for smart programmers.

Thank you mersinvald for edits

Source: habr.com

Add a comment