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.
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.
Sergei has worked for more than three years as a back-end developer of web applications specializing in Node.js with MondoDB/PostgreSQL.
Expertise
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 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.
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:
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, likeeslint
orjshint
, 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
Is Go a scripting language?
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.
Is Go perfect?
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.
Voronezh, Voronezh Oblast, Russia
Member since February 20, 2018
About the author
Sergei has worked for more than three years as a back-end developer of web applications specializing in Node.js with MondoDB/PostgreSQL.