Cover image
Back-end
9 minute read

4 Go Language Criticisms

Go is quickly becoming one of the most popular languages: It currently ranks 19 in the TIOBE programming community index, and powers popular software like Kubernetes, Docker, and Heroku CLI. However, for all its simplicity, Go may still be missing some things. In this article, Toptal Freelance Go Developer Sergei Peshkov shares his concerns about why Go is still far from perfect and how we can make it better.

Read the Spanishes version of this article translated by Yesica Danderfer

Go (a.k.a. Golang) is one of the languages people are most interested in. As of April, 2018, it stands at 19th place in the TIOBE index. More and more people are switching from PHP, Node.js, and other languages to Go and using it in production. A lot of cool software (like Kubernetes, Docker, and Heroku CLI) is written using Go.

So, what is Go’s key to success? There are a lot of things inside the language that make it really cool. But one of the main things that made Go so popular is its simplicity, as pointed out by one of its creators, Rob Pike.

Simplicity is cool: You don’t need to learn a lot of keywords. It makes language learning very easy and fast. However, on the other hand, sometimes developers lack some features that they have in other languages and, therefore, they need to code workarounds or write more code in the long run. Unfortunately, Go lacks a lot of features by design, and sometimes it’s really annoying.

Golang was meant to make development faster, but in a lot of situations, you are writing more code than you’d write using other programming languages. I’ll describe some such cases in my Go language criticisms below.

The 4 Go Language Criticisms

1. Lack of Function Overloading and Default Values for Arguments

I’ll post a real code example here. When I was working on Golang’s Selenium binding, I needed to write a function that has three parameters. Two of them were optional. Here is what it looks like after the implementation:

func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
    // the actual implementation was here
}

func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error {
    return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}

func (wd *remoteWD) Wait(condition Condition) error {
    return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}

I had to implement three different functions because I couldn’t just overload the function or pass the default values—Go doesn’t provide it by design. Imagine what would happen if I accidentally called the wrong one? Here’s an example:

I'd get a bunch of `undefined`

I have to admit that sometimes function overloading can result in messy code. On the other hand, because of it, programmers need to write more code.

How Can It Be Improved?

Here is the same (well, almost the same) example in JavaScript:

function Wait (condition, timeout = DefaultWaitTimeout, interval = DefaultWaitInterval) {
    // actual implementation here
}

As you can see, it looks much clearer.

I also like the Elixir approach on that. Here is how it would look in Elixir (I know that I could use default values, like in the example above—I’m just showing it as a way it can be done):

defmodule Waiter do
@default_interval 1
        @default_timeout 10

    def wait(condition, timeout, interval) do
            // implementation here
    end
    def wait(condition, timeout), do: wait(condition, timeout, @default_interval)
    def wait(condition), do: wait(condition, @default_timeout, @default_interval)
end

Waiter.wait("condition", 2, 20)
Waiter.wait("condition", 2)
Waiter.wait("condition")

2. Lack of Generics

This is arguably the feature Go users are asking for the most.

Imagine that you want to write a map function, where you are passing the array of integers and the function, which will be applied to all of its elements. Sounds easy, right?

Let’s do it for integers:

package main

import "fmt"

func mapArray(arr []int, callback func (int) (int)) []int {
    newArray := make([]int, len(arr))
    for index, value := range arr {
     newArray[index] = callback(value)
    }
    
    return newArray;
}

func main() {
        square := func(x int) int { return x * x }
    fmt.Println(mapArray([]int{1,2,3,4,5}, square)) // prints [1 4 9 16 25]
}

Looks good, right?

Well, imagine you also need to do it for strings. You’ll need to write another implementation, which is exactly the same except for the signature. This function will need a different name, since Golang does not support function overloading. As a result, you will have a bunch of similar functions with different names, and it will look something like this:

func mapArrayOfInts(arr []int, callback func (int) (int)) []int {
    // implementation
}

func mapArrayOfFloats(arr []float64, callback func (float64) (float64)) []float64 {
    // implementation
}

func mapArrayOfStrings(arr []string, callback func (string) (string)) []string {
    // implementation
}

That definitely goes against the DRY (Don’t Repeat Yourself) principle, which states that you need to write as little copy/paste code as possible and instead move it to functions and reuse them.

A lack of generics means hundreds of variant functions

Another approach would be to use single implementations with interface{} as a parameter, but this can result in a runtime error because the runtime type-checking is more error-prone. And also it will be more slow, so there’s no simple way to implement these functions as one.

How Can It Be Improved?

There are a lot of good languages that include generics support. For example, here is the same code in Rust (I’ve used vec instead of array to make it simpler):

fn map<T>(vec:Vec<T>, callback:fn(T) -> T) -> Vec<T> {
    let mut new_vec = vec![];
    for value in vec {
            new_vec.push(callback(value));
    }
    return new_vec;
}

fn square (val:i32) -> i32 {
    return val * val;
}

fn underscorify(val:String) -> String {
    return format!("_{}_", val);
}

fn main() {
    let int_vec = vec![1, 2, 3, 4, 5];
    println!("{:?}", map::<i32>(int_vec, square)); // prints [1, 4, 9, 16, 25]

    
    let string_vec = vec![
            "hello".to_string(),
            "this".to_string(),
            "is".to_string(),
            "a".to_string(),
            "vec".to_string()
    ];
    println!("{:?}", map::<String>(string_vec, underscorify)); // prints ["_hello_", "_this_", "_is_", "_a_", "_vec_"]
}

Note that there’s a single implementation of map function, and it can be used for any types you need, even the custom ones.

3. Dependency Management

Anybody who has experience in Go can say that dependency management is really hard. Go tools allow users to install different libraries by running go get <library repo>. The problem here is version management. If the library maintainer makes some backwards-incompatible changes and uploads it to GitHub, anybody who tries to use your program after that will get an error, because go get does nothing but git clone your repository into a library folder. Also if the library is not installed, the program will not compile because of that.

You can do slightly better by using Dep for managing dependencies (https://github.com/golang/dep), but the problem here is you are either storing all your dependencies on your repository (which is not good, because your repository will contain not only your code but thousands and thousands of lines of dependency code), or just store the package list (but again, if the maintainer of the dependency makes a backward-incompatible change, it will all crash).

How Can It Be Improved?

I think the perfect example here is Node.js (and JavaScript in general, I suppose) and NPM. NPM is a package repository. It stores the different versions of packages, so if you need a specific version of a package, no problem—you can get it from there. Also, one of the things in any Node.js/JavaScript application is the package.json file. Here, all of the dependencies and their versions are listed, so you can install them all (and get the versions that are definitely working with your code) with npm install.

Also, the great examples of package management are RubyGems/Bundler (for Ruby packages) and Crates.io/Cargo (for Rust libraries).

4. Error Handling

Error handling in Go is dead simple. In Go, basically you can return multiple values from functions, and function can return an error. Something like this:

err, value := someFunction();
if err != nil {
    // handle it somehow
}

Now imagine you need to write a function that does three actions that return an error. It will look something like this:

func doSomething() (err, int) {
    err, value1 := someFunction();
    if err != nil {
            return err, nil
    }
    err, value2 := someFunction2(value1);
    if err != nil {
            return err, nil
    }
    err, value3 := someFunction3(value2);
    if err != nil {
            return err, nil
    }
    return value3;
}

There’s a lot of repeatable code here, which is not good. And with large functions, it can be even worse! You’ll probably need a key on your keyboard for this:

humorous image of error handling code on a keyboard

How Can It Be Improved?

I like JavaScript’s approach on that. The function can throw an error, and you can catch it. Consider the example:

function doStuff() {
    const value1 = someFunction();
    const value2 = someFunction2(value1);
    const value3 = someFunction3(value2);
    return value3;
}

try {
    const value = doStuff();
    // do something with it
} catch (err) {
   // handle the error
}

It’s way more clear and it doesn’t contain repeatable code for error handling.

The Good Things in Go

Although Go has many flaws by design, it has some really cool features as well.

1. Goroutines

Async programming was made really simple in Go. While multithreading programming is usually hard in other languages, spawning a new thread and running function in it so it won’t block the current thread is really simple:

func doSomeCalculations() {
    // do some CPU intensive/long running tasks
}

func main() {
    go doSomeCalculations(); // This will run in another thread;
}

2. Tools That Are Bundled with Go

While in other programming languages you need to install different libraries/tools for different tasks (such as testing, static code formatting etc.), there are a lot of cool tools that are already included in Go by default, such as:

  • gofmt - A tool for static code analysis. Comparing to JavaScript, where you need to install an additional dependency, like eslint or jshint, here it’s included by default. And the program will not even compile if you don’t write Go-style code (not using declared variables, importing unused packages, etc.).
  • go test - A testing framework. Again, comparing to JavaScript, you need to install additional dependencies for testing (Jest, Mocha, AVA, etc.). Here, it’s included by default. And it allows you to do a lot of cool stuff by default, such as benchmarking, converting code in documentation to tests, etc.
  • godoc - A documentation tool. It’s nice to have it included in the default tools.
  • The compiler itself. It’s incredibly fast, comparing to other compiled languages!

3. Defer

I think this is one of the nicest features in the language. Imagine you need to write a function that opens three files. And if something fails, you will need to close existing opened files. If there are a lot of constructions like that, it will look like a mess. Consider this pseudo-code example:

function openManyFiles() {
    let file1, file2, file3;
    try {
        file1 = open(‘path-to-file1’);
    } catch (err) {
        return;
    }

    try {
        file2 = open(‘path-to-file2’);
    } catch (err) {
        // we need to close first file, remember?
        close(file1);
        return;
    }

    try {
        file3 = open(‘path-to-file3’);
    } catch (err) {
        // and now we need to close both first and second file
        close(file1);
close(file2);
        return;
    }

    // do some stuff with files

    // closing files after successfully processing them
    close(file1);
    close(file2);
    close(file3);
    return;
}

Looks complicated. That’s where Go’s defer comes into place:

package main

import (
    "fmt"
)

func openFiles() {
    // Pretending we’re opening files
    fmt.Printf("Opening file 1\n");
    defer fmt.Printf("Closing file 1\n");
    
    fmt.Printf("Opening file 2\n");
    defer fmt.Printf("Closing file 2\n");
    
    fmt.Printf("Opening file 3\n");
    // Pretend we've got an error on file opening
    // In real products, an error will be returned here.
    return;
}

func main() {
    openFiles()

    /* Prints:

    Opening file 1
    Opening file 2
    Opening file 3
    Closing file 2
    Closing file 1

    */

}

As you see, if we’ll get an error on opening file number three, other files will be automatically closed, as the defer statements are executed before return in reverse order. Also, it’s nice to have file opening and closing at the same place instead of different parts of a function.

Conclusion

I didn’t mention all of the good and bad things in Go, just the ones I consider the best and the worst things.

Go is really one of the interesting programming languages in current use, and it really has potential. It provides us with really cool tools and features. However, there are lot of things that can be improved there.

If we, as Go developers, will implement these changes, it will benefit our community a lot, because it will make programming with Go far more pleasant.

In the meantime, if you’re trying to improve your tests with Go, try Testing Your Go App: Get Started the Right Way by fellow Toptaler Gabriel Aszalos.

Understanding the basics

There's a thin line between the definition of a script and a program, but I'd say it's not a scripting language, since Go programs are not run in runtime—they are compiled and run as an executable.

No. Go is great, and it improves the experience of the developer, but it is not perfect, as I describe in this article. It may never be perfect, but I believe we can bring it close.

Comments

Ruslan Voroshchuk
Awesome, thanks for the great article, Sergei!
Ruslan Voroshchuk
Awesome, thanks for the great article, Sergei!
jeremylowery
govendor solves the dependency versioning problem
jeremylowery
govendor solves the dependency versioning problem
Tim Murnaghan
I was trying to use a cloud platform which has a router layer written in go. I was really surprised to find that it had horribly bad performance on bigger messages which was due to the message copying behavior of the underlying go networking libraries. For something that positions itself as a systems programming language - not to make streaming really easy is a big limitation. I'm much more bothered by this than a bit of syntax like overloading. Like I say I was surprised that it was so bad. I'd say give it a few more years until they've got I/O sorted.
Tim Murnaghan
I was trying to use a cloud platform which has a router layer written in go. I was really surprised to find that it had horribly bad performance on bigger messages which was due to the message copying behavior of the underlying go networking libraries. For something that positions itself as a systems programming language - not to make streaming really easy is a big limitation. I'm much more bothered by this than a bit of syntax like overloading. Like I say I was surprised that it was so bad. I'd say give it a few more years until they've got I/O sorted.
Jesse Stewart
everyone is GREAT in my book.
Jesse Stewart
everyone is GREAT in my book.
heatbr
Great aticle It's a very interesting article. This could be better when it explores the design decision for language. The overwritten method should be rethought with the use of DI and interfaces. Looking at an example code below. import "wait / timeout" or import "wait / interval" For the Wait Interface (...) With regard to generics the "fmt.Println" package is great as an example of how to implement generic methods. I have to agree that exception handling is very different. But it forces a developer to be more organized. I've seen many codes try {} catch (Exception ex) {} with no meaning at all because the method called for handling the exception. The specific dependency versions are already in the most current version of go. Using the vendor folder. I miss a deep dependency like npm. But it's been more than enough.
heatbr
Great aticle It's a very interesting article. This could be better when it explores the design decision for language. The overwritten method should be rethought with the use of DI and interfaces. Looking at an example code below. import "wait / timeout" or import "wait / interval" For the Wait Interface (...) With regard to generics the "fmt.Println" package is great as an example of how to implement generic methods. I have to agree that exception handling is very different. But it forces a developer to be more organized. I've seen many codes try {} catch (Exception ex) {} with no meaning at all because the method called for handling the exception. The specific dependency versions are already in the most current version of go. Using the vendor folder. I miss a deep dependency like npm. But it's been more than enough.
John Robie
I'm keeping an eye on Go, but I'm sticking with Elixir as my main backend language...
John Robie
I'm keeping an eye on Go, but I'm sticking with Elixir as my main backend language...
hmijail
I'm new to Go, but the lack of try/catch and addition of defer seems like a very purposeful choice, as can be seen on the author's own example for defer. So I don't understand why complain about the lack of one and praise the availability of the other.
hmijail
I'm new to Go, but the lack of try/catch and addition of defer seems like a very purposeful choice, as can be seen on the author's own example for defer. So I don't understand why complain about the lack of one and praise the availability of the other.
Sergei Peshkov
Didn't know about it, thanks a lot! Though there are better solutions, like npm, it would be awesome to have something like that in Go.
Sergei Peshkov
Didn't know about it, thanks a lot! Though there are better solutions, like npm, it would be awesome to have something like that in Go.
Jon Cody
Which underlying libraries in particular?
Jon Cody
Which underlying libraries in particular?
Fernando
For the second case you can easily use interfaces...
Fernando
For the second case you can easily use interfaces...
Jon Cody
I like the conclusion.
Jon Cody
I like the conclusion.
Eduardo Reyes
Lol, did he actually delete your comment?
Eduardo Reyes
Lol, did he actually delete your comment?
Jon Cody
No, I did. I figured criticizing Sergei or his article was hypocritical, and I do like the conclusion. Let's keep overloading out of Go https://golang.org/doc/faq#overloading ; and let's not forget about variadic greatness.
Jon Cody
No, I did. I figured criticizing Sergei or his article was hypocritical, and I do like the conclusion. Let's keep overloading out of Go https://golang.org/doc/faq#overloading ; and let's not forget about variadic greatness.
Jerry T
Go is 19th on the list of language popularity? I wouldn't say that qualifies it as being the language everyone wants to learn. Even Assembly and Object Pascal are higher. Luckily is still beats COBOL. lol
Jerry T
Go is 19th on the list of language popularity? I wouldn't say that qualifies it as being the language everyone wants to learn. Even Assembly and Object Pascal are higher. Luckily is still beats COBOL. lol
Tim Murnaghan
The standard net/http library. It reads whole messages into memory. This is really bad for a router handling big messages.
Tim Murnaghan
The standard net/http library. It reads whole messages into memory. This is really bad for a router handling big messages.
Sergei Peshkov
I don't have any power on deleting comments here, and even if I had, why would I? It's all about discussion
Sergei Peshkov
I don't have any power on deleting comments here, and even if I had, why would I? It's all about discussion
Mateusz Kozak
heroku cli is written in node.js, not golang (there was golang implementation for some time but they abandoned it). for dependency management there is dep that will be official package manager.
Mateusz Kozak
heroku cli is written in node.js, not golang (there was golang implementation for some time but they abandoned it). for dependency management there is dep that will be official package manager.
Jon Cody
You could do this: func wait(condition string, timers ...time.Duration) error {} Of course, this only works with your example because the last two parameters are of the same type.
Jon Cody
You could do this: func wait(condition string, timers ...time.Duration) error {} Of course, this only works with your example because the last two parameters are of the same type.
@TaDaDeK_
Fascinating thing. I look forward when will I have more time and possibilities to learn programming langs. It has somekind of rough/raw infascination like Medieval Music notations. Pure, clarife, clear, solid. Mind Boozing in a way like some good thinking exercises, not all this Jogi's stuff people are fed nowadays.
@TaDaDeK_
Fascinating thing. I look forward when will I have more time and possibilities to learn programming langs. It has somekind of rough/raw infascination like Medieval Music notations. Pure, clarife, clear, solid. Mind Boozing in a way like some good thinking exercises, not all this Jogi's stuff people are fed nowadays.
cyberbolt
gvm
cyberbolt
gvm
Decebal Dobrica
I was just going through these critics of yours, awesome article by the way, and couldn't really find one that I would agree with. I really like that golang does not allow function overloading and their different approach to error handling, I really hope their going to stick with their values and these features stay away from the language. The lack of these needed features compels to better code architecture in terms of a more robust code base. Although it does not feature reactivity at front, it is close enough in terms of reactivity for me to something like haskell.
Decebal Dobrica
I was just going through these critics of yours, awesome article by the way, and couldn't really find one that I would agree with. I really like that golang does not allow function overloading and their different approach to error handling, I really hope their going to stick with their values and these features stay away from the language. The lack of these needed features compels to better code architecture in terms of a more robust code base. Although it does not feature reactivity at front, it is close enough in terms of reactivity for me to something like haskell.
Oscar Balladares
You didn't propose any alternative to the issues described. I do understand the issue with the error handling, a try/catch won't work for async things so they just scrap it from Golang, that means there's only one way to handle errors. But what about the other issues? What about the map issue? The function overloading? You can do something about it in almost every major programming language.
Ezequiel Regaldo
Ill disagree with everything lol ... well first point isnt "bad" 'cause today generics are implemented, this is fine ... but on the other side, nil conditionals are more or less the same like exceptions, that doesnt generate boilerplate, and defer exists 'cause "try", "catch" and "finally" doesnt exist, its all
comments powered by Disqus