Hypocritical Gophers

Go is the COVID-19 of languages; it spread really fast, it didn’t seem that bad at first, but it sucks and I hate it.

Hypocritical Gophers
The Golang gopher, looking nervous that someone’s about call him out for his b******t.

Most of my time over the past two years has been developing primary in Go. If you’re not familiar with the Go programming language let me sum it up for you real quick: take a modern, statically-linked, type-strict language and remove every advancement in development in the last 10 years or so, do some pretentious talks about how your language is the best one out there, and you have Go. Now, don’t get me wrong, Go is a fantastic language. The problem is that its also a horrible language.

Error Handling

In Go, errors are “bubbled up” to the top of the call stack, meaning that functions are expected to return an error as one of the return values. This is laid out in “Effective Go”, the official Golang.org guide to writing “idiomatic Go code”.

Library routines must often return some sort of error indication to the caller. As mentioned earlier, Go’s multivalue return makes it easy to return a detailed error description alongside the normal return value. It is good style to use this feature to provide detailed error information. For example, as we’ll see, os.Open doesn't just return a nil pointer on failure, it also returns an error value that describes what went wrong.

[https://golang.org/doc/effective_go.html#errors]

With that in mind, a typical idiomatic Go function might look something like this:

func Foo() (string, error) {
	val, err := Bar()    
    if err != nil {        
    	return val, err    
    }    
    return val, nil
}

We can see that the Foo function calls another function Bar and checks to see if Bar returned an error; if so that error is returned along with any value Bar may have returned, otherwise we return the value val and nil for the error. This might sound somewhat reasonable, and you would be right. This pattern is the standard for Go programs and is implemented everywhere throughout the standard library. Except where it isn’t.

You see, while Go tells us that we should always bubble up errors, the stdlib is free to do whatever it wants. Why? We’ll get to that. But have a look at one such function that is anti-pattern to this, strconv.Itoa() . This function allows you to convert integers to strings, which can be useful in certain situations. Here’s the source for it:

// Itoa is equivalent to FormatInt(int64(i), 10).
func Itoa(i int) string {
	return FormatInt(int64(i), 10)
}

As you can see, Itoa only returns a string. Additionally, the function that Itoa calls also doesn’t return an error, just a string as well. In fact, the majority of the strconv package does this as well. Of the 31 functions available in the strconv package, only seven of them actually return an error. In fact, a quick browse of the stdlib shows this blatant disregard for the language’s own style conventions show up all throughout, with core top-level packages such as runtime, time, regexp, os , and many more.

Error Handling, Part 2

While we’re on the subject of error handling, let’s revisit our demo function once again.

func Foo() (string, error) {    
	val, err := Bar()    
    if err != nil {        
    	return val, err    
    }    
    return val, nil
}

Do you notice something there in the middle? if err != nil { ... } is a snippet that, as a Go developer, you should get very used to typing. Why is that? Because Go has no concept of try/except. Every time a function is called and returns an error for one of its values, you absolutely must check that value to see if it is non-nil. This means, for every function you call, you should be typing if err != nil { ... } and checking for that error. Are there ways to defer handling the errors? Sure, you can create a slice of errors and append returned errors to it then iterate thought the slice later looking for non-nil values, but this is not “idiomatic” and is an anti-pattern. Don’t Repeat Yourself should apply to more than just functions, too.

Dead Rising: Goto

“Surely Go, a language that came out in 2012, doesn’t have labels and go-to statements”, I can hear you saying. Well, guess what? It does. Not that you’d know that from reading the official style guides. In fact, “goto” isn’t mentioned once in Idiomatic Go. Yet when peering through the source for the Go stdlib, you find a myriad of uses of labels and goto statements in Go. The syscall package is probably the worst offender in this regard, but you can find it elsewhere as well. Goto statements have long been heralded as either obsolete or the herald of spaghetti-like webs of un-follow-able code and were assumed for a long time to be dead in modern languages. Yet here we are, in 2020, writing software with a language that makes liberal use of goto.

Go Has No Generic Types, Except When It Does

One of the main selling points of Go, at least on the label, is that it doesn’t have generic types. This means that every object must have a declared type and, hopefully, type errors and related issues can be caught by the compiler instead of at runtime. In theory this should make for more stable and performant programs. Except this isn’t exactly the case. Enter the interface{} .

Interfaces have two purposes in Go. The first is to allow for objects to implement the methods and characteristics of other objects. More simply put, it allows you to write an object that fulfills the interface for another object and this can be used instead of that object. A good example is that you can write an object that implements the same methods and values as json.Unmarshal and can be used in all the same places as it, allowing you to write a custom unmarshaling function. The second use for interfaces is as generic types; or, more appropriately, an undefined type.func Baz() (interface{}, error) {
   return "my name is Bob", nil
}

In the above function you can see that our function Baz is returning two values: one of type interface{} and an error. Looking at the return value you might assume that the real return value would be string, you would be wrong. Were we to call this function, the real values would be interface{} and error. We can then cast our interface to string, completing the conversion:data, err := Baz()
if err != nil {
   panic(err) // you shouldn't do this
}
newData := data.(string) + "!"
fmt.Println(newData)
// my name is Bob!

So what happens if we try to cast to a type that doesn’t match the underlying data, like int? Well, it panics. That’s because while we the developers see the type as interface{} Go actually sees it as a string. We can confirm this by using the reflect package to inspect data. We can use this to our advantage, however, especially when it comes to unpacking data we don’t already know the type of, like JSON. We can unpack raw JSON to a map[string]interface{} with no problem, despite the JSON data not being a true interface in the classical sense. From there we can inspect the underlying data types and cast as necessary.

So while Go doesn’t have generics, it has interfaces, which are practically just generics by another name.

Go Tooling. What Tooling?

Let’s be honest, there’s no way around this one: the tooling around Go sucks. From package managers that rely on VCS commits and releases to a complete lack of debugging tooling, the amount of holes in the tool ecosystem is astonishing.

gopls

Perhaps the biggest bane to my existence as a developer is the Go language server, gopls. It exists to make developing in a modern IDE like VS Code or Atom easier by providing real-time linting, formatting, documentation, and a bunch of other features, but the fact is that its a buggy and horrible to use piece of software. Want to have multiple Go projects open at the same time? Don’t. The language server completely freaks out and starts forgetting that the stdlib exists. There’s a reason that GO: Restart Language Server is the #1 most used item on my VS Code command list. Moreover, gopls doesn’t come with a way to disable certain checks. With its latest update gopls has started flagging what it considers “unnecessary complex” composite literals, like maps of structs. This completely ignores the fact that there are perfectly reasonable situations where such a thing might be useful, such as representing complex data structures. For those of us who enjoy seeing the “Problems” tab in VS Code read “0”, this is a serious issue. Moreover, when developing coding standards in a development team, you should be able to tell the compiler which things to warn on and which things to ignore; Go gives you no such options, even outside gopls.

Debugging

Its telling to me that the most used Golang debugger isn’t part of the Go project. Delve is the go-to debugger in VS Code and is overall rather excellent, but what I can’t understand is why Go didn’t ship with this functionality as part of its core toolset. A good debugger is invaluable for diagnosing bugs in your code, how a “modern” language shipped without one is beyond me. Actually, I was wrong, Go does have a debugger called GDB, but let me show you this from the documentation:

Go provides GDB support via the standard Go compiler and Gccgo. The stack management, threading, and runtime contain aspects that differ enough from the execution model GDB expects that they can confuse the debugger, even when the program is compiled with gccgo. Even though GDB can be used to debug Go programs, it is not ideal and may create confusion.

Tracing

Modern languages need to provide facilities for gathering metrics and tracing functions. Python, Java, NodeJS, Rust, are all (just to name a few) that allow for this kind of granular monitoring without an extensive and exhaustive function-by-function implementation. Adding tracing to Go programs is time intensive and requires heavy modification of your application code due to the requirements to pass span contexts from function to function. While tools like Jaeger, Skywalker, and Zipkin exist to take in these traces, the actual implementation of them come with such a great cost that its often not worth the effort. Packages like pprof can help but only to a certain extent and don’t provide insight into individual call stacks or requests like a good tracing library can. Implementing tracing in Go the right way involves either designing your program from Day 1 to have tracing enabled for a certain vendor or format selected, or writing costly (in both man-hours and performance) abstractions on tracing functions to instrument your code. Neither solution is such and should not be considered a real solution.

There’s so many more things to cover, but I want to hear from you. What are areas of Go that drive you f*****g bonkers that you’d like to see fixed?